@mblinkov/whats-missing 20.0.27 → 20.0.28
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 +3 -3
- package/dist/whats-missing.es.js +192 -213
- package/package.json +1 -1
- package/src/Game.tsx +227 -241
package/package.json
CHANGED
package/src/Game.tsx
CHANGED
|
@@ -37,7 +37,7 @@ const levenshtein = (a: string, b: string) => {
|
|
|
37
37
|
return dp[a.length][b.length];
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
// ✅ фикс визуального подзума
|
|
40
|
+
// ✅ фикс визуального подзума
|
|
41
41
|
export function useNeutralizeBodyZoom(
|
|
42
42
|
ref: React.RefObject<HTMLDivElement | null>,
|
|
43
43
|
opts: { minDesktopWidth?: number; forceResize?: boolean } = { minDesktopWidth: 1200, forceResize: false }
|
|
@@ -58,7 +58,6 @@ export function useNeutralizeBodyZoom(
|
|
|
58
58
|
if (zoomInline && !isNaN(zoomInline) && zoomInline > 0) return zoomInline;
|
|
59
59
|
const zoomComputed = parseFloat((cs.zoom as string) || "");
|
|
60
60
|
if (zoomComputed && !isNaN(zoomComputed) && zoomComputed > 0) return zoomComputed;
|
|
61
|
-
|
|
62
61
|
const probe = document.createElement("div");
|
|
63
62
|
probe.style.position = "absolute";
|
|
64
63
|
probe.style.left = "0";
|
|
@@ -66,7 +65,6 @@ export function useNeutralizeBodyZoom(
|
|
|
66
65
|
probe.style.width = "100px";
|
|
67
66
|
probe.style.height = "1px";
|
|
68
67
|
probe.style.visibility = "hidden";
|
|
69
|
-
probe.style.pointerEvents = "none";
|
|
70
68
|
document.documentElement.appendChild(probe);
|
|
71
69
|
const rect = probe.getBoundingClientRect();
|
|
72
70
|
document.documentElement.removeChild(probe);
|
|
@@ -270,7 +268,10 @@ export default function Game({
|
|
|
270
268
|
setAnswered(true);
|
|
271
269
|
setResult("wrong");
|
|
272
270
|
const correct = files[hiddenIndex!].name;
|
|
273
|
-
setResultsTable((prev) => [
|
|
271
|
+
setResultsTable((prev) => [
|
|
272
|
+
...prev,
|
|
273
|
+
{ round: currentRound, answer: inputValue, correct, result: "wrong" },
|
|
274
|
+
]);
|
|
274
275
|
return;
|
|
275
276
|
}
|
|
276
277
|
const t = setTimeout(() => setTimeLeft((s) => s - 1), 1000);
|
|
@@ -332,6 +333,17 @@ export default function Game({
|
|
|
332
333
|
[]
|
|
333
334
|
);
|
|
334
335
|
|
|
336
|
+
// ✅ масштаб интерфейса под реальное разрешение
|
|
337
|
+
const baseWidth = 1440;
|
|
338
|
+
const baseHeight = 900;
|
|
339
|
+
const scale =
|
|
340
|
+
typeof window !== "undefined"
|
|
341
|
+
? Math.min(
|
|
342
|
+
(screenWidth ?? window.innerWidth) / baseWidth,
|
|
343
|
+
(screenHeight ?? window.innerHeight) / baseHeight
|
|
344
|
+
)
|
|
345
|
+
: 1;
|
|
346
|
+
|
|
335
347
|
return (
|
|
336
348
|
<div
|
|
337
349
|
ref={containerRef}
|
|
@@ -342,10 +354,8 @@ export default function Game({
|
|
|
342
354
|
justifyContent: "center",
|
|
343
355
|
alignItems: "center",
|
|
344
356
|
background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
|
|
345
|
-
transition: "background 0.3s ease",
|
|
346
357
|
overflow: "hidden",
|
|
347
358
|
zIndex: 1,
|
|
348
|
-
pointerEvents: "auto",
|
|
349
359
|
}}
|
|
350
360
|
>
|
|
351
361
|
<div
|
|
@@ -357,267 +367,243 @@ export default function Game({
|
|
|
357
367
|
height: isMobile
|
|
358
368
|
? "100%"
|
|
359
369
|
: `${Math.min(gameCubeSize ?? 1000, screenWidth ?? 1000, screenHeight ?? 1000)}px`,
|
|
360
|
-
maxWidth: "calc(100vw - 24px)",
|
|
361
|
-
maxHeight: "calc(100vh - 20px)",
|
|
362
|
-
display: "flex",
|
|
363
|
-
justifyContent: "center",
|
|
364
|
-
alignItems: "center",
|
|
365
|
-
overflow: "hidden",
|
|
366
370
|
borderRadius: isMobile ? 0 : "20px",
|
|
367
371
|
background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
|
|
368
|
-
boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.08)",
|
|
369
372
|
position: "relative",
|
|
373
|
+
overflow: "hidden",
|
|
374
|
+
boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.08)",
|
|
375
|
+
display: "flex",
|
|
376
|
+
justifyContent: "center",
|
|
377
|
+
alignItems: "center",
|
|
370
378
|
}}
|
|
371
379
|
>
|
|
372
380
|
<div
|
|
381
|
+
id="whats-missing-root"
|
|
373
382
|
style={{
|
|
374
|
-
transform:
|
|
375
|
-
transformOrigin: "
|
|
376
|
-
width:
|
|
377
|
-
height:
|
|
383
|
+
transform: `scale(${scale})`,
|
|
384
|
+
transformOrigin: "center center",
|
|
385
|
+
width: `${100 / scale}%`,
|
|
386
|
+
height: `${100 / scale}%`,
|
|
378
387
|
display: "flex",
|
|
388
|
+
flexDirection: "column",
|
|
379
389
|
justifyContent: "center",
|
|
380
390
|
alignItems: "center",
|
|
381
391
|
}}
|
|
382
392
|
>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
{!isMobile && MemoizedLogo}
|
|
394
|
+
|
|
395
|
+
{/* ====== ЛОББИ ====== */}
|
|
396
|
+
{!theme && !started && (
|
|
397
|
+
<div style={styles.gmCenterScreen}>
|
|
398
|
+
<h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
|
|
399
|
+
<p style={styles.gmBodyM}>Select a theme:</p>
|
|
400
|
+
<div style={{ display: "flex", gap: 16 }}>
|
|
401
|
+
{["animals", "food", "toys"].map((t) => (
|
|
402
|
+
<button key={t} style={styles.gmButton} onClick={() => setTheme(t as Theme)}>
|
|
403
|
+
{t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
|
|
404
|
+
</button>
|
|
405
|
+
))}
|
|
406
|
+
</div>
|
|
407
|
+
<div style={{ marginTop: 24 }}>
|
|
408
|
+
<p style={styles.gmBodyS}>Choose number of rounds:</p>
|
|
409
|
+
<div style={{ display: "flex", gap: 12, marginTop: 8 }}>
|
|
410
|
+
{[3, 4, 5].map((n) => (
|
|
392
411
|
<button
|
|
393
|
-
key={
|
|
394
|
-
style={
|
|
395
|
-
|
|
412
|
+
key={n}
|
|
413
|
+
style={{
|
|
414
|
+
...styles.gmButton,
|
|
415
|
+
...(rounds === n ? styles.gmButtonActive : {}),
|
|
416
|
+
}}
|
|
417
|
+
onClick={() => setRounds(n)}
|
|
396
418
|
>
|
|
397
|
-
{
|
|
398
|
-
? "🐶 Animals"
|
|
399
|
-
: t === "food"
|
|
400
|
-
? "🍎 Food"
|
|
401
|
-
: "🧸 Toys"}
|
|
419
|
+
{n}
|
|
402
420
|
</button>
|
|
403
421
|
))}
|
|
404
422
|
</div>
|
|
405
|
-
<div style={{ marginTop: 24 }}>
|
|
406
|
-
<p style={styles.gmBodyS}>Choose number of rounds:</p>
|
|
407
|
-
<div style={{ display: "flex", gap: 12, marginTop: 8 }}>
|
|
408
|
-
{[3, 4, 5].map((n) => (
|
|
409
|
-
<button
|
|
410
|
-
key={n}
|
|
411
|
-
style={{
|
|
412
|
-
...styles.gmButton,
|
|
413
|
-
...(rounds === n ? styles.gmButtonActive : {}),
|
|
414
|
-
}}
|
|
415
|
-
onClick={() => setRounds(n)}
|
|
416
|
-
>
|
|
417
|
-
{n}
|
|
418
|
-
</button>
|
|
419
|
-
))}
|
|
420
|
-
</div>
|
|
421
|
-
</div>
|
|
422
423
|
</div>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
|
|
427
|
+
{/* ====== ПОДТВЕРЖДЕНИЕ ====== */}
|
|
428
|
+
{theme && !started && (
|
|
429
|
+
<div style={styles.gmCenterScreen}>
|
|
430
|
+
<h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
|
|
431
|
+
<p style={styles.gmBodyM}>Rounds: {rounds}</p>
|
|
432
|
+
<button style={styles.gmButton} onClick={startGame}>
|
|
433
|
+
▶ Start game
|
|
434
|
+
</button>
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* ====== РЕЗУЛЬТАТЫ ====== */}
|
|
439
|
+
{finished && (
|
|
440
|
+
<div style={styles.gmCenterScreen}>
|
|
441
|
+
<h1 style={styles.gmHeadline1}>Results</h1>
|
|
442
|
+
<h2 style={styles.gmHeadline3}>
|
|
443
|
+
Your score: {score} / {rounds}
|
|
444
|
+
</h2>
|
|
445
|
+
<p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>
|
|
446
|
+
Yahoo! You did it! 🍬✨
|
|
447
|
+
</p>
|
|
448
|
+
<table style={styles.gmTable}>
|
|
449
|
+
<thead>
|
|
450
|
+
<tr>
|
|
451
|
+
<th>Round</th>
|
|
452
|
+
<th>Your Answer</th>
|
|
453
|
+
<th>Correct</th>
|
|
454
|
+
<th>Result</th>
|
|
455
|
+
</tr>
|
|
456
|
+
</thead>
|
|
457
|
+
<tbody>
|
|
458
|
+
{resultsTable.map((r, i) => (
|
|
459
|
+
<tr key={i}>
|
|
460
|
+
<td style={styles.gmTableCell}>{r.round}</td>
|
|
461
|
+
<td style={styles.gmTableCell}>{r.answer || "—"}</td>
|
|
462
|
+
<td style={styles.gmTableCell}>{r.correct}</td>
|
|
463
|
+
<td style={styles.gmTableCell}>
|
|
464
|
+
{r.result === "correct"
|
|
465
|
+
? "✔ Correct"
|
|
466
|
+
: r.result === "almost"
|
|
467
|
+
? "◐ Almost (0.5)"
|
|
468
|
+
: "✘ Wrong"}
|
|
469
|
+
</td>
|
|
470
|
+
</tr>
|
|
471
|
+
))}
|
|
472
|
+
</tbody>
|
|
473
|
+
</table>
|
|
474
|
+
<div style={{ display: "flex", gap: 12, marginTop: 24 }}>
|
|
429
475
|
<button style={styles.gmButton} onClick={startGame}>
|
|
430
|
-
|
|
476
|
+
🔁 Play again
|
|
477
|
+
</button>
|
|
478
|
+
<button style={styles.gmButton} onClick={exitGame}>
|
|
479
|
+
⬅️ Choose theme
|
|
431
480
|
</button>
|
|
432
481
|
</div>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
{r.result === "correct"
|
|
463
|
-
? "✔ Correct"
|
|
464
|
-
: r.result === "almost"
|
|
465
|
-
? "◐ Almost (0.5)"
|
|
466
|
-
: "✘ Wrong"}
|
|
467
|
-
</td>
|
|
468
|
-
</tr>
|
|
469
|
-
))}
|
|
470
|
-
</tbody>
|
|
471
|
-
</table>
|
|
472
|
-
<div style={{ display: "flex", gap: 12, marginTop: 24 }}>
|
|
473
|
-
<button style={styles.gmButton} onClick={startGame}>
|
|
474
|
-
🔁 Play again
|
|
475
|
-
</button>
|
|
476
|
-
<button style={styles.gmButton} onClick={exitGame}>
|
|
477
|
-
⬅️ Choose theme
|
|
478
|
-
</button>
|
|
479
|
-
</div>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
{/* ====== ИГРОВОЙ ЭКРАН ====== */}
|
|
486
|
+
{started && !finished && (
|
|
487
|
+
<div style={styles.gmGameLayout}>
|
|
488
|
+
<div
|
|
489
|
+
style={{
|
|
490
|
+
minHeight: 160,
|
|
491
|
+
display: "flex",
|
|
492
|
+
flexDirection: "column",
|
|
493
|
+
justifyContent: "center",
|
|
494
|
+
alignItems: "center",
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
{phase === "ready" && (
|
|
498
|
+
<>
|
|
499
|
+
<h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
|
|
500
|
+
<div style={styles.gmHourglass}>⏳</div>
|
|
501
|
+
</>
|
|
502
|
+
)}
|
|
503
|
+
{phase === "memorize" && (
|
|
504
|
+
<p style={{ ...styles.gmBodyM, color: "#10b981" }}>
|
|
505
|
+
MEMORIZE ({memorizeTime})
|
|
506
|
+
</p>
|
|
507
|
+
)}
|
|
508
|
+
{phase === "guess" && !answered && (
|
|
509
|
+
<p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
|
|
510
|
+
)}
|
|
480
511
|
</div>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
boxSizing: "border-box",
|
|
560
|
-
...(result === "correct" &&
|
|
561
|
-
hiddenIndex === i
|
|
562
|
-
? styles.gmCorrect
|
|
563
|
-
: {}),
|
|
564
|
-
...(result === "wrong" &&
|
|
565
|
-
hiddenIndex === i
|
|
566
|
-
? styles.gmWrong
|
|
567
|
-
: {}),
|
|
568
|
-
}}
|
|
569
|
-
>
|
|
570
|
-
{!isHidden && (
|
|
571
|
-
<img
|
|
572
|
-
src={file.src}
|
|
573
|
-
alt={file.name}
|
|
574
|
-
style={{
|
|
575
|
-
width: "100%",
|
|
576
|
-
height: "100%",
|
|
577
|
-
objectFit: "cover",
|
|
578
|
-
display: "block",
|
|
579
|
-
}}
|
|
580
|
-
/>
|
|
581
|
-
)}
|
|
582
|
-
</div>
|
|
583
|
-
);
|
|
584
|
-
})}
|
|
585
|
-
</div>
|
|
586
|
-
);
|
|
587
|
-
})()}
|
|
588
|
-
{/* Инпут и кнопки */}
|
|
589
|
-
<div style={{ marginTop: 16, height: 80 }}>
|
|
590
|
-
{phase === "guess" && !answered && (
|
|
591
|
-
<div>
|
|
592
|
-
<input
|
|
593
|
-
type="text"
|
|
594
|
-
placeholder="Type the missing word"
|
|
595
|
-
value={inputValue}
|
|
596
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
597
|
-
style={styles.gmInput}
|
|
598
|
-
/>
|
|
599
|
-
<button
|
|
600
|
-
style={{ ...styles.gmButton, marginLeft: 8 }}
|
|
601
|
-
onClick={checkAnswer}
|
|
602
|
-
disabled={animating}
|
|
603
|
-
>
|
|
604
|
-
{animating ? "..." : "Check"}
|
|
605
|
-
</button>
|
|
606
|
-
</div>
|
|
607
|
-
)}
|
|
608
|
-
{answered && (
|
|
512
|
+
|
|
513
|
+
{/* Сетка карточек */}
|
|
514
|
+
{phase !== "ready" && (() => {
|
|
515
|
+
const gap = isMobile ? 12 : 20;
|
|
516
|
+
const innerGutters = 32;
|
|
517
|
+
const effective = Math.min(
|
|
518
|
+
squareRef.current?.clientWidth ?? gameCubeSize ?? 1000,
|
|
519
|
+
squareRef.current?.clientHeight ?? screenHeight ?? 1000,
|
|
520
|
+
screenWidth ?? 1000
|
|
521
|
+
);
|
|
522
|
+
const columns = isMobile ? 2 : 3;
|
|
523
|
+
const cardSize = Math.max(
|
|
524
|
+
90,
|
|
525
|
+
Math.floor((effective - innerGutters - gap * (columns - 1)) / columns)
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<div
|
|
530
|
+
style={{
|
|
531
|
+
...styles.gmGrid,
|
|
532
|
+
gridTemplateColumns: `repeat(${columns}, ${cardSize}px)`,
|
|
533
|
+
gridAutoRows: `${cardSize}px`,
|
|
534
|
+
gap: `${gap}px`,
|
|
535
|
+
justifyContent: "center",
|
|
536
|
+
justifyItems: "center",
|
|
537
|
+
padding: 8,
|
|
538
|
+
boxSizing: "border-box",
|
|
539
|
+
}}
|
|
540
|
+
>
|
|
541
|
+
{files.map((file, i) => {
|
|
542
|
+
const isHidden = hiddenIndex === i && phase === "guess" && !answered;
|
|
543
|
+
return (
|
|
544
|
+
<div
|
|
545
|
+
key={i}
|
|
546
|
+
style={{
|
|
547
|
+
...styles.gmCard,
|
|
548
|
+
width: `${cardSize}px`,
|
|
549
|
+
height: `${cardSize}px`,
|
|
550
|
+
padding: isMobile ? 8 : 10,
|
|
551
|
+
boxSizing: "border-box",
|
|
552
|
+
...(result === "correct" && hiddenIndex === i
|
|
553
|
+
? styles.gmCorrect
|
|
554
|
+
: {}),
|
|
555
|
+
...(result === "wrong" && hiddenIndex === i
|
|
556
|
+
? styles.gmWrong
|
|
557
|
+
: {}),
|
|
558
|
+
}}
|
|
559
|
+
>
|
|
560
|
+
{!isHidden && (
|
|
561
|
+
<img
|
|
562
|
+
src={file.src}
|
|
563
|
+
alt={file.name}
|
|
564
|
+
style={{
|
|
565
|
+
width: "100%",
|
|
566
|
+
height: "100%",
|
|
567
|
+
objectFit: "cover",
|
|
568
|
+
display: "block",
|
|
569
|
+
}}
|
|
570
|
+
/>
|
|
571
|
+
)}
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
})}
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
})()}
|
|
578
|
+
|
|
579
|
+
{/* Инпут и кнопки */}
|
|
580
|
+
<div style={{ marginTop: 16, height: 80 }}>
|
|
581
|
+
{phase === "guess" && !answered && (
|
|
582
|
+
<div>
|
|
583
|
+
<input
|
|
584
|
+
type="text"
|
|
585
|
+
placeholder="Type the missing word"
|
|
586
|
+
value={inputValue}
|
|
587
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
588
|
+
style={styles.gmInput}
|
|
589
|
+
/>
|
|
609
590
|
<button
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
591
|
+
style={{ ...styles.gmButton, marginLeft: 8 }}
|
|
592
|
+
onClick={checkAnswer}
|
|
593
|
+
disabled={animating}
|
|
613
594
|
>
|
|
614
|
-
|
|
595
|
+
{animating ? "..." : "Check"}
|
|
615
596
|
</button>
|
|
616
|
-
|
|
617
|
-
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
{answered && (
|
|
600
|
+
<button type="button" style={styles.gmButton} onClick={nextRound}>
|
|
601
|
+
Next round
|
|
602
|
+
</button>
|
|
603
|
+
)}
|
|
618
604
|
</div>
|
|
619
|
-
|
|
620
|
-
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
621
607
|
</div>
|
|
622
608
|
</div>
|
|
623
609
|
</div>
|