usegamigameapi 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # useGameAPI
2
2
 
3
- A React hook for managing interactive quiz/game flows through iframe communication using postMessage. This hook handles question flow, answer submission, result tracking, and seamless communication between a parent window and an iframe.
3
+ A React hook for managing interactive quiz/game flows through iframe
4
+ communication using postMessage. This hook handles question flow, answer
5
+ submission, result tracking, and seamless communication between a parent window
6
+ and an iframe.
4
7
 
5
8
  ## Installation
6
9
 
@@ -15,25 +18,26 @@ npm install usegamigameapi
15
18
  ## Basic Usage
16
19
 
17
20
  ```tsx
18
- import { useGameAPI } from 'usegamigameapi';
21
+ import { useGameAPI } from "usegamigameapi";
19
22
 
20
23
  function QuizComponent() {
21
24
  const {
22
25
  quiz,
26
+ username,
23
27
  selectedAnswer,
24
28
  handleAnswerSelect,
25
29
  updateAnswer,
26
30
  handleContinue,
27
31
  isCompleted,
28
- currentResult
32
+ currentResult,
29
33
  } = useGameAPI({
30
34
  onAnswerCorrect: ({ currentQuestionIndex }) => {
31
- console.log('Correct answer at question', currentQuestionIndex);
35
+ console.log("Correct answer at question", currentQuestionIndex);
32
36
  // Trigger confetti or other celebrations
33
37
  },
34
38
  onAnswerIncorrect: () => {
35
- console.log('Incorrect answer');
36
- }
39
+ console.log("Incorrect answer");
40
+ },
37
41
  });
38
42
 
39
43
  // Render your quiz UI using the returned state and methods
@@ -46,10 +50,10 @@ function QuizComponent() {
46
50
 
47
51
  The `useGameAPI` hook accepts an object with the following optional callbacks:
48
52
 
49
- | Parameter | Type | Description |
50
- |-----------|------|-------------|
51
- | `onAnswerCorrect` | `(params: { currentQuestionIndex: number }) => void` | Callback fired when the user submits a correct answer |
52
- | `onAnswerIncorrect` | `() => void` | Callback fired when the user submits an incorrect answer |
53
+ | Parameter | Type | Description |
54
+ | ------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
55
+ | `onAnswerCorrect` | `(params: { currentQuestionIndex: number; correctAnswerId?: number; incorrectAnswerId?: number }) => void` | Callback fired when the user submits a correct answer |
56
+ | `onAnswerIncorrect` | `(params: { currentQuestionIndex: number; correctAnswerId?: number; incorrectAnswerId?: number }) => void` | Callback fired when the user submits an incorrect answer |
53
57
 
54
58
  ### Return Values
55
59
 
@@ -57,42 +61,44 @@ The hook returns an object with the following properties:
57
61
 
58
62
  #### State Data
59
63
 
60
- | Property | Type | Description |
61
- |----------|------|-------------|
62
- | `quiz` | `any` | The current question data received from the parent window (contains text, audio URL, answers array, etc.) |
63
- | `currentResult` | `any` | The result object after submitting an answer (contains `isCorrect`, `isLastQuestion`, and explanation) |
64
- | `answers` | `(boolean \| null)[]` | Array of answer history where each index corresponds to a question (true = correct, false = incorrect, null = unanswered) |
65
- | `correctCount` | `number` | Total count of correctly answered questions |
66
- | `currentQuestionIndex` | `number` | Zero-based index of the current question |
64
+ | Property | Type | Description |
65
+ | ---------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | `quiz` | `any` | The current question data received from the parent window (contains text, audio URL, answers array, etc.) |
67
+ | `currentResult` | `any` | The result object after submitting an answer (contains `isCorrect`, `isLastQuestion`, `correctAnswerId`, `incorrectAnswerId`, and explanation) |
68
+ | `answers` | `(boolean \| null)[]` | Array of answer history where each index corresponds to a question (true = correct, false = incorrect, null = unanswered) |
69
+ | `correctCount` | `number` | Total count of correctly answered questions |
70
+ | `currentQuestionIndex` | `number` | Zero-based index of the current question |
71
+ | `username` | `string` | The username received from the parent window during initialization |
67
72
 
68
73
  #### State Status
69
74
 
70
- | Property | Type | Description |
71
- |----------|------|-------------|
72
- | `selectedAnswer` | `{ id: number; content: string } \| null` | The currently selected answer object |
73
- | `isSubmitting` | `boolean` | Indicates whether an answer submission is in progress |
74
- | `hasSubmitted` | `boolean` | Indicates whether the current question has been submitted and is waiting for the Continue action |
75
- | `isCompleted` | `boolean` | Indicates whether all questions have been completed |
75
+ | Property | Type | Description |
76
+ | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
77
+ | `selectedAnswer` | `{ id: number; content: string } \| null` | The currently selected answer object |
78
+ | `isSubmitting` | `boolean` | Indicates whether an answer submission is in progress |
79
+ | `hasSubmitted` | `boolean` | Indicates whether the current question has been submitted and is waiting for the Continue action |
80
+ | `isCompleted` | `boolean` | Indicates whether all questions have been completed |
76
81
 
77
82
  #### Methods
78
83
 
79
- | Method | Type | Description |
80
- |--------|------|-------------|
81
- | `handleAnswerSelect` | `(answer: { id: number; content: string }) => void` | Selects an answer for the current question. Only works if not submitting and not already submitted |
82
- | `updateAnswer` | `() => Promise<void>` | Submits the selected answer to the parent window. Returns a promise that resolves immediately |
83
- | `handleContinue` | `() => void` | Moves to the next question or marks the quiz as completed if it's the last question. Only works after an answer has been submitted |
84
- | `finish` | `() => void` | Sends a finish signal to the parent window (typically used to resize iframe) |
84
+ | Method | Type | Description |
85
+ | -------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `handleAnswerSelect` | `(answer: { id: number; content: string }) => void` | Selects an answer for the current question. Only works if not submitting and not already submitted |
87
+ | `updateAnswer` | `() => Promise<void>` | Submits the selected answer to the parent window. Returns a promise that resolves immediately |
88
+ | `handleContinue` | `() => void` | Moves to the next question or marks the quiz as completed if it's the last question. Only works after an answer has been submitted |
89
+ | `finish` | `() => void` | Sends a finish signal to the parent window (typically used to resize iframe) |
85
90
 
86
91
  ## Usage Examples
87
92
 
88
93
  ### Complete Quiz Flow Example
89
94
 
90
95
  ```tsx
