@mblinkov/whats-missing 20.0.46 → 20.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblinkov/whats-missing",
3
- "version": "20.0.46",
3
+ "version": "20.0.48",
4
4
  "main": "dist/whats-missing.umd.js",
5
5
  "module": "dist/whats-missing.es.js",
6
6
  "types": "dist/index.d.ts",
package/src/Game.tsx CHANGED
@@ -92,6 +92,11 @@ export default function Game({
92
92
  if (style) {
93
93
  style.remove();
94
94
  }
95
+
96
+ // ✅ Cancel any ongoing preloads
97
+ if (abortController.current) {
98
+ abortController.current.abort();
99
+ }
95
100
  };
96
101
  }, []);
97
102
 
@@ -126,8 +131,10 @@ export default function Game({
126
131
  const [isIPadProPortrait, setIsIPadProPortrait] = useState(false);
127
132
  const [isIPadProLandscape, setIsIPadProLandscape] = useState(false);
128
133
  const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
129
- const [loadedImages, setLoadedImages] = useState<Set<string>>(new Set());
130
- const [preloadQueue, setPreloadQueue] = useState<string[]>([]);
134
+
135
+ // Система предзагрузки изображений
136
+ const preloadedUrls = useRef<Set<string>>(new Set());
137
+ const abortController = useRef<AbortController | null>(null);
131
138
 
132
139
  // ✅ адаптив под мобилки, планшеты и десктоп
133
140
  useEffect(() => {
@@ -137,6 +144,7 @@ export default function Game({
137
144
  const mobile = width < 768 || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape тоже считается мобильным
138
145
  const isLandscape = (width > height && mobile) || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape
139
146
  const isSmallHeight = height < 700; // Nest Hub, маленькие экраны
147
+ const isWideScreen = width / height > 1.8; // ✅ Широкие экраны
140
148
 
141
149
  // Определяем iPhone до 14 Pro Max в landscape режиме
142
150
  // iPhone 14 Pro Max: 926x428 (landscape)
@@ -203,7 +211,9 @@ export default function Game({
203
211
  // ✅ Вычисляем горизонтальный layout ОДИН РАЗ
204
212
  const isHorizontal =
205
213
  (mobile && width > height) ||
214
+ mobile || // ✅ ВСЕ мобильные устройства (включая portrait)
206
215
  height < 700 ||
216
+ isWideScreen || // ✅ Широкие экраны
207
217
  (width === 1366 && height === 766) ||
208
218
  (width === 1366 && height === 768) ||
209
219
  (width === 1280 && height === 720) ||
@@ -233,6 +243,15 @@ export default function Game({
233
243
  // Маленькие экраны: используем переданный gameCubeSize или полную высоту
234
244
  setContainerSize(gameCubeSize && gameCubeSize >= 400 ? gameCubeSize : null);
235
245
  setScale(1);
246
+ } else if (isWideScreen) {
247
+ // ✅ Широкие экраны: используем стандартный размер но уменьшаем масштаб картинок
248
+ const minSize = 400;
249
+ const maxSize = 1200;
250
+ const finalSize = gameCubeSize
251
+ ? Math.max(minSize, Math.min(maxSize, gameCubeSize))
252
+ : Math.min(1000, Math.min(width, height) * 0.9);
253
+ setContainerSize(finalSize);
254
+ setScale(0.85); // Уменьшаем масштаб картинок на 15%
236
255
  } else {
237
256
  // Десктопы: используем переданный gameCubeSize с разумными ограничениями
238
257
  const minSize = 400;
@@ -252,33 +271,46 @@ export default function Game({
252
271
  const getRandomSix = (arr: ImageItem[]) =>
253
272
  [...arr].sort(() => Math.random() - 0.5).slice(0, 6);
254
273
 
255
- // ✅ Функции предзагрузки изображений
274
+ // ✅ Утилиты предзагрузки
256
275
  const preloadImage = (src: string): Promise<void> => {
257
- return new Promise((resolve, reject) => {
276
+ return new Promise((resolve) => {
258
277
  const img = new Image();
259
- img.onload = () => {
260
- setLoadedImages(prev => new Set(prev).add(src));
261
- resolve();
262
- };
263
- img.onerror = reject;
278
+ img.onload = () => resolve();
279
+ img.onerror = () => resolve(); // Continue even on error
264
280
  img.src = src;
265
281
  });
266
282
  };
267
283
 
268
- const preloadImages = async (urls: string[]) => {
269
- // Загружаем с задержкой, чтобы не блокировать UI
284
+ const preloadImages = async (urls: string[], signal?: AbortSignal): Promise<void> => {
270
285
  const promises = urls.map(url => preloadImage(url));
271
286
  await Promise.allSettled(promises);
272
287
  };
273
288
 
274
- const startGame = async () => {
275
- if (!theme) return;
289
+ // Функция предзагрузки темы
290
+ const startThemePreload = (themeName: Theme) => {
291
+ // Cancel previous preload
292
+ if (abortController.current) {
293
+ abortController.current.abort();
294
+ }
276
295
 
277
- const selected = getRandomSix(themes[theme]);
296
+ // Create new controller
297
+ abortController.current = new AbortController();
278
298
 
279
- // Предзагружаем текущие изображения с высоким приоритетом
280
- await preloadImages(selected.map(item => item.src));
299
+ // Get theme images and start preloading
300
+ const themeImages = themes[themeName];
301
+ const urls = themeImages.map(item => item.src);
281
302
 
303
+ preloadImages(urls, abortController.current.signal).then(() => {
304
+ // Mark as preloaded
305
+ urls.forEach(url => preloadedUrls.current.add(url));
306
+ }).catch(() => {
307
+ // Ignore abort errors
308
+ });
309
+ };
310
+
311
+ const startGame = () => {
312
+ if (!theme) return;
313
+ const selected = getRandomSix(themes[theme]);
282
314
  setFiles(selected);
283
315
  setStarted(true);
284
316
  setFinished(false);
@@ -287,11 +319,13 @@ export default function Game({
287
319
  setResultsTable([]);
288
320
  setUsedHidden([]);
289
321
 
290
- // ✅ Загружаем следующий раунд в фоне
291
- const nextBatch = getRandomSix(
292
- themes[theme].filter(item => !selected.includes(item))
293
- );
294
- preloadImages(nextBatch.map(item => item.src)); // Не ждем завершения
322
+ // ✅ Preload current round images
323
+ const urls = selected.map(item => item.src);
324
+ preloadImages(urls, abortController.current?.signal).then(() => {
325
+ urls.forEach(url => preloadedUrls.current.add(url));
326
+ }).catch(() => {
327
+ // Ignore abort errors
328
+ });
295
329
 
296
330
  startRound(selected, [], true);
297
331
  };
@@ -313,9 +347,21 @@ export default function Game({
313
347
  setInputValue("");
314
348
  setTimeLeft(20);
315
349
  setResult(null);
350
+
351
+ // ✅ Preload next round images in background
352
+ if (theme && !isFirst) {
353
+ const nextPool = getRandomSix(themes[theme].filter(item => !used.includes(item.name)));
354
+ const nextUrls = nextPool.map(item => item.src);
355
+ preloadImages(nextUrls, abortController.current?.signal).then(() => {
356
+ nextUrls.forEach(url => preloadedUrls.current.add(url));
357
+ }).catch(() => {
358
+ // Ignore abort errors
359
+ });
360
+ }
361
+
316
362
  if (isFirst) {
317
363
  setPhase("ready");
318
- setReadyTime(3);
364
+ setReadyTime(5); // Увеличено с 3 до 5
319
365
  setMemorizeTime(10);
320
366
  } else {
321
367
  setPhase("memorize");
@@ -398,20 +444,20 @@ export default function Game({
398
444
 
399
445
  return (
400
446
  // ensure logo is positioned inside the square container
401
- <div style={{ ...styles.gmLogoFixed, position: "absolute", top: 16, left: 16, zIndex: 30 }}>
402
- <picture>
403
- <source
404
- srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"}
405
- type="image/svg+xml"
406
- />
407
- <img
408
- src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"}
409
- alt="SPEAKID Logo"
410
- style={styles.gmLogoImg}
411
- loading="lazy"
412
- />
413
- </picture>
414
- </div>
447
+ <div style={{ ...styles.gmLogoFixed, position: "absolute", top: 16, left: 16, zIndex: 30 }}>
448
+ <picture>
449
+ <source
450
+ srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"}
451
+ type="image/svg+xml"
452
+ />
453
+ <img
454
+ src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"}
455
+ alt="SPEAKID Logo"
456
+ style={styles.gmLogoImg}
457
+ loading="lazy"
458
+ />
459
+ </picture>
460
+ </div>
415
461
  );
416
462
  },
417
463
  [isMobile]
@@ -449,6 +495,7 @@ export default function Game({
449
495
  boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.1)",
450
496
  margin: isMobile ? "0 auto" : "unset",
451
497
  position: "relative", // needed so absolute logo is inside the square
498
+ transform: `scale(${scale})`, // ✅ Применяем масштаб для широких экранов
452
499
  }}
453
500
  >
454
501
  <div
@@ -462,62 +509,65 @@ export default function Game({
462
509
  }}
463
510
  >
464
511
  <div id="whats-missing-root">
465
- {!isMobile && MemoizedLogo}
512
+ {!isMobile && MemoizedLogo}
466
513
  {/* ====== ИГРОВАЯ ЛОГИКА ====== */}
467
- {!theme && !started && (
468
- <div style={styles.gmCenterScreen}>
469
- <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
470
- <p style={styles.gmBodyM}>Select a theme:</p>
514
+ {!theme && !started && (
515
+ <div style={styles.gmCenterScreen}>
516
+ <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
517
+ <p style={styles.gmBodyM}>Select a theme:</p>
471
518
  <div style={{ display: "flex", gap: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "8px" : "16px" }}>
472
- {["animals", "food", "toys"].map((t) => (
519
+ {["animals", "food", "toys"].map((t) => (
473
520
  <button key={t} style={{
474
521
  ...styles.gmButton,
475
522
  padding: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "8px 12px" : "12px 24px",
476
523
  fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "12px" : "16px",
477
524
  minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "70px" : "auto"
478
- }} onClick={() => setTheme(t as Theme)}>
479
- {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
480
- </button>
481
- ))}
482
- </div>
483
- <div style={{ marginTop: 24 }}>
484
- <p style={styles.gmBodyS}>Choose number of rounds:</p>
525
+ }} onClick={() => {
526
+ setTheme(t as Theme);
527
+ startThemePreload(t as Theme);
528
+ }}>
529
+ {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
530
+ </button>
531
+ ))}
532
+ </div>
533
+ <div style={{ marginTop: 24 }}>
534
+ <p style={styles.gmBodyS}>Choose number of rounds:</p>
485
535
  <div style={{ display: "flex", gap: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "6px" : "12px", marginTop: 8 }}>
486
- {[3, 4, 5].map((n) => (
487
- <button
488
- key={n}
489
- style={{
490
- ...styles.gmButton,
491
- ...(rounds === n ? styles.gmButtonActive : {}),
536
+ {[3, 4, 5].map((n) => (
537
+ <button
538
+ key={n}
539
+ style={{
540
+ ...styles.gmButton,
541
+ ...(rounds === n ? styles.gmButtonActive : {}),
492
542
  padding: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "6px 10px" : "12px 24px",
493
543
  fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "12px" : "16px",
494
544
  minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "40px" : "auto"
495
- }}
496
- onClick={() => setRounds(n)}
497
- >
498
- {n}
499
- </button>
500
- ))}
501
- </div>
545
+ }}
546
+ onClick={() => setRounds(n)}
547
+ >
548
+ {n}
549
+ </button>
550
+ ))}
502
551
  </div>
503
552
  </div>
504
- )}
505
- {theme && !started && (
506
- <div style={styles.gmCenterScreen}>
507
- <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
508
- <p style={styles.gmBodyM}>Rounds: {rounds}</p>
553
+ </div>
554
+ )}
555
+ {theme && !started && (
556
+ <div style={styles.gmCenterScreen}>
557
+ <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
558
+ <p style={styles.gmBodyM}>Rounds: {rounds}</p>
509
559
  <button style={{
510
560
  ...styles.gmButton,
511
561
  padding: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "8px 16px" : "12px 24px",
512
562
  fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "14px" : "16px",
513
563
  minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "120px" : "auto"
514
564
  }} onClick={startGame}>
515
- ▶ Start game
516
- </button>
517
- </div>
518
- )}
519
- {finished && (
520
- <div style={styles.gmCenterScreen}>
565
+ ▶ Start game
566
+ </button>
567
+ </div>
568
+ )}
569
+ {finished && (
570
+ <div style={styles.gmCenterScreen}>
521
571
  <h1 style={{
522
572
  ...styles.gmHeadline1,
523
573
  marginTop: (isMobile && window.innerWidth > window.innerHeight) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) || isIPadMiniPortrait || isIPadMiniLandscape || isIPadAirPortrait || isIPadAirLandscape || isSurfaceDuoPortrait || isSurfaceDuoLandscape || isIPadProPortrait || isIPadProLandscape ? "0px" : styles.gmHeadline1.marginTop,
@@ -541,31 +591,31 @@ export default function Game({
541
591
  marginTop: (isMobile && window.innerWidth > window.innerHeight) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) || isIPadMiniPortrait || isIPadMiniLandscape || isIPadAirPortrait || isIPadAirLandscape || isSurfaceDuoPortrait || isSurfaceDuoLandscape || isIPadProPortrait || isIPadProLandscape ? "0px" : "20px",
542
592
  marginBottom: (isMobile && window.innerWidth > window.innerHeight) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) || isIPadMiniPortrait || isIPadMiniLandscape || isIPadAirPortrait || isIPadAirLandscape || isSurfaceDuoPortrait || isSurfaceDuoLandscape || isIPadProPortrait || isIPadProLandscape ? "4px" : "32px"
543
593
  }}>
544
- <thead>
545
- <tr>
546
- <th>Round</th>
547
- <th>Your Answer</th>
548
- <th>Correct</th>
549
- <th>Result</th>
594
+ <thead>
595
+ <tr>
596
+ <th>Round</th>
597
+ <th>Your Answer</th>
598
+ <th>Correct</th>
599
+ <th>Result</th>
600
+ </tr>
601
+ </thead>
602
+ <tbody>
603
+ {resultsTable.map((r, i) => (
604
+ <tr key={i}>
605
+ <td style={styles.gmTableCell}>{r.round}</td>
606
+ <td style={styles.gmTableCell}>{r.answer || "—"}</td>
607
+ <td style={styles.gmTableCell}>{r.correct}</td>
608
+ <td style={styles.gmTableCell}>
609
+ {r.result === "correct"
610
+ ? "✔ Correct"
611
+ : r.result === "almost"
612
+ ? "◐ Almost (0.5)"
613
+ : "✘ Wrong"}
614
+ </td>
550
615
  </tr>
551
- </thead>
552
- <tbody>
553
- {resultsTable.map((r, i) => (
554
- <tr key={i}>
555
- <td style={styles.gmTableCell}>{r.round}</td>
556
- <td style={styles.gmTableCell}>{r.answer || "—"}</td>
557
- <td style={styles.gmTableCell}>{r.correct}</td>
558
- <td style={styles.gmTableCell}>
559
- {r.result === "correct"
560
- ? "✔ Correct"
561
- : r.result === "almost"
562
- ? "◐ Almost (0.5)"
563
- : "✘ Wrong"}
564
- </td>
565
- </tr>
566
- ))}
567
- </tbody>
568
- </table>
616
+ ))}
617
+ </tbody>
618
+ </table>
569
619
  <div style={{
570
620
  display: "flex",
571
621
  gap: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "6px" : "12px",
@@ -577,23 +627,23 @@ export default function Game({
577
627
  fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "12px" : "16px",
578
628
  minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "80px" : "auto"
579
629
  }} onClick={startGame}>
580
- 🔁 Play again
581
- </button>
630
+ 🔁 Play again
631
+ </button>
582
632
  <button style={{
583
633
  ...styles.gmButton,
584
634
  padding: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "6px 10px" : "12px 24px",
585
635
  fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "12px" : "16px",
586
636
  minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "80px" : "auto"
587
637
  }} onClick={exitGame}>
588
- ⬅️ Choose theme
589
- </button>
590
- </div>
638
+ ⬅️ Choose theme
639
+ </button>
591
640
  </div>
592
- )}
593
- {started && !finished && (
594
- <div style={styles.gmGameLayout}>
595
- <div
596
- style={{
641
+ </div>
642
+ )}
643
+ {started && !finished && (
644
+ <div style={styles.gmGameLayout}>
645
+ <div
646
+ style={{
597
647
  minHeight: isMobile && window.innerWidth <= 375 && window.innerHeight <= 667
598
648
  ? "45px" // iPhone SE: еще более компактная высота
599
649
  : (isMobile && window.innerWidth > window.innerHeight && window.innerHeight <= 428)
@@ -607,10 +657,10 @@ export default function Game({
607
657
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700
608
658
  ? "60px" // iPhone landscape и малые экраны: компактная высота
609
659
  : "160px",
610
- display: "flex",
611
- flexDirection: "column",
612
- justifyContent: "center",
613
- alignItems: "center",
660
+ display: "flex",
661
+ flexDirection: "column",
662
+ justifyContent: "center",
663
+ alignItems: "center",
614
664
  paddingTop: isMobile && window.innerWidth <= 375 && window.innerHeight <= 667
615
665
  ? "8px" // iPhone SE: поднимаем таймер еще выше
616
666
  : (isMobile && window.innerWidth > window.innerHeight && window.innerHeight <= 428)
@@ -624,23 +674,23 @@ export default function Game({
624
674
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700
625
675
  ? "15px" // iPhone landscape и малые экраны: поднимаем выше
626
676
  : "40px"
627
- }}
628
- >
629
- {phase === "ready" && (
630
- <>
631
- <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
632
- <div style={styles.gmHourglass}>⏳</div>
633
- </>
634
- )}
635
- {phase === "memorize" && (
636
- <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
637
- MEMORIZE ({memorizeTime})
638
- </p>
639
- )}
640
- {phase === "guess" && !answered && (
641
- <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
642
- )}
643
- </div>
677
+ }}
678
+ >
679
+ {phase === "ready" && (
680
+ <>
681
+ <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
682
+ <div style={styles.gmHourglass}>⏳</div>
683
+ </>
684
+ )}
685
+ {phase === "memorize" && (
686
+ <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
687
+ MEMORIZE ({memorizeTime})
688
+ </p>
689
+ )}
690
+ {phase === "guess" && !answered && (
691
+ <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
692
+ )}
693
+ </div>
644
694
  {phase !== "ready" && (
645
695
  <div
646
696
  style={{
@@ -770,37 +820,15 @@ export default function Game({
770
820
  }}
771
821
  >
772
822
  {!isHidden && (
773
- <div style={{ position: "relative", width: "100%", height: "100%" }}>
774
- {!loadedImages.has(file.src) && (
775
- <div style={{
776
- position: "absolute",
777
- top: 0,
778
- left: 0,
779
- width: "100%",
780
- height: "100%",
781
- background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
782
- backgroundSize: "200% 100%",
783
- borderRadius: "inherit",
784
- display: "flex",
785
- justifyContent: "center",
786
- alignItems: "center"
787
- }} />
788
- )}
789
- <img
790
- src={file.src}
791
- alt={file.name}
792
- // @ts-ignore - fetchpriority is a newer API
793
- fetchpriority={loadedImages.has(file.src) ? "auto" : "high"}
794
- onLoad={() => setLoadedImages(prev => new Set(prev).add(file.src))}
795
- style={{
796
- width: "100%",
797
- height: "100%",
798
- objectFit: "cover",
799
- opacity: loadedImages.has(file.src) ? 1 : 0,
800
- transition: "opacity 0.3s ease"
801
- }}
802
- />
803
- </div>
823
+ <img
824
+ src={file.src}
825
+ alt={file.name}
826
+ style={{
827
+ width: "100%",
828
+ height: "100%",
829
+ objectFit: "cover",
830
+ }}
831
+ />
804
832
  )}
805
833
  </div>
806
834
  );
@@ -835,7 +863,7 @@ export default function Game({
835
863
  ? "50px" // iPhone landscape и малые экраны: компактная высота
836
864
  : "80px"
837
865
  }}>
838
- {phase === "guess" && !answered && (
866
+ {phase === "guess" && !answered && (
839
867
  <div style={{
840
868
  display: "flex",
841
869
  flexDirection: isHorizontalLayout ? "row" : "column",
@@ -890,11 +918,11 @@ export default function Game({
890
918
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700 ? "400px" : "300px"
891
919
  }}>
892
920
  <>
893
- <input
894
- type="text"
895
- placeholder="Type the missing word"
896
- value={inputValue}
897
- onChange={(e) => setInputValue(e.target.value)}
921
+ <input
922
+ type="text"
923
+ placeholder="Type the missing word"
924
+ value={inputValue}
925
+ onChange={(e) => setInputValue(e.target.value)}
898
926
  style={{
899
927
  ...styles.gmInput,
900
928
  width: isMobile && window.innerWidth <= 375 && window.innerHeight <= 667
@@ -968,8 +996,8 @@ export default function Game({
968
996
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700 ? "14px" : "16px",
969
997
  flex: isHorizontalLayout ? "1" : "none"
970
998
  }}
971
- />
972
- <button
999
+ />
1000
+ <button
973
1001
  style={{
974
1002
  ...styles.gmButton,
975
1003
  marginLeft: isHorizontalLayout ? "8px" : "0",
@@ -1044,15 +1072,15 @@ export default function Game({
1044
1072
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700 ? "80px" : "100px",
1045
1073
  flexShrink: 0
1046
1074
  }}
1047
- onClick={checkAnswer}
1048
- disabled={animating}
1049
- >
1050
- {animating ? "..." : "Check"}
1051
- </button>
1075
+ onClick={checkAnswer}
1076
+ disabled={animating}
1077
+ >
1078
+ {animating ? "..." : "Check"}
1079
+ </button>
1052
1080
  </>
1053
- </div>
1054
- )}
1055
- {answered && (
1081
+ </div>
1082
+ )}
1083
+ {answered && (
1056
1084
  <button style={{
1057
1085
  ...styles.gmButton,
1058
1086
  padding: isMobile && window.innerWidth <= 375 && window.innerHeight <= 667
@@ -1102,12 +1130,12 @@ export default function Game({
1102
1130
  ? "14px" // iPad и Surface DUO: размер шрифта кнопки Next round
1103
1131
  : (isMobile && window.innerWidth > window.innerHeight) || window.innerHeight < 700 ? "14px" : "16px"
1104
1132
  }} onClick={nextRound}>
1105
- Next round
1106
- </button>
1107
- )}
1108
- </div>
1133
+ Next round
1134
+ </button>
1135
+ )}
1109
1136
  </div>
1110
- )}
1137
+ </div>
1138
+ )}
1111
1139
  </div>
1112
1140
  </div>
1113
1141
  </div>