@mblinkov/whats-missing 20.0.11 → 20.0.12

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/src/Game.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo } from "react";
1
+ import { useState, useEffect, useMemo, useRef } from "react";
2
2
  import { themes } from "./themes";
3
3
  import { styles } from "./Game.styles";
4
4
  import type { Theme } from "./themes";
@@ -11,57 +11,20 @@ type RoundResult = {
11
11
  result: "correct" | "almost" | "wrong";
12
12
  };
13
13
 
14
- // ✅ Финальный reset без скроллов, полос и с растяжением фона
14
+ // ✅ Минимальный reset без вмешательства в body
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-x: hidden;
24
- overflow-y: hidden;
25
- background: linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%);
26
- background-repeat: no-repeat;
27
- background-attachment: fixed;
28
- background-size: cover;
29
- }
30
-
31
- #whats-missing-root {
32
- all: unset;
33
- display: flex;
34
- flex-direction: column;
35
- justify-content: center;
36
- align-items: center;
37
- position: fixed;
38
- inset: 0;
39
- width: 100vw;
40
- height: 100vh;
41
- margin: 0;
42
- padding: 0;
43
- color: #1f2937;
44
- text-align: center;
45
- background: linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%);
46
- background-size: cover;
47
- }
48
-
49
18
  #whats-missing-root, #whats-missing-root * {
50
19
  box-sizing: border-box;
51
20
  font-family: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
52
21
  }
53
-
54
22
  #whats-missing-root img {
55
23
  max-width: 100%;
56
24
  height: auto;
57
25
  display: block;
58
26
  user-select: none;
59
27
  }
