@mblinkov/whats-missing 20.0.14 → 20.0.16
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/dist/whats-missing.cjs.js +4 -17
- package/dist/whats-missing.es.js +232 -314
- package/package.json +1 -1
- package/src/Game.tsx +203 -285
package/package.json
CHANGED
package/src/Game.tsx
CHANGED
|
@@ -11,42 +11,28 @@ type RoundResult = {
|
|
|
11
11
|
result: "correct" | "almost" | "wrong";
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
// ✅
|
|
14
|
+
// ✅ базовый reset
|
|
15
15
|
const globalReset = () => {
|
|
16
16
|
const style = document.createElement("style");
|
|
17
17
|
style.textContent = `
|
|
18
|
-
html, body {
|
|
19
|
-
margin: 0;
|
|
20
|
-
padding: 0;
|
|
21
|
-
width: 100%;
|
|
22
|
-
height: 100%;
|
|
23
|
-
overflow: hidden;
|
|
24
|
-
background: radial-gradient(circle at center, #fff8f8 0%, #fdfdfd 80%, #ffffff 100%);
|
|
25
|
-
background-repeat: no-repeat;
|
|
26
|
-
background-size: cover;
|
|
27
|
-
transform-origin: center;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
#whats-missing-root, #whats-missing-root * {
|
|
31
19
|
box-sizing: border-box;
|
|
32
20
|
font-family: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
33
|
-
transform: none !important;
|
|
34
21
|
}
|
|
35
|
-
|
|
36
22
|
#whats-missing-root img {
|
|
37
23
|
max-width: 100%;
|
|
38
24
|
height: auto;
|
|
39
25
|
display: block;
|
|
40
26
|
user-select: none;
|
|
41
27
|
}
|
|
28
|
+
html, body { margin: 0; padding: 0; }
|
|
42
29
|
`;
|
|
43
30
|
document.head.appendChild(style);
|
|
44
31
|
};
|
|
45
32
|
|
|
33
|
+
// простая функция сравнения
|
|
46
34
|
const levenshtein = (a: string, b: string) => {
|
|
47
|
-
const dp = Array.from({ length: a.length + 1 }, () =>
|
|
48
|
-
Array(b.length + 1).fill(0)
|
|
49
|
-
);
|
|
35
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
50
36
|
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
51
37
|
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
52
38
|
for (let i = 1; i <= a.length; i++) {
|
|
@@ -85,18 +71,31 @@ export default function Game() {
|
|
|
85
71
|
const [inputValue, setInputValue] = useState("");
|
|
86
72
|
const [answered, setAnswered] = useState(false);
|
|
87
73
|
const [animating, setAnimating] = useState(false);
|
|
88
|
-
const [result, setResult] = useState<"correct" | "almost" | "wrong" | null>(
|
|
89
|
-
null
|
|
90
|
-
);
|
|
74
|
+
const [result, setResult] = useState<"correct" | "almost" | "wrong" | null>(null);
|
|
91
75
|
const [resultsTable, setResultsTable] = useState<RoundResult[]>([]);
|
|
92
76
|
const [usedHidden, setUsedHidden] = useState<string[]>([]);
|
|
93
77
|
const [isMobile, setIsMobile] = useState(false);
|
|
78
|
+
const [scale, setScale] = useState(1);
|
|
94
79
|
|
|
80
|
+
// ✅ адаптив под мобилки и десктоп
|
|
95
81
|
useEffect(() => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
const resize = () => {
|
|
83
|
+
const mobile = window.innerWidth < 768;
|
|
84
|
+
setIsMobile(mobile);
|
|
85
|
+
|
|
86
|
+
if (!mobile) {
|
|
87
|
+
// виртуальный размер игры (1440x768)
|
|
88
|
+
const baseW = 1440;
|
|
89
|
+
const baseH = 768;
|
|
90
|
+
const ratio = Math.min(window.innerWidth / baseW, window.innerHeight / baseH);
|
|
91
|
+
setScale(ratio);
|
|
92
|
+
} else {
|
|
93
|
+
setScale(1);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
resize();
|
|
97
|
+
window.addEventListener("resize", resize);
|
|
98
|
+
return () => window.removeEventListener("resize", resize);
|
|
100
99
|
}, []);
|
|
101
100
|
|
|
102
101
|
const getRandomSix = (arr: ImageItem[]) =>
|
|
@@ -115,11 +114,7 @@ export default function Game() {
|
|
|
115
114
|
startRound(selected, [], true);
|
|
116
115
|
};
|
|
117
116
|
|
|
118
|
-
const startRound = (
|
|
119
|
-
images = files,
|
|
120
|
-
used: string[] = usedHidden,
|
|
121
|
-
isFirst = false
|
|
122
|
-
) => {
|
|
117
|
+
const startRound = (images = files, used: string[] = usedHidden, isFirst = false) => {
|
|
123
118
|
const roundSet = getRandomSix(images.length ? images : themes[theme!]);
|
|
124
119
|
let idx = Math.floor(Math.random() * roundSet.length);
|
|
125
120
|
let candidate = roundSet[idx].name;
|
|
@@ -169,31 +164,14 @@ export default function Game() {
|
|
|
169
164
|
const correct = files[hiddenIndex!].name;
|
|
170
165
|
setResultsTable((prev) => [
|
|
171
166
|
...prev,
|
|
172
|
-
{
|
|
173
|
-
round: currentRound,
|
|
174
|
-
answer: inputValue,
|
|
175
|
-
correct,
|
|
176
|
-
result: "wrong",
|
|
177
|
-
},
|
|
167
|
+
{ round: currentRound, answer: inputValue, correct, result: "wrong" },
|
|
178
168
|
]);
|
|
179
169
|
return;
|
|
180
170
|
}
|
|
181
171
|
const t = setTimeout(() => setTimeLeft((s) => s - 1), 1000);
|
|
182
172
|
return () => clearTimeout(t);
|
|
183
173
|
}
|
|
184
|
-
}, [
|
|
185
|
-
phase,
|
|
186
|
-
readyTime,
|
|
187
|
-
memorizeTime,
|
|
188
|
-
timeLeft,
|
|
189
|
-
started,
|
|
190
|
-
finished,
|
|
191
|
-
answered,
|
|
192
|
-
files,
|
|
193
|
-
hiddenIndex,
|
|
194
|
-
currentRound,
|
|
195
|
-
inputValue,
|
|
196
|
-
]);
|
|
174
|
+
}, [phase, readyTime, memorizeTime, timeLeft, started, finished, answered, files, hiddenIndex, currentRound, inputValue]);
|
|
197
175
|
|
|
198
176
|
const checkAnswer = () => {
|
|
199
177
|
if (hiddenIndex === null || animating) return;
|
|
@@ -212,10 +190,7 @@ export default function Game() {
|
|
|
212
190
|
setResult("wrong");
|
|
213
191
|
roundResult = "wrong";
|
|
214
192
|
}
|
|
215
|
-
setResultsTable((prev) => [
|
|
216
|
-
...prev,
|
|
217
|
-
{ round: currentRound, answer: userAnswer, correct, result: roundResult },
|
|
218
|
-
]);
|
|
193
|
+
setResultsTable((prev) => [...prev, { round: currentRound, answer: userAnswer, correct, result: roundResult }]);
|
|
219
194
|
setAnimating(true);
|
|
220
195
|
setTimeout(() => {
|
|
221
196
|
setAnimating(false);
|
|
@@ -224,9 +199,7 @@ export default function Game() {
|
|
|
224
199
|
};
|
|
225
200
|
|
|
226
201
|
const nextRound = () =>
|
|
227
|
-
currentRound < rounds
|
|
228
|
-
? (setCurrentRound((r) => r + 1), startRound())
|
|
229
|
-
: setFinished(true);
|
|
202
|
+
currentRound < rounds ? (setCurrentRound((r) => r + 1), startRound()) : setFinished(true);
|
|
230
203
|
|
|
231
204
|
const exitGame = () => {
|
|
232
205
|
setStarted(false);
|
|
@@ -238,279 +211,224 @@ export default function Game() {
|
|
|
238
211
|
() => (
|
|
239
212
|
<div style={styles.gmLogoFixed}>
|
|
240
213
|
<picture>
|
|
241
|
-
<source
|
|
242
|
-
|
|
243
|
-
window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"
|
|
244
|
-
}
|
|
245
|
-
type="image/svg+xml"
|
|
246
|
-
/>
|
|
247
|
-
<img
|
|
248
|
-
src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"}
|
|
249
|
-
alt="SPEAKID Logo"
|
|
250
|
-
style={styles.gmLogoImg}
|
|
251
|
-
loading="lazy"
|
|
252
|
-
/>
|
|
214
|
+
<source srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"} type="image/svg+xml" />
|
|
215
|
+
<img src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"} alt="SPEAKID Logo" style={styles.gmLogoImg} loading="lazy" />
|
|
253
216
|
</picture>
|
|
254
217
|
</div>
|
|
255
218
|
),
|
|
256
219
|
[]
|
|
257
220
|
);
|
|
258
221
|
|
|
222
|
+
// =============================
|
|
223
|
+
// RENDER
|
|
224
|
+
// =============================
|
|
259
225
|
return (
|
|
260
226
|
<div
|
|
261
227
|
ref={containerRef}
|
|
262
228
|
style={{
|
|
229
|
+
width: "100%",
|
|
230
|
+
height: "100vh",
|
|
263
231
|
display: "flex",
|
|
264
232
|
justifyContent: "center",
|
|
265
233
|
alignItems: "center",
|
|
266
|
-
|
|
267
|
-
|
|
234
|
+
background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
|
|
235
|
+
transition: "background 0.3s ease",
|
|
268
236
|
}}
|
|
269
237
|
>
|
|
270
238
|
<div
|
|
271
239
|
style={{
|
|
272
|
-
width:
|
|
273
|
-
height:
|
|
240
|
+
width: isMobile ? "100%" : 1440,
|
|
241
|
+
height: isMobile ? "100%" : 768,
|
|
274
242
|
display: "flex",
|
|
275
243
|
justifyContent: "center",
|
|
276
244
|
alignItems: "center",
|
|
277
|
-
flexDirection: "column",
|
|
278
245
|
overflow: "hidden",
|
|
279
|
-
borderRadius: "20px",
|
|
280
|
-
background:
|
|
281
|
-
|
|
282
|
-
|
|
246
|
+
borderRadius: isMobile ? 0 : "20px",
|
|
247
|
+
background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
|
|
248
|
+
boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.1)",
|
|
249
|
+
margin: isMobile ? "0 auto" : "unset",
|
|
283
250
|
}}
|
|
284
251
|
>
|
|
285
|
-
|
|
286
|
-
|
|
252
|
+
{/* ✅ добавленный блок для правильного масштабирования и центрирования */}
|
|
253
|
+
<div
|
|
254
|
+
style={{
|
|
255
|
+
transform: isMobile ? "none" : `scale(${scale})`,
|
|
256
|
+
transformOrigin: "center center",
|
|
257
|
+
width: "100%",
|
|
258
|
+
height: "100%",
|
|
259
|
+
display: "flex",
|
|
260
|
+
justifyContent: "center",
|
|
261
|
+
alignItems: "center",
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<div id="whats-missing-root">
|
|
265
|
+
{!isMobile && MemoizedLogo}
|
|
287
266
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
key={t}
|
|
296
|
-
|
|
297
|
-
onClick={() => setTheme(t as Theme)}
|
|
298
|
-
>
|
|
299
|
-
{t === "animals"
|
|
300
|
-
? "🐶 Animals"
|
|
301
|
-
: t === "food"
|
|
302
|
-
? "🍎 Food"
|
|
303
|
-
: "🧸 Toys"}
|
|
304
|
-
</button>
|
|
305
|
-
))}
|
|
306
|
-
</div>
|
|
307
|
-
<div style={{ marginTop: 24 }}>
|
|
308
|
-
<p style={styles.gmBodyS}>Choose number of rounds:</p>
|
|
309
|
-
<div style={{ display: "flex", gap: 12, marginTop: 8 }}>
|
|
310
|
-
{[3, 4, 5].map((n) => (
|
|
311
|
-
<button
|
|
312
|
-
key={n}
|
|
313
|
-
style={{
|
|
314
|
-
...styles.gmButton,
|
|
315
|
-
...(rounds === n ? styles.gmButtonActive : {}),
|
|
316
|
-
}}
|
|
317
|
-
onClick={() => setRounds(n)}
|
|
318
|
-
>
|
|
319
|
-
{n}
|
|
267
|
+
{/* ====== ИГРОВАЯ ЛОГИКА ====== */}
|
|
268
|
+
{!theme && !started && (
|
|
269
|
+
<div style={styles.gmCenterScreen}>
|
|
270
|
+
<h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
|
|
271
|
+
<p style={styles.gmBodyM}>Select a theme:</p>
|
|
272
|
+
<div style={{ display: "flex", gap: 16 }}>
|
|
273
|
+
{["animals", "food", "toys"].map((t) => (
|
|
274
|
+
<button key={t} style={styles.gmButton} onClick={() => setTheme(t as Theme)}>
|
|
275
|
+
{t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
|
|
320
276
|
</button>
|
|
321
277
|
))}
|
|
322
278
|
</div>
|
|
279
|
+
<div style={{ marginTop: 24 }}>
|
|
280
|
+
<p style={styles.gmBodyS}>Choose number of rounds:</p>
|
|
281
|
+
<div style={{ display: "flex", gap: 12, marginTop: 8 }}>
|
|
282
|
+
{[3, 4, 5].map((n) => (
|
|
283
|
+
<button
|
|
284
|
+
key={n}
|
|
285
|
+
style={{
|
|
286
|
+
...styles.gmButton,
|
|
287
|
+
...(rounds === n ? styles.gmButtonActive : {}),
|
|
288
|
+
}}
|
|
289
|
+
onClick={() => setRounds(n)}
|
|
290
|
+
>
|
|
291
|
+
{n}
|
|
292
|
+
</button>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
323
296
|
</div>
|
|
324
|
-
|
|
325
|
-
)}
|
|
326
|
-
|
|
327
|
-
{theme && !started && (
|
|
328
|
-
<div style={styles.gmCenterScreen}>
|
|
329
|
-
<h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
|
|
330
|
-
<p style={styles.gmBodyM}>Rounds: {rounds}</p>
|
|
331
|
-
<button style={styles.gmButton} onClick={startGame}>
|
|
332
|
-
▶ Start game
|
|
333
|
-
</button>
|
|
334
|
-
</div>
|
|
335
|
-
)}
|
|
297
|
+
)}
|
|
336
298
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
Your score: {score} / {rounds}
|
|
342
|
-
</h2>
|
|
343
|
-
<p
|
|
344
|
-
style={{
|
|
345
|
-
...styles.gmBodyM,
|
|
346
|
-
color: "#10b981",
|
|
347
|
-
marginTop: 12,
|
|
348
|
-
}}
|
|
349
|
-
>
|
|
350
|
-
Great job! 🎉 You did it!
|
|
351
|
-
</p>
|
|
352
|
-
<table style={styles.gmTable}>
|
|
353
|
-
<thead>
|
|
354
|
-
<tr>
|
|
355
|
-
<th>Round</th>
|
|
356
|
-
<th>Your Answer</th>
|
|
357
|
-
<th>Correct</th>
|
|
358
|
-
<th>Result</th>
|
|
359
|
-
</tr>
|
|
360
|
-
</thead>
|
|
361
|
-
<tbody>
|
|
362
|
-
{resultsTable.map((r, i) => (
|
|
363
|
-
<tr key={i}>
|
|
364
|
-
<td style={styles.gmTableCell}>{r.round}</td>
|
|
365
|
-
<td style={styles.gmTableCell}>
|
|
366
|
-
{r.answer || "—"}
|
|
367
|
-
</td>
|
|
368
|
-
<td style={styles.gmTableCell}>{r.correct}</td>
|
|
369
|
-
<td style={styles.gmTableCell}>
|
|
370
|
-
{r.result === "correct"
|
|
371
|
-
? "✔ Correct"
|
|
372
|
-
: r.result === "almost"
|
|
373
|
-
? "◐ Almost (0.5)"
|
|
374
|
-
: "✘ Wrong"}
|
|
375
|
-
</td>
|
|
376
|
-
</tr>
|
|
377
|
-
))}
|
|
378
|
-
</tbody>
|
|
379
|
-
</table>
|
|
380
|
-
<div style={{ display: "flex", gap: 12, marginTop: 24 }}>
|
|
299
|
+
{theme && !started && (
|
|
300
|
+
<div style={styles.gmCenterScreen}>
|
|
301
|
+
<h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
|
|
302
|
+
<p style={styles.gmBodyM}>Rounds: {rounds}</p>
|
|
381
303
|
<button style={styles.gmButton} onClick={startGame}>
|
|
382
|
-
|
|
383
|
-
</button>
|
|
384
|
-
<button style={styles.gmButton} onClick={exitGame}>
|
|
385
|
-
⬅️ Choose theme
|
|
304
|
+
▶ Start game
|
|
386
305
|
</button>
|
|
387
306
|
</div>
|
|
388
|
-
|
|
389
|
-
)}
|
|
307
|
+
)}
|
|
390
308
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
style={
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
309
|
+
{finished && (
|
|
310
|
+
<div style={styles.gmCenterScreen}>
|
|
311
|
+
<h1 style={styles.gmHeadline1}>Results</h1>
|
|
312
|
+
<h2 style={styles.gmHeadline3}>
|
|
313
|
+
Your score: {score} / {rounds}
|
|
314
|
+
</h2>
|
|
315
|
+
<p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>Yahoo! You did it! 🍬✨</p>
|
|
316
|
+
<table style={styles.gmTable}>
|
|
317
|
+
<thead>
|
|
318
|
+
<tr>
|
|
319
|
+
<th>Round</th>
|
|
320
|
+
<th>Your Answer</th>
|
|
321
|
+
<th>Correct</th>
|
|
322
|
+
<th>Result</th>
|
|
323
|
+
</tr>
|
|
324
|
+
</thead>
|
|
325
|
+
<tbody>
|
|
326
|
+
{resultsTable.map((r, i) => (
|
|
327
|
+
<tr key={i}>
|
|
328
|
+
<td style={styles.gmTableCell}>{r.round}</td>
|
|
329
|
+
<td style={styles.gmTableCell}>{r.answer || "—"}</td>
|
|
330
|
+
<td style={styles.gmTableCell}>{r.correct}</td>
|
|
331
|
+
<td style={styles.gmTableCell}>
|
|
332
|
+
{r.result === "correct"
|
|
333
|
+
? "✔ Correct"
|
|
334
|
+
: r.result === "almost"
|
|
335
|
+
? "◐ Almost (0.5)"
|
|
336
|
+
: "✘ Wrong"}
|
|
337
|
+
</td>
|
|
338
|
+
</tr>
|
|
339
|
+
))}
|
|
340
|
+
</tbody>
|
|
341
|
+
</table>
|
|
342
|
+
<div style={{ display: "flex", gap: 12, marginTop: 24 }}>
|
|
343
|
+
<button style={styles.gmButton} onClick={startGame}>
|
|
344
|
+
🔁 Play again
|
|
345
|
+
</button>
|
|
346
|
+
<button style={styles.gmButton} onClick={exitGame}>
|
|
347
|
+
⬅️ Choose theme
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
428
350
|
</div>
|
|
351
|
+
)}
|
|
429
352
|
|
|
430
|
-
|
|
353
|
+
{started && !finished && (
|
|
354
|
+
<div style={styles.gmGameLayout}>
|
|
431
355
|
<div
|
|
432
356
|
style={{
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
gap: isMobile ? "12px" : "20px",
|
|
439
|
-
justifyItems: "center",
|
|
357
|
+
minHeight: 160,
|
|
358
|
+
display: "flex",
|
|
359
|
+
flexDirection: "column",
|
|
360
|
+
justifyContent: "center",
|
|
361
|
+
alignItems: "center",
|
|
440
362
|
}}
|
|
441
363
|
>
|
|
442
|
-
{
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
style={{
|
|
451
|
-
...styles.gmCard,
|
|
452
|
-
...(result === "correct" &&
|
|
453
|
-
hiddenIndex === i
|
|
454
|
-
? styles.gmCorrect
|
|
455
|
-
: {}),
|
|
456
|
-
...(result === "wrong" &&
|
|
457
|
-
hiddenIndex === i
|
|
458
|
-
? styles.gmWrong
|
|
459
|
-
: {}),
|
|
460
|
-
}}
|
|
461
|
-
>
|
|
462
|
-
{!isHidden && (
|
|
463
|
-
<img
|
|
464
|
-
src={file.src}
|
|
465
|
-
alt={file.name}
|
|
466
|
-
style={{
|
|
467
|
-
width: "100%",
|
|
468
|
-
height: "100%",
|
|
469
|
-
objectFit: "cover",
|
|
470
|
-
}}
|
|
471
|
-
/>
|
|
472
|
-
)}
|
|
473
|
-
</div>
|
|
474
|
-
);
|
|
475
|
-
})}
|
|
364
|
+
{phase === "ready" && (
|
|
365
|
+
<>
|
|
366
|
+
<h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
|
|
367
|
+
<div style={styles.gmHourglass}>⏳</div>
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
{phase === "memorize" && <p style={{ ...styles.gmBodyM, color: "#10b981" }}>MEMORIZE ({memorizeTime})</p>}
|
|
371
|
+
{phase === "guess" && !answered && <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>}
|
|
476
372
|
</div>
|
|
477
|
-
)}
|
|
478
373
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
style={styles.gmInput}
|
|
490
|
-
/>
|
|
491
|
-
<button
|
|
492
|
-
style={{
|
|
493
|
-
...styles.gmButton,
|
|
494
|
-
marginLeft: 8,
|
|
495
|
-
}}
|
|
496
|
-
onClick={checkAnswer}
|
|
497
|
-
disabled={animating}
|
|
498
|
-
>
|
|
499
|
-
{animating ? "..." : "Check"}
|
|
500
|
-
</button>
|
|
501
|
-
</div>
|
|
502
|
-
)}
|
|
503
|
-
{answered && (
|
|
504
|
-
<button
|
|
505
|
-
style={styles.gmButton}
|
|
506
|
-
onClick={nextRound}
|
|
374
|
+
{phase !== "ready" && (
|
|
375
|
+
<div
|
|
376
|
+
style={{
|
|
377
|
+
...styles.gmGrid,
|
|
378
|
+
gridTemplateColumns: isMobile ? "repeat(2, 1fr)" : "repeat(3, 210px)",
|
|
379
|
+
gridAutoRows: isMobile ? "150px" : "210px",
|
|
380
|
+
gap: isMobile ? "12px" : "20px",
|
|
381
|
+
justifyItems: "center",
|
|
382
|
+
}}
|
|
507
383
|
>
|
|
508
|
-
|
|
509
|
-
|
|
384
|
+
{files.map((file, i) => {
|
|
385
|
+
const isHidden = hiddenIndex === i && phase === "guess" && !answered;
|
|
386
|
+
return (
|
|
387
|
+
<div
|
|
388
|
+
key={i}
|
|
389
|
+
style={{
|
|
390
|
+
...styles.gmCard,
|
|
391
|
+
...(result === "correct" && hiddenIndex === i ? styles.gmCorrect : {}),
|
|
392
|
+
...(result === "wrong" && hiddenIndex === i ? styles.gmWrong : {}),
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
{!isHidden && (
|
|
396
|
+
<img
|
|
397
|
+
src={file.src}
|
|
398
|
+
alt={file.name}
|
|
399
|
+
style={{
|
|
400
|
+
width: "100%",
|
|
401
|
+
height: "100%",
|
|
402
|
+
objectFit: "cover",
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
})}
|
|
409
|
+
</div>
|
|
510
410
|
)}
|
|
411
|
+
|
|
412
|
+
<div style={{ marginTop: 16, height: 80 }}>
|
|
413
|
+
{phase === "guess" && !answered && (
|
|
414
|
+
<div>
|
|
415
|
+
<input
|
|
416
|
+
type="text"
|
|
417
|
+
placeholder="Type the missing word"
|
|
418
|
+
value={inputValue}
|
|
419
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
420
|
+
style={styles.gmInput}
|
|
421
|
+
/>
|
|
422
|
+
<button style={{ ...styles.gmButton, marginLeft: 8 }} onClick={checkAnswer} disabled={animating}>
|
|
423
|
+
{animating ? "..." : "Check"}
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
{answered && <button style={styles.gmButton} onClick={nextRound}>Next round</button>}
|
|
428
|
+
</div>
|
|
511
429
|
</div>
|
|
512
|
-
|
|
513
|
-
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
514
432
|
</div>
|
|
515
433
|
</div>
|
|
516
434
|
</div>
|