91
- import { useGameAPI } from 'usegamigameapi';
96
+ import { useGameAPI } from "usegamigameapi";
92
97
 
93
98
  function GameQuiz() {
94
99
  const {
95
100
  quiz,
101
+ username,
96
102
  selectedAnswer,
97
103
  currentResult,
98
104
  answers,
@@ -104,16 +110,22 @@ function GameQuiz() {
104
110
  handleAnswerSelect,
105
111
  updateAnswer,
106
112
  handleContinue,
107
- finish
113
+ finish,
108
114
  } = useGameAPI({
109
- onAnswerCorrect: ({ currentQuestionIndex }) => {
115
+ onAnswerCorrect: ({ currentQuestionIndex, correctAnswerId }) => {
110
116
  // Show success animation or confetti
111
- console.log(`Question ${currentQuestionIndex + 1} answered correctly!`);
117
+ console.log(`Question ${currentQuestionIndex + 1} answered correctly!`, {
118
+ correctAnswerId,
119
+ });
112
120
  },
113
- onAnswerIncorrect: () => {
121
+ onAnswerIncorrect: ({
122
+ currentQuestionIndex,
123
+ correctAnswerId,
124
+ incorrectAnswerId,
125
+ }) => {
114
126
  // Show error feedback
115
- console.log('Incorrect answer, try again next time!');
116
- }
127
+ console.log("Incorrect answer", { correctAnswerId, incorrectAnswerId });
128
+ },
117
129
  });
118
130
 
119
131
  // Show loading state while waiting for first question
@@ -126,7 +138,9 @@ function GameQuiz() {
126
138
  return (
127
139
  <div>
128
140
  <h2>Quiz Completed!</h2>
129
- <p>You got {correctCount} out of {answers.length} questions correct.</p>
141
+ <p>
142
+ You got {correctCount} out of {answers.length} questions correct.
143
+ </p>
130
144
  <button onClick={finish}>Finish</button>
131
145
  </div>
132
146
  );
@@ -137,14 +151,15 @@ function GameQuiz() {
137
151
  {/* Progress bar */}
138
152
  <div>
139
153
  Question {currentQuestionIndex + 1} of {answers.length + 1}
140
- <div style={{ display: 'flex', gap: '4px' }}>
154
+ <div style={{ display: "flex", gap: "4px" }}>
141
155
  {answers.map((answer, idx) => (
142
156
  <div
143
157
  key={idx}
144
158
  style={{
145
- width: '20px',
146
- height: '4px',
147
- backgroundColor: answer === true ? 'green' : answer === false ? 'red' : 'gray'
159
+ width: "20px",
160
+ height: "4px",
161
+ backgroundColor:
162
+ answer === true ? "green" : answer === false ? "red" : "gray",
148
163
  }}
149
164
  />
150
165
  ))}
@@ -163,10 +178,10 @@ function GameQuiz() {
163
178
  onClick={() => handleAnswerSelect(answer)}
164
179
  disabled={isSubmitting || hasSubmitted}
165
180
  style={{
166
- backgroundColor: selectedAnswer?.id === answer.id ? '#007bff' : '#f0f0f0',
167
- opacity: hasSubmitted ? 0.6 : 1
168
- }}
169
- >
181
+ backgroundColor:
182
+ selectedAnswer?.id === answer.id ? "#007bff" : "#f0f0f0",
183
+ opacity: hasSubmitted ? 0.6 : 1,
184
+ }}>
170
185
  {answer.content}
171
186
  </button>
172
187
  ))}
@@ -176,8 +191,7 @@ function GameQuiz() {
176
191
  {!hasSubmitted && (
177
192
  <button
178
193
  onClick={updateAnswer}
179
- disabled={!selectedAnswer || isSubmitting}
180
- >
194
+ disabled={!selectedAnswer || isSubmitting}>
181
195
  Submit Answer
182
196
  </button>
183
197
  )}
@@ -185,12 +199,10 @@ function GameQuiz() {
185
199
  {/* Result display and Continue button */}
186
200
  {hasSubmitted && currentResult && (
187
201
  <div>
188
- <p>
189
- {currentResult.isCorrect ? '✓ Correct!' : '✗ Incorrect'}
190
- </p>
202
+ <p>{currentResult.isCorrect ? "✓ Correct!" : "✗ Incorrect"}</p>
191
203
  {currentResult.explanation && <p>{currentResult.explanation}</p>}
192
204
  <button onClick={handleContinue}>
193
- {currentResult.isLastQuestion ? 'Finish Quiz' : 'Next Question'}
205
+ {currentResult.isLastQuestion ? "Finish Quiz" : "Next Question"}
194
206
  </button>
195
207
  </div>
196
208
  )}
@@ -205,10 +217,17 @@ function GameQuiz() {
205
217
  ### Minimal Example
206
218
 
207
219
  ```tsx
208
- import { useGameAPI } from 'usegamigameapi';
220
+ import { useGameAPI } from "usegamigameapi";
209
221
 
210
222
  function SimpleQuiz() {
211
- const { quiz, handleAnswerSelect, updateAnswer, handleContinue, selectedAnswer, hasSubmitted } = useGameAPI();
223
+ const {
224
+ quiz,
225
+ handleAnswerSelect,
226
+ updateAnswer,
227
+ handleContinue,
228
+ selectedAnswer,
229
+ hasSubmitted,
230
+ } = useGameAPI();
212
231
 
213
232
  return (
214
233
  <div>
@@ -217,8 +236,7 @@ function SimpleQuiz() {
217
236
  <button
218
237
  key={answer.id}
219
238
  onClick={() => handleAnswerSelect(answer)}
220
- disabled={hasSubmitted}
221
- >
239
+ disabled={hasSubmitted}>
222
240
  {answer.content}
223
241
  </button>
224
242
  ))}
@@ -227,9 +245,7 @@ function SimpleQuiz() {
227
245
  Submit
228
246
  </button>
229
247
  )}
230
- {hasSubmitted && (
231
- <button onClick={handleContinue}>Continue</button>
232
- )}
248
+ {hasSubmitted && <button onClick={handleContinue}>Continue</button>}
233
249
  </div>
234
250
  );
235
251
  }
@@ -237,21 +253,34 @@ function SimpleQuiz() {
237
253
 
238
254
  ## Architecture
239
255
 
240
- This hook uses a **bridge pattern** to facilitate communication between a parent window (Main Web) and a child iframe (Sub Web) through `window.postMessage`. The communication flow works as follows:
256
+ This hook uses a **bridge pattern** to facilitate communication between a parent
257
+ window (Main Web) and a child iframe (Sub Web) through `window.postMessage`. The
258
+ communication flow works as follows:
241
259
 
242
260
  ### Communication Flow
243
261
 
244
- 1. **Initialization**: When the hook mounts, it sends `SHOW_LO5` to request the first question
245
- 2. **Question Received**: Parent responds with `RETURN_LO5` containing question data
246
- 3. **Answer Submission**: User selects an answer and calls `updateAnswer()`, which sends `UPDATE_ANSWER` with the answer ID
247
- 4. **Result Received**: Parent responds with `RETURN_UPDATE_ANSWER` containing `isCorrect`, `isLastQuestion`, and result data
248
- 5. **Continue**: User calls `handleContinue()` which either requests the next question (`SHOW_LO5`) or marks the quiz as completed
249
- 6. **Finish**: When complete, `finish()` sends `FINISH` to signal completion (typically for iframe resizing)
262
+ 1. **Initialization**: When the hook mounts, it listens for `INIT` message from
263
+ the parent window containing game information (including `username`)
264
+ 2. **Start Game**: After initialization, the hook sends `SHOW_LO5` to request
265
+ the first question
266
+ 3. **Question Received**: Parent responds with `RETURN_LO5` containing question
267
+ data
268
+ 4. **Answer Submission**: User selects an answer and calls `updateAnswer()`,
269
+ which sends `UPDATE_ANSWER` with the answer ID
270
+ 5. **Result Received**: Parent responds with `RETURN_UPDATE_ANSWER` containing
271
+ `isCorrect`, `isLastQuestion`, `correctAnswerId`, `incorrectAnswerId`, and
272
+ result data
273
+ 6. **Continue**: User calls `handleContinue()` which either requests the next
274
+ question (`SHOW_LO5`) or marks the quiz as completed
275
+ 7. **Finish**: When complete, `finish()` sends `FINISH` to signal completion
276
+ (typically for iframe resizing)
250
277
 
251
278
  ### Action Types
252
279
 
253
280
  The bridge uses the following action constants (defined internally):
254
281
 
282
+ - `INIT`: Main → Sub - Initialize game with user information (includes
283
+ `username`)
255
284
  - `SHOW_LO5`: Sub → Main - Request a question
256
285
  - `RETURN_LO5`: Main → Sub - Send question data
257
286
  - `UPDATE_ANSWER`: Sub → Main - Submit selected answer
@@ -260,11 +289,15 @@ The bridge uses the following action constants (defined internally):
260
289
 
261
290
  ## Notes
262
291
 
263
- - The hook automatically requests the first question when mounted
292
+ - The hook automatically listens for `INIT` message from the parent window when
293
+ mounted to receive game initialization data (including username)
294
+ - The hook automatically requests the first question after initialization
264
295
  - Answer history is tracked in the `answers` array, indexed by question number
265
- - The hook uses both `useState` and `useRef` to ensure correct closure behavior across renders
296
+ - The hook uses both `useState` and `useRef` to ensure correct closure behavior
297
+ across renders
266
298
  - All communication is asynchronous through postMessage events
267
- - Make sure your parent window is set up to handle the corresponding postMessage events
299
+ - Make sure your parent window is set up to send `INIT` message with game data
300
+ and handle the corresponding postMessage events
268
301
 
269
302
  ## License
270
303
 
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "usegamigameapi",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
7
- "files": ["dist"],
7
+ "files": [
8
+ "dist"
9
+ ],
8
10
  "scripts": {
9
- "build": "tsup",
11
+ "build": "tsup",
10
12
  "publish": "npm run build && npm publish --access public"
11
13
  },
12
14
  "peerDependencies": {
@@ -19,4 +21,4 @@
19
21
  "tsup": "^8.5.1",
20
22
  "typescript": "^5.9.3"
21
23
  }
22
- }
24
+ }
package/dist/index.d.mts DELETED
@@ -1,23 +0,0 @@
1
- declare function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }: any): {
2
- quiz: any;
3
- currentResult: any;
4
- answers: (boolean | null)[];
5
- correctCount: number;
6
- currentQuestionIndex: number;
7
- selectedAnswer: {
8
- id: number;
9
- content: string;
10
- } | null;
11
- isSubmitting: boolean;
12
- hasSubmitted: boolean;
13
- isCompleted: boolean;
14
- handleAnswerSelect: (answer: {
15
- id: number;
16
- content: string;
17
- }) => void;
18
- updateAnswer: () => Promise<void>;
19
- handleContinue: () => void;
20
- finish: () => void;
21
- };
22
-
23
- export { useGameAPI };
package/dist/index.d.ts DELETED
@@ -1,23 +0,0 @@
1
- declare function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }: any): {
2
- quiz: any;
3
- currentResult: any;
4
- answers: (boolean | null)[];
5
- correctCount: number;
6
- currentQuestionIndex: number;
7
- selectedAnswer: {
8
- id: number;
9
- content: string;
10
- } | null;
11
- isSubmitting: boolean;
12
- hasSubmitted: boolean;
13
- isCompleted: boolean;
14
- handleAnswerSelect: (answer: {
15
- id: number;
16
- content: string;
17
- }) => void;
18
- updateAnswer: () => Promise<void>;
19
- handleContinue: () => void;
20
- finish: () => void;
21
- };
22
-
23
- export { useGameAPI };
package/dist/index.js DELETED
@@ -1,179 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- useGameAPI: () => useGameAPI
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
-
27
- // src/useGameAPI.ts
28
- var import_react = require("react");
29
-
30
- // src/bridge.ts
31
- var ACTIONS = {
32
- SHOW_LO5: "SHOW_LO5",
33
- // Sub -> Main: Web con đã sẵn sàng, yêu cầu web cha gửi câu hỏi
34
- RETURN_LO5: "RETURN_LO5",
35
- // Main -> Sub: Web cha gửi nội dung câu hỏi
36
- UPDATE_ANSWER: "UPDATE_ANSWER",
37
- // Sub -> Main: Gửi đáp án user chọn cho web cha
38
- RETURN_UPDATE_ANSWER: "RETURN_UPDATE_ANSWER",
39
- // Main -> Sub: Web cha gửi lại kết quả đúng/sai cho web con
40
- FINISH: "FINISH"
41
- // Sub -> Main: Yêu cầu resize iframe
42
- };
43
- var QuestionBridge = class {
44
- constructor() {
45
- this.handlers = {};
46
- window.addEventListener("message", this.handleMessage.bind(this));
47
- }
48
- // Hàm nhận message
49
- handleMessage(event) {
50
- const { type, payload } = event.data;
51
- if (this.handlers[type]) {
52
- this.handlers[type](payload);
53
- }
54
- }
55
- // Đăng ký hàm xử lý cho từng loại sự kiện (để UI gọi)
56
- // stw -> lovable
57
- on(type, callback) {
58
- this.handlers[type] = callback;
59
- }
60
- // lovable -> stw
61
- // Gửi message lên Main Web
62
- send(type, payload = {}) {
63
- if (window.parent) {
64
- window.parent.postMessage({ type, payload }, "*");
65
- }
66
- }
67
- };
68
- var bridge = new QuestionBridge();
69
-
70
- // src/useGameAPI.ts
71
- console.log("useGameAPI-V1.0.13");
72
- function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }) {
73
- const [currentQuestionIndex, setCurrentQuestionIndex] = (0, import_react.useState)(0);
74
- const currentQuestionIndexRef = (0, import_react.useRef)(0);
75
- const [selectedAnswer, setSelectedAnswer] = (0, import_react.useState)(null);
76
- const [answers, setAnswers] = (0, import_react.useState)([]);
77
- const [quiz, setQuiz] = (0, import_react.useState)(null);
78
- const [currentResult, setCurrentResult] = (0, import_react.useState)(null);
79
- const [correctCount, setCorrectCount] = (0, import_react.useState)(0);
80
- const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
81
- const [hasSubmitted, setHasSubmitted] = (0, import_react.useState)(false);
82
- const [isCompleted, setIsCompleted] = (0, import_react.useState)(false);
83
- const isLastQuestionRef = (0, import_react.useRef)(false);
84
- const finish = (0, import_react.useCallback)(() => {
85
- bridge.send(ACTIONS.FINISH);
86
- }, []);
87
- const updateAnswer = (0, import_react.useCallback)(async () => {
88
- if (!selectedAnswer || isSubmitting) {
89
- return;
90
- }
91
- bridge.send(ACTIONS.UPDATE_ANSWER, selectedAnswer.id);
92
- setIsSubmitting(true);
93
- setHasSubmitted(true);
94
- return Promise.resolve();
95
- }, [selectedAnswer, isSubmitting]);
96
- const handleContinue = (0, import_react.useCallback)(() => {
97
- if (!hasSubmitted) return;
98
- if (isLastQuestionRef.current) {
99
- setIsCompleted(true);
100
- } else {
101
- bridge.send(ACTIONS.SHOW_LO5);
102
- const nextIndex = currentQuestionIndexRef.current + 1;
103
- currentQuestionIndexRef.current = nextIndex;
104
- setCurrentQuestionIndex(nextIndex);
105
- setSelectedAnswer(null);
106
- setIsSubmitting(false);
107
- setHasSubmitted(false);
108
- setCurrentResult(null);
109
- }
110
- }, [hasSubmitted]);
111
- const handleAnswerSelect = (0, import_react.useCallback)(
112
- (answer) => {
113
- if (!isSubmitting && !hasSubmitted) {
114
- setSelectedAnswer(answer);
115
- }
116
- },
117
- [isSubmitting, hasSubmitted]
118
- );
119
- (0, import_react.useEffect)(() => {
120
- bridge.send(ACTIONS.SHOW_LO5);
121
- bridge.on(ACTIONS.RETURN_LO5, (_data) => {
122
- setQuiz(_data);
123
- });
124
- bridge.on(ACTIONS.RETURN_UPDATE_ANSWER, (_data) => {
125
- const isCorrect = (_data == null ? void 0 : _data.isCorrect) === true;
126
- const isLast = (_data == null ? void 0 : _data.isLastQuestion) === true;
127
- isLastQuestionRef.current = isLast;
128
- setAnswers((prevAnswers) => {
129
- const next = [...prevAnswers];
130
- next[currentQuestionIndexRef.current] = isCorrect;
131
- return next;
132
- });
133
- if (isCorrect) {
134
- setCorrectCount((prev) => prev + 1);
135
- console.log({ currentQuestionIndex: currentQuestionIndexRef.current });
136
- onAnswerCorrect == null ? void 0 : onAnswerCorrect({ currentQuestionIndex: currentQuestionIndexRef.current });
137
- } else {
138
- onAnswerIncorrect == null ? void 0 : onAnswerIncorrect();
139
- }
140
- setIsSubmitting(false);
141
- setCurrentResult(_data);
142
- });
143
- }, []);
144
- return {
145
- // State Data
146
- quiz,
147
- // Dữ liệu câu hỏi hiện tại (để render UI)
148
- currentResult,
149
- // Kết quả vừa trả lời (để hiện giải thích đúng/sai)
150
- answers,
151
- // Mảng lịch sử kết quả (để vẽ progress bar)
152
- correctCount,
153
- // Số câu đúng
154
- currentQuestionIndex,
155
- // Index hiện tại (để UI biết đang ở câu mấy)
156
- // State Status
157
- selectedAnswer,
158
- // Đáp án đang được chọn
159
- isSubmitting,
160
- // Trạng thái đang submmit
161
- hasSubmitted,
162
- isCompleted,
163
- // Trạng thái khi hoàn thành câu hỏi cuối
164
- // Methods
165
- handleAnswerSelect,
166
- // Hàm gán đáp án user đã chọn
167
- updateAnswer,
168
- // Hàm gửi đáp án user chọn lên web cha
169
- handleContinue,
170
- // Hàm chuyển qua câu hỏi tiếp theo hoặc kết thúc
171
- finish
172
- // Hàm kết thúc
173
- };
174
- }
175
- // Annotate the CommonJS export names for ESM import in node:
176
- 0 && (module.exports = {
177
- useGameAPI
178
- });
179
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/useGameAPI.ts","../src/bridge.ts"],"sourcesContent":["export * from \"./useGameAPI\";","import { useState, useCallback, useRef, useEffect } from \"react\";\n// Lưu ý: Đảm bảo đường dẫn import bridge đúng với cấu trúc dự án của bạn\nimport { bridge, ACTIONS } from \"./bridge\";\n\nconsole.log('useGameAPI-V1.0.13');\n\nexport function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }: any) {\n // ================= STATE =================\n const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); // State để UI re-render\n const currentQuestionIndexRef = useRef(0); // Ref để logic chạy đúng trong closure\n\n const [selectedAnswer, setSelectedAnswer] = useState<{ id: number; content: string } | null>(null);\n const [answers, setAnswers] = useState<(boolean | null)[]>([]); // Lưu lịch sử đúng/sai\n\n // Game Status\n const [quiz, setQuiz] = useState<any>(null); // Data câu hỏi hiện tại từ Main Web\n const [currentResult, setCurrentResult] = useState<any>(null); // Kết quả trả về sau khi submit\n const [correctCount, setCorrectCount] = useState(0);\n\n // Flow Flags\n const [isSubmitting, setIsSubmitting] = useState(false); // Đang chờ kết quả\n const [hasSubmitted, setHasSubmitted] = useState(false); // Đã có kết quả, chờ bấm Continue\n const [isCompleted, setIsCompleted] = useState(false); // Game hoàn tất\n\n // Biến cờ tạm lưu trạng thái câu cuối cùng từ server trả về\n const isLastQuestionRef = useRef(false);\n\n // ================= ACTIONS =================\n\n const finish = useCallback(() => {\n bridge.send(ACTIONS.FINISH);\n }, []);\n\n // 1. Gửi đáp án lên Main Web\n const updateAnswer = useCallback(async () => {\n if (!selectedAnswer || isSubmitting) {\n return;\n }\n\n bridge.send(ACTIONS.UPDATE_ANSWER, selectedAnswer.id);\n setIsSubmitting(true);\n setHasSubmitted(true);\n\n // Trả về promise giả để UI có thể await nếu cần (ví dụ trigger confetti ngay lập tức)\n return Promise.resolve();\n }, [selectedAnswer, isSubmitting]);\n\n // 2. Chuyển câu hỏi hoặc Kết thúc\n const handleContinue = useCallback(() => {\n if (!hasSubmitted) return;\n\n // Check cờ isLastQuestion được set từ lúc nhận kết quả\n if (isLastQuestionRef.current) {\n setIsCompleted(true);\n } else {\n // Yêu cầu câu hỏi tiếp theo\n bridge.send(ACTIONS.SHOW_LO5);\n\n const nextIndex = currentQuestionIndexRef.current + 1;\n currentQuestionIndexRef.current = nextIndex;\n setCurrentQuestionIndex(nextIndex); // Sync state for UI\n\n // Reset state cho câu mới\n setSelectedAnswer(null);\n setIsSubmitting(false);\n setHasSubmitted(false);\n setCurrentResult(null);\n }\n }, [hasSubmitted]);\n\n // 3. UI Handler: Chọn đáp án\n const handleAnswerSelect = useCallback(\n (answer: { id: number; content: string }) => {\n if (!isSubmitting && !hasSubmitted) {\n setSelectedAnswer(answer);\n }\n },\n [isSubmitting, hasSubmitted],\n );\n\n // ================= LISTENERS =================\n useEffect(() => {\n // 1. Start Game: Yêu cầu câu hỏi đầu tiên\n bridge.send(ACTIONS.SHOW_LO5);\n\n // 2. Nhận câu hỏi mới\n bridge.on(ACTIONS.RETURN_LO5, (_data) => {\n // _data ở đây là nội dung câu hỏi (text, audio url, answers array...)\n setQuiz(_data);\n });\n\n // 3. Nhận kết quả trả lời\n bridge.on(ACTIONS.RETURN_UPDATE_ANSWER, (_data) => {\n const isCorrect = _data?.isCorrect === true;\n const isLast = _data?.isLastQuestion === true;\n\n // Lưu cờ báo hiệu câu cuối\n isLastQuestionRef.current = isLast;\n\n // Cập nhật lịch sử trả lời\n setAnswers((prevAnswers) => {\n const next = [...prevAnswers];\n // Đảm bảo array đủ dài (phòng trường hợp nhảy cóc - dù khó xảy ra)\n next[currentQuestionIndexRef.current] = isCorrect;\n return next;\n });\n\n // Xử lý đếm đúng\n if (isCorrect) {\n setCorrectCount((prev) => prev + 1);\n console.log({ currentQuestionIndex: currentQuestionIndexRef.current });\n onAnswerCorrect?.({ currentQuestionIndex: currentQuestionIndexRef.current });\n } else {\n onAnswerIncorrect?.();\n }\n\n // Update UI Status\n setIsSubmitting(false);\n setCurrentResult(_data);\n });\n }, []); // Run once on mount\n\n return {\n // State Data\n quiz, // Dữ liệu câu hỏi hiện tại (để render UI)\n currentResult, // Kết quả vừa trả lời (để hiện giải thích đúng/sai)\n answers, // Mảng lịch sử kết quả (để vẽ progress bar)\n correctCount, // Số câu đúng\n currentQuestionIndex, // Index hiện tại (để UI biết đang ở câu mấy)\n\n // State Status\n selectedAnswer, // Đáp án đang được chọn\n isSubmitting, // Trạng thái đang submmit\n hasSubmitted,\n isCompleted, // Trạng thái khi hoàn thành câu hỏi cuối\n\n // Methods\n handleAnswerSelect, // Hàm gán đáp án user đã chọn\n updateAnswer, // Hàm gửi đáp án user chọn lên web cha\n handleContinue, // Hàm chuyển qua câu hỏi tiếp theo hoặc kết thúc\n finish, // Hàm kết thúc\n };\n}\n","// src/bridge.ts\n\n// Định nghĩa các loại action\nexport const ACTIONS = {\n SHOW_LO5: \"SHOW_LO5\", // Sub -> Main: Web con đã sẵn sàng, yêu cầu web cha gửi câu hỏi\n RETURN_LO5: \"RETURN_LO5\", // Main -> Sub: Web cha gửi nội dung câu hỏi\n UPDATE_ANSWER: \"UPDATE_ANSWER\", // Sub -> Main: Gửi đáp án user chọn cho web cha\n RETURN_UPDATE_ANSWER: \"RETURN_UPDATE_ANSWER\", // Main -> Sub: Web cha gửi lại kết quả đúng/sai cho web con\n FINISH: \"FINISH\", // Sub -> Main: Yêu cầu resize iframe\n};\n\nclass QuestionBridge {\n private handlers: Record<string, (payload: any) => void> = {};\n\n constructor() {\n // Lắng nghe message từ Main Web\n window.addEventListener(\"message\", this.handleMessage.bind(this));\n }\n\n // Hàm nhận message\n private handleMessage(event: MessageEvent) {\n const { type, payload } = event.data;\n if (this.handlers[type]) {\n this.handlers[type](payload);\n }\n }\n\n // Đăng ký hàm xử lý cho từng loại sự kiện (để UI gọi)\n // stw -> lovable\n on(type: string, callback: (payload: any) => void) {\n this.handlers[type] = callback;\n }\n\n // lovable -> stw\n // Gửi message lên Main Web\n send(type: string, payload: any = {}) {\n if (window.parent) {\n window.parent.postMessage({ type, payload }, \"*\"); // Thay '*' bằng domain Main Web khi prod\n }\n }\n}\n\n// Singleton instance để dùng toàn app\nexport const bridge = new QuestionBridge();\n"],"mappings":"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","names":[]}
package/dist/index.mjs DELETED
@@ -1,152 +0,0 @@
1
- // src/useGameAPI.ts
2
- import { useState, useCallback, useRef, useEffect } from "react";
3
-
4
- // src/bridge.ts
5
- var ACTIONS = {
6
- SHOW_LO5: "SHOW_LO5",
7
- // Sub -> Main: Web con đã sẵn sàng, yêu cầu web cha gửi câu hỏi
8
- RETURN_LO5: "RETURN_LO5",
9
- // Main -> Sub: Web cha gửi nội dung câu hỏi
10
- UPDATE_ANSWER: "UPDATE_ANSWER",
11
- // Sub -> Main: Gửi đáp án user chọn cho web cha
12
- RETURN_UPDATE_ANSWER: "RETURN_UPDATE_ANSWER",
13
- // Main -> Sub: Web cha gửi lại kết quả đúng/sai cho web con
14
- FINISH: "FINISH"
15
- // Sub -> Main: Yêu cầu resize iframe
16
- };
17
- var QuestionBridge = class {
18
- constructor() {
19
- this.handlers = {};
20
- window.addEventListener("message", this.handleMessage.bind(this));
21
- }
22
- // Hàm nhận message
23
- handleMessage(event) {
24
- const { type, payload } = event.data;
25
- if (this.handlers[type]) {
26
- this.handlers[type](payload);
27
- }
28
- }
29
- // Đăng ký hàm xử lý cho từng loại sự kiện (để UI gọi)
30
- // stw -> lovable
31
- on(type, callback) {
32
- this.handlers[type] = callback;
33
- }
34
- // lovable -> stw
35
- // Gửi message lên Main Web
36
- send(type, payload = {}) {
37
- if (window.parent) {
38
- window.parent.postMessage({ type, payload }, "*");
39
- }
40
- }
41
- };
42
- var bridge = new QuestionBridge();
43
-
44
- // src/useGameAPI.ts
45
- console.log("useGameAPI-V1.0.13");
46
- function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }) {
47
- const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
48
- const currentQuestionIndexRef = useRef(0);
49
- const [selectedAnswer, setSelectedAnswer] = useState(null);
50
- const [answers, setAnswers] = useState([]);
51
- const [quiz, setQuiz] = useState(null);
52
- const [currentResult, setCurrentResult] = useState(null);
53
- const [correctCount, setCorrectCount] = useState(0);
54
- const [isSubmitting, setIsSubmitting] = useState(false);
55
- const [hasSubmitted, setHasSubmitted] = useState(false);
56
- const [isCompleted, setIsCompleted] = useState(false);
57
- const isLastQuestionRef = useRef(false);
58
- const finish = useCallback(() => {
59
- bridge.send(ACTIONS.FINISH);
60
- }, []);
61
- const updateAnswer = useCallback(async () => {
62
- if (!selectedAnswer || isSubmitting) {
63
- return;
64
- }
65
- bridge.send(ACTIONS.UPDATE_ANSWER, selectedAnswer.id);
66
- setIsSubmitting(true);
67
- setHasSubmitted(true);
68
- return Promise.resolve();
69
- }, [selectedAnswer, isSubmitting]);
70
- const handleContinue = useCallback(() => {
71
- if (!hasSubmitted) return;
72
- if (isLastQuestionRef.current) {
73
- setIsCompleted(true);
74
- } else {
75
- bridge.send(ACTIONS.SHOW_LO5);
76
- const nextIndex = currentQuestionIndexRef.current + 1;
77
- currentQuestionIndexRef.current = nextIndex;
78
- setCurrentQuestionIndex(nextIndex);
79
- setSelectedAnswer(null);
80
- setIsSubmitting(false);
81
- setHasSubmitted(false);
82
- setCurrentResult(null);
83
- }
84
- }, [hasSubmitted]);
85
- const handleAnswerSelect = useCallback(
86
- (answer) => {
87
- if (!isSubmitting && !hasSubmitted) {
88
- setSelectedAnswer(answer);
89
- }
90
- },
91
- [isSubmitting, hasSubmitted]
92
- );
93
- useEffect(() => {
94
- bridge.send(ACTIONS.SHOW_LO5);
95
- bridge.on(ACTIONS.RETURN_LO5, (_data) => {
96
- setQuiz(_data);
97
- });
98
- bridge.on(ACTIONS.RETURN_UPDATE_ANSWER, (_data) => {
99
- const isCorrect = (_data == null ? void 0 : _data.isCorrect) === true;
100
- const isLast = (_data == null ? void 0 : _data.isLastQuestion) === true;
101
- isLastQuestionRef.current = isLast;
102
- setAnswers((prevAnswers) => {
103
- const next = [...prevAnswers];
104
- next[currentQuestionIndexRef.current] = isCorrect;
105
- return next;
106
- });
107
- if (isCorrect) {
108
- setCorrectCount((prev) => prev + 1);
109
- console.log({ currentQuestionIndex: currentQuestionIndexRef.current });
110
- onAnswerCorrect == null ? void 0 : onAnswerCorrect({ currentQuestionIndex: currentQuestionIndexRef.current });
111
- } else {
112
- onAnswerIncorrect == null ? void 0 : onAnswerIncorrect();
113
- }
114
- setIsSubmitting(false);
115
- setCurrentResult(_data);
116
- });
117
- }, []);
118
- return {
119
- // State Data
120
- quiz,
121
- // Dữ liệu câu hỏi hiện tại (để render UI)
122
- currentResult,
123
- // Kết quả vừa trả lời (để hiện giải thích đúng/sai)
124
- answers,
125
- // Mảng lịch sử kết quả (để vẽ progress bar)
126
- correctCount,
127
- // Số câu đúng
128
- currentQuestionIndex,
129
- // Index hiện tại (để UI biết đang ở câu mấy)
130
- // State Status
131
- selectedAnswer,
132
- // Đáp án đang được chọn
133
- isSubmitting,
134
- // Trạng thái đang submmit
135
- hasSubmitted,
136
- isCompleted,
137
- // Trạng thái khi hoàn thành câu hỏi cuối
138
- // Methods
139
- handleAnswerSelect,
140
- // Hàm gán đáp án user đã chọn
141
- updateAnswer,
142
- // Hàm gửi đáp án user chọn lên web cha
143
- handleContinue,
144
- // Hàm chuyển qua câu hỏi tiếp theo hoặc kết thúc
145
- finish
146
- // Hàm kết thúc
147
- };
148
- }
149
- export {
150
- useGameAPI
151
- };
152
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useGameAPI.ts","../src/bridge.ts"],"sourcesContent":["import { useState, useCallback, useRef, useEffect } from \"react\";\n// Lưu ý: Đảm bảo đường dẫn import bridge đúng với cấu trúc dự án của bạn\nimport { bridge, ACTIONS } from \"./bridge\";\n\nconsole.log('useGameAPI-V1.0.13');\n\nexport function useGameAPI({ onAnswerCorrect, onAnswerIncorrect }: any) {\n // ================= STATE =================\n const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); // State để UI re-render\n const currentQuestionIndexRef = useRef(0); // Ref để logic chạy đúng trong closure\n\n const [selectedAnswer, setSelectedAnswer] = useState<{ id: number; content: string } | null>(null);\n const [answers, setAnswers] = useState<(boolean | null)[]>([]); // Lưu lịch sử đúng/sai\n\n // Game Status\n const [quiz, setQuiz] = useState<any>(null); // Data câu hỏi hiện tại từ Main Web\n const [currentResult, setCurrentResult] = useState<any>(null); // Kết quả trả về sau khi submit\n const [correctCount, setCorrectCount] = useState(0);\n\n // Flow Flags\n const [isSubmitting, setIsSubmitting] = useState(false); // Đang chờ kết quả\n const [hasSubmitted, setHasSubmitted] = useState(false); // Đã có kết quả, chờ bấm Continue\n const [isCompleted, setIsCompleted] = useState(false); // Game hoàn tất\n\n // Biến cờ tạm lưu trạng thái câu cuối cùng từ server trả về\n const isLastQuestionRef = useRef(false);\n\n // ================= ACTIONS =================\n\n const finish = useCallback(() => {\n bridge.send(ACTIONS.FINISH);\n }, []);\n\n // 1. Gửi đáp án lên Main Web\n const updateAnswer = useCallback(async () => {\n if (!selectedAnswer || isSubmitting) {\n return;\n }\n\n bridge.send(ACTIONS.UPDATE_ANSWER, selectedAnswer.id);\n setIsSubmitting(true);\n setHasSubmitted(true);\n\n // Trả về promise giả để UI có thể await nếu cần (ví dụ trigger confetti ngay lập tức)\n return Promise.resolve();\n }, [selectedAnswer, isSubmitting]);\n\n // 2. Chuyển câu hỏi hoặc Kết thúc\n const handleContinue = useCallback(() => {\n if (!hasSubmitted) return;\n\n // Check cờ isLastQuestion được set từ lúc nhận kết quả\n if (isLastQuestionRef.current) {\n setIsCompleted(true);\n } else {\n // Yêu cầu câu hỏi tiếp theo\n bridge.send(ACTIONS.SHOW_LO5);\n\n const nextIndex = currentQuestionIndexRef.current + 1;\n currentQuestionIndexRef.current = nextIndex;\n setCurrentQuestionIndex(nextIndex); // Sync state for UI\n\n // Reset state cho câu mới\n setSelectedAnswer(null);\n setIsSubmitting(false);\n setHasSubmitted(false);\n setCurrentResult(null);\n }\n }, [hasSubmitted]);\n\n // 3. UI Handler: Chọn đáp án\n const handleAnswerSelect = useCallback(\n (answer: { id: number; content: string }) => {\n if (!isSubmitting && !hasSubmitted) {\n setSelectedAnswer(answer);\n }\n },\n [isSubmitting, hasSubmitted],\n );\n\n // ================= LISTENERS =================\n useEffect(() => {\n // 1. Start Game: Yêu cầu câu hỏi đầu tiên\n bridge.send(ACTIONS.SHOW_LO5);\n\n // 2. Nhận câu hỏi mới\n bridge.on(ACTIONS.RETURN_LO5, (_data) => {\n // _data ở đây là nội dung câu hỏi (text, audio url, answers array...)\n setQuiz(_data);\n });\n\n // 3. Nhận kết quả trả lời\n bridge.on(ACTIONS.RETURN_UPDATE_ANSWER, (_data) => {\n const isCorrect = _data?.isCorrect === true;\n const isLast = _data?.isLastQuestion === true;\n\n // Lưu cờ báo hiệu câu cuối\n isLastQuestionRef.current = isLast;\n\n // Cập nhật lịch sử trả lời\n setAnswers((prevAnswers) => {\n const next = [...prevAnswers];\n // Đảm bảo array đủ dài (phòng trường hợp nhảy cóc - dù khó xảy ra)\n next[currentQuestionIndexRef.current] = isCorrect;\n return next;\n });\n\n // Xử lý đếm đúng\n if (isCorrect) {\n setCorrectCount((prev) => prev + 1);\n console.log({ currentQuestionIndex: currentQuestionIndexRef.current });\n onAnswerCorrect?.({ currentQuestionIndex: currentQuestionIndexRef.current });\n } else {\n onAnswerIncorrect?.();\n }\n\n // Update UI Status\n setIsSubmitting(false);\n setCurrentResult(_data);\n });\n }, []); // Run once on mount\n\n return {\n // State Data\n quiz, // Dữ liệu câu hỏi hiện tại (để render UI)\n currentResult, // Kết quả vừa trả lời (để hiện giải thích đúng/sai)\n answers, // Mảng lịch sử kết quả (để vẽ progress bar)\n correctCount, // Số câu đúng\n currentQuestionIndex, // Index hiện tại (để UI biết đang ở câu mấy)\n\n // State Status\n selectedAnswer, // Đáp án đang được chọn\n isSubmitting, // Trạng thái đang submmit\n hasSubmitted,\n isCompleted, // Trạng thái khi hoàn thành câu hỏi cuối\n\n // Methods\n handleAnswerSelect, // Hàm gán đáp án user đã chọn\n updateAnswer, // Hàm gửi đáp án user chọn lên web cha\n handleContinue, // Hàm chuyển qua câu hỏi tiếp theo hoặc kết thúc\n finish, // Hàm kết thúc\n };\n}\n","// src/bridge.ts\n\n// Định nghĩa các loại action\nexport const ACTIONS = {\n SHOW_LO5: \"SHOW_LO5\", // Sub -> Main: Web con đã sẵn sàng, yêu cầu web cha gửi câu hỏi\n RETURN_LO5: \"RETURN_LO5\", // Main -> Sub: Web cha gửi nội dung câu hỏi\n UPDATE_ANSWER: \"UPDATE_ANSWER\", // Sub -> Main: Gửi đáp án user chọn cho web cha\n RETURN_UPDATE_ANSWER: \"RETURN_UPDATE_ANSWER\", // Main -> Sub: Web cha gửi lại kết quả đúng/sai cho web con\n FINISH: \"FINISH\", // Sub -> Main: Yêu cầu resize iframe\n};\n\nclass QuestionBridge {\n private handlers: Record<string, (payload: any) => void> = {};\n\n constructor() {\n // Lắng nghe message từ Main Web\n window.addEventListener(\"message\", this.handleMessage.bind(this));\n }\n\n // Hàm nhận message\n private handleMessage(event: MessageEvent) {\n const { type, payload } = event.data;\n if (this.handlers[type]) {\n this.handlers[type](payload);\n }\n }\n\n // Đăng ký hàm xử lý cho từng loại sự kiện (để UI gọi)\n // stw -> lovable\n on(type: string, callback: (payload: any) => void) {\n this.handlers[type] = callback;\n }\n\n // lovable -> stw\n // Gửi message lên Main Web\n send(type: string, payload: any = {}) {\n if (window.parent) {\n window.parent.postMessage({ type, payload }, \"*\"); // Thay '*' bằng domain Main Web khi prod\n }\n }\n}\n\n// Singleton instance để dùng toàn app\nexport const bridge = new QuestionBridge();\n"],"mappings":"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","names":[]}