60
-
61
- @keyframes spin {
62
- from { transform: rotate(0deg); }
63
- to { transform: rotate(360deg); }
64
- }
65
28
  `;
66
29
  document.head.appendChild(style);
67
30
  };
@@ -82,8 +45,13 @@ const levenshtein = (a: string, b: string) => {
82
45
  };
83
46
 
84
47
  export default function Game() {
48
+ const containerRef = useRef<HTMLDivElement>(null);
49
+
85
50
  useEffect(() => {
86
51
  globalReset();
52
+ return () => {
53
+ document.body.style.overflow = "";
54
+ };
87
55
  }, []);
88
56
 
89
57
  const [theme, setTheme] = useState<Theme | null>(null);
@@ -159,17 +127,15 @@ export default function Game() {
159
127
  useEffect(() => {
160
128
  if (!started || finished || answered) return;
161
129
  if (phase === "ready") {
162
- if (readyTime <= 0) {
163
- setPhase("memorize");
164
- } else {
130
+ if (readyTime <= 0) setPhase("memorize");
131
+ else {
165
132
  const t = setTimeout(() => setReadyTime((r) => r - 1), 1000);
166
133
  return () => clearTimeout(t);
167
134
  }
168
135
  }
169
136
  if (phase === "memorize") {
170
- if (memorizeTime <= 0) {
171
- setPhase("guess");
172
- } else {
137
+ if (memorizeTime <= 0) setPhase("guess");
138
+ else {
173
139
  const t = setTimeout(() => setMemorizeTime((m) => m - 1), 1000);
174
140
  return () => clearTimeout(t);
175
141
  }
@@ -188,19 +154,7 @@ export default function Game() {
188
154
  const t = setTimeout(() => setTimeLeft((s) => s - 1), 1000);
189
155
  return () => clearTimeout(t);
190
156
  }
191
- }, [
192
- phase,
193
- readyTime,
194
- memorizeTime,
195
- timeLeft,
196
- started,
197
- finished,
198
- answered,
199
- files,
200
- hiddenIndex,
201
- currentRound,
202
- inputValue,
203
- ]);
157
+ }, [phase, readyTime, memorizeTime, timeLeft, started, finished, answered, files, hiddenIndex, currentRound, inputValue]);
204
158
 
205
159
  const checkAnswer = () => {
206
160
  if (hiddenIndex === null || animating) return;
@@ -219,10 +173,7 @@ export default function Game() {
219
173
  setResult("wrong");
220
174
  roundResult = "wrong";
221
175
  }
222
- setResultsTable((prev) => [
223
- ...prev,
224
- { round: currentRound, answer: userAnswer, correct, result: roundResult },
225
- ]);
176
+ setResultsTable((prev) => [...prev, { round: currentRound, answer: userAnswer, correct, result: roundResult }]);
226
177
  setAnimating(true);
227
178
  setTimeout(() => {
228
179
  setAnimating(false);
@@ -231,9 +182,7 @@ export default function Game() {
231
182
  };
232
183
 
233
184
  const nextRound = () =>
234
- currentRound < rounds
235
- ? (setCurrentRound((r) => r + 1), startRound())
236
- : setFinished(true);
185
+ currentRound < rounds ? (setCurrentRound((r) => r + 1), startRound()) : setFinished(true);
237
186
 
238
187
  const exitGame = () => {
239
188
  setStarted(false);
@@ -245,224 +194,228 @@ export default function Game() {
245
194
  () => (
246
195
  <div style={styles.gmLogoFixed}>
247
196
  <picture>
248
- <source
249
- srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"}
250
- type="image/svg+xml"
251
- />
252
- <img
253
- src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"}
254
- alt="SPEAKID Logo"
255
- style={styles.gmLogoImg}
256
- loading="lazy"
257
- />
197
+ <source srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"} type="image/svg+xml" />
198
+ <img src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"} alt="SPEAKID Logo" style={styles.gmLogoImg} loading="lazy" />
258
199
  </picture>
259
200
  </div>
260
201
  ),
261
202
  []
262
203
  );
263
204
 
205
+ // ✅ Новый контейнер для защиты от zoom
264
206
  return (
265
- <div id="whats-missing-root">
266
- {!isMobile && MemoizedLogo}
207
+ <div
208
+ ref={containerRef}
209
+ style={{
210
+ display: "flex",
211
+ justifyContent: "center",
212
+ alignItems: "center",
213
+ width: "100%",
214
+ height: "100%",
215
+ overflow: "hidden",
216
+ }}
217
+ >
218
+ <div
219
+ style={{
220
+ width: window.innerWidth > window.innerHeight ? "90vh" : "90vw",
221
+ height: window.innerWidth > window.innerHeight ? "90vh" : "90vw",
222
+ display: "flex",
223
+ justifyContent: "center",
224
+ alignItems: "center",
225
+ flexDirection: "column",
226
+ borderRadius: "20px",
227
+ background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
228
+ }}
229
+ >
230
+ <div id="whats-missing-root">
231
+ {!isMobile && MemoizedLogo}
267
232
 
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"}
276
- </button>
277
- ))}
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
- ))}
233
+ {!theme && !started && (
234
+ <div style={styles.gmCenterScreen}>
235
+ <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
236
+ <p style={styles.gmBodyM}>Select a theme:</p>
237
+ <div style={{ display: "flex", gap: 16 }}>
238
+ {["animals", "food", "toys"].map((t) => (
239
+ <button key={t} style={styles.gmButton} onClick={() => setTheme(t as Theme)}>
240
+ {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
241
+ </button>
242
+ ))}
243
+ </div>
244
+ <div style={{ marginTop: 24 }}>
245
+ <p style={styles.gmBodyS}>Choose number of rounds:</p>
246
+ <div style={{ display: "flex", gap: 12, marginTop: 8 }}>
247
+ {[3, 4, 5].map((n) => (
248
+ <button
249
+ key={n}
250
+ style={{
251
+ ...styles.gmButton,
252
+ ...(rounds === n ? styles.gmButtonActive : {}),
253
+ }}
254
+ onClick={() => setRounds(n)}
255
+ >
256
+ {n}
257
+ </button>
258
+ ))}
259
+ </div>
260
+ </div>
294
261
  </div>
295
- </div>
296
- </div>
297
- )}
298
-
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>
303
- <button style={styles.gmButton} onClick={startGame}>
304
- ▶ Start game
305
- </button>
306
- </div>
307
- )}
262
+ )}
308
263
 
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 }}>
316
- Yahoo! You did it! 🍬✨
317
- </p>
318
- <table style={styles.gmTable}>
319
- <thead>
320
- <tr>
321
- <th>Round</th>
322
- <th>Your Answer</th>
323
- <th>Correct</th>
324
- <th>Result</th>
325
- </tr>
326
- </thead>
327
- <tbody>
328
- {resultsTable.map((r, i) => (
329
- <tr key={i}>
330
- <td style={styles.gmTableCell}>{r.round}</td>
331
- <td style={styles.gmTableCell}>{r.answer || "—"}</td>
332
- <td style={styles.gmTableCell}>{r.correct}</td>
333
- <td style={styles.gmTableCell}>
334
- {r.result === "correct"
335
- ? "✔ Correct"
336
- : r.result === "almost"
337
- ? "◐ Almost (0.5)"
338
- : "✘ Wrong"}
339
- </td>
340
- </tr>
341
- ))}
342
- </tbody>
343
- </table>
344
- <div style={{ display: "flex", gap: 12, marginTop: 24 }}>
345
- <button style={styles.gmButton} onClick={startGame}>
346
- 🔁 Play again
347
- </button>
348
- <button style={styles.gmButton} onClick={exitGame}>
349
- ⬅️ Choose theme
350
- </button>
351
- </div>
352
- </div>
353
- )}
264
+ {theme && !started && (
265
+ <div style={styles.gmCenterScreen}>
266
+ <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
267
+ <p style={styles.gmBodyM}>Rounds: {rounds}</p>
268
+ <button style={styles.gmButton} onClick={startGame}>
269
+ ▶ Start game
270
+ </button>
271
+ </div>
272
+ )}
354
273
 
355
- {started && !finished && (
356
- <div style={styles.gmGameLayout}>
357
- <div
358
- style={{
359
- minHeight: 160,
360
- display: "flex",
361
- flexDirection: "column",
362
- justifyContent: "center",
363
- alignItems: "center",
364
- }}
365
- >
366
- {phase === "ready" && (
367
- <>
368
- <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
369
- <div style={styles.gmHourglass}>⏳</div>
370
- </>
371
- )}
372
- {phase === "memorize" && (
373
- <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
374
- MEMORIZE ({memorizeTime})
274
+ {finished && (
275
+ <div style={styles.gmCenterScreen}>
276
+ <h1 style={styles.gmHeadline1}>Results</h1>
277
+ <h2 style={styles.gmHeadline3}>
278
+ Your score: {score} / {rounds}
279
+ </h2>
280
+ <p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>
281
+ Yahoo! You did it! 🍬✨
375
282
  </p>
376
- )}
377
- {phase === "guess" && !answered && (
378
- <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
379
- )}
380
- </div>
283
+ <table style={styles.gmTable}>
284
+ <thead>
285
+ <tr>
286
+ <th>Round</th>
287
+ <th>Your Answer</th>
288
+ <th>Correct</th>
289
+ <th>Result</th>
290
+ </tr>
291
+ </thead>
292
+ <tbody>
293
+ {resultsTable.map((r, i) => (
294
+ <tr key={i}>
295
+ <td style={styles.gmTableCell}>{r.round}</td>
296
+ <td style={styles.gmTableCell}>{r.answer || "—"}</td>
297
+ <td style={styles.gmTableCell}>{r.correct}</td>
298
+ <td style={styles.gmTableCell}>
299
+ {r.result === "correct"
300
+ ? "✔ Correct"
301
+ : r.result === "almost"
302
+ ? "◐ Almost (0.5)"
303
+ : "✘ Wrong"}
304
+ </td>
305
+ </tr>
306
+ ))}
307
+ </tbody>
308
+ </table>
309
+ <div style={{ display: "flex", gap: 12, marginTop: 24 }}>
310
+ <button style={styles.gmButton} onClick={startGame}>
311
+ 🔁 Play again
312
+ </button>
313
+ <button style={styles.gmButton} onClick={exitGame}>
314
+ ⬅️ Choose theme
315
+ </button>
316
+ </div>
317
+ </div>
318
+ )}
381
319
 
382
- {phase !== "ready" && (
383
- <div
384
- style={{
385
- ...styles.gmGrid,
386
- gridTemplateColumns: isMobile ? "repeat(2, 1fr)" : "repeat(3, 210px)",
387
- gridAutoRows: isMobile ? "150px" : "210px",
388
- gap: isMobile ? "12px" : "20px",
389
- justifyItems: "center",
390
- ...(window.innerWidth > 2000
391
- ? {
392
- gridTemplateColumns: "repeat(3, 260px)",
393
- gridAutoRows: "260px",
394
- gap: "28px",
395
- }
396
- : {}),
397
- ...(window.innerWidth > 2560
398
- ? {
399
- gridTemplateColumns: "repeat(3, 300px)",
400
- gridAutoRows: "300px",
401
- gap: "32px",
402
- }
403
- : {}),
404
- }}
405
- >
406
- {files.map((file, i) => {
407
- const isHidden =
408
- hiddenIndex === i && phase === "guess" && !answered;
409
- return (
410
- <div
411
- key={i}
412
- style={{
413
- ...styles.gmCard,
414
- ...(result === "correct" && hiddenIndex === i
415
- ? styles.gmCorrect
416
- : {}),
417
- ...(result === "wrong" && hiddenIndex === i
418
- ? styles.gmWrong
419
- : {}),
420
- }}
421
- >
422
- {!isHidden && (
423
- <img
424
- src={file.src}
425
- alt={file.name}
320
+ {started && !finished && (
321
+ <div style={styles.gmGameLayout}>
322
+ <div
323
+ style={{
324
+ minHeight: 160,
325
+ display: "flex",
326
+ flexDirection: "column",
327
+ justifyContent: "center",
328
+ alignItems: "center",
329
+ }}
330
+ >
331
+ {phase === "ready" && (
332
+ <>
333
+ <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
334
+ <div style={styles.gmHourglass}>⏳</div>
335
+ </>
336
+ )}
337
+ {phase === "memorize" && (
338
+ <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
339
+ MEMORIZE ({memorizeTime})
340
+ </p>
341
+ )}
342
+ {phase === "guess" && !answered && (
343
+ <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
344
+ )}
345
+ </div>
346
+
347
+ {phase !== "ready" && (
348
+ <div
349
+ style={{
350
+ ...styles.gmGrid,
351
+ gridTemplateColumns: isMobile ? "repeat(2, 1fr)" : "repeat(3, 210px)",
352
+ gridAutoRows: isMobile ? "150px" : "210px",
353
+ gap: isMobile ? "12px" : "20px",
354
+ justifyItems: "center",
355
+ }}
356
+ >
357
+ {files.map((file, i) => {
358
+ const isHidden =
359
+ hiddenIndex === i && phase === "guess" && !answered;
360
+ return (
361
+ <div
362
+ key={i}
426
363
  style={{
427
- width: "100%",
428
- height: "100%",
429
- objectFit: "cover",
364
+ ...styles.gmCard,
365
+ ...(result === "correct" && hiddenIndex === i
366
+ ? styles.gmCorrect
367
+ : {}),
368
+ ...(result === "wrong" && hiddenIndex === i
369
+ ? styles.gmWrong
370
+ : {}),
430
371
  }}
431
- />
432
- )}
372
+ >
373
+ {!isHidden && (
374
+ <img
375
+ src={file.src}
376
+ alt={file.name}
377
+ style={{
378
+ width: "100%",
379
+ height: "100%",
380
+ objectFit: "cover",
381
+ }}
382
+ />
383
+ )}
384
+ </div>
385
+ );
386
+ })}
387
+ </div>
388
+ )}
389
+
390
+ <div style={{ marginTop: 16, height: 80 }}>
391
+ {phase === "guess" && !answered && (
392
+ <div>
393
+ <input
394
+ type="text"
395
+ placeholder="Type the missing word"
396
+ value={inputValue}
397
+ onChange={(e) => setInputValue(e.target.value)}
398
+ style={styles.gmInput}
399
+ />
400
+ <button
401
+ style={{ ...styles.gmButton, marginLeft: 8 }}
402
+ onClick={checkAnswer}
403
+ disabled={animating}
404
+ >
405
+ {animating ? "..." : "Check"}
406
+ </button>
433
407
  </div>
434
- );
435
- })}
408
+ )}
409
+ {answered && (
410
+ <button style={styles.gmButton} onClick={nextRound}>
411
+ Next round
412
+ </button>
413
+ )}
414
+ </div>
436
415
  </div>
437
416
  )}
438
-
439
- <div style={{ marginTop: 16, height: 80 }}>
440
- {phase === "guess" && !answered && (
441
- <div>
442
- <input
443
- type="text"
444
- placeholder="Type the missing word"
445
- value={inputValue}
446
- onChange={(e) => setInputValue(e.target.value)}
447
- style={styles.gmInput}
448
- />
449
- <button
450
- style={{ ...styles.gmButton, marginLeft: 8 }}
451
- onClick={checkAnswer}
452
- disabled={animating}
453
- >
454
- {animating ? "..." : "Check"}
455
- </button>
456
- </div>
457
- )}
458
- {answered && (
459
- <button style={styles.gmButton} onClick={nextRound}>
460
- Next round
461
- </button>
462
- )}
463
- </div>
464
417
  </div>
465
- )}
418
+ </div>
466
419
  </div>
467
420
  );
468
421
  }