Skip to content

Commit 54a45db

Browse files
author
Jonay Pelluz
committed
feature/letter-matcher-game feature: add letterMatcher game (Maestro de letras)
- Pure LetterMatchProcessor builds fill-the-gap challenges from ChangeRules - useLetterMatcher hook + UI, ES/EN routes, LetterMatcher styles - Generic Spinner component shown while words load - GameRules hides Additional rules / Tips sections when empty - Equal-height home cards (grid-auto-rows) - ICU plural for the few-mistakes result message - Register WORDS_MATCHER storage key + fetch group, catalog + routes - Tests: LetterMatchProcessor, useLetterMatcher, Spinner, GameRules
1 parent 2be5edf commit 54a45db

21 files changed

Lines changed: 1070 additions & 36 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import LetterMatcherUI from '@games/letterMatcher/UI';
5+
import useLetterMatcher from '@games/letterMatcher/useLetterMatcher';
6+
import { createGamesConfig } from '@hooks/useGamesConfig';
7+
import MainLayout from '@layouts/MainLayout';
8+
import { useWordsContext } from '@store/WordsContext';
9+
import '@styles/LetterMatcher.scss';
10+
11+
const LetterMatcherPage: React.FC = () => {
12+
const { locale } = useWordsContext();
13+
14+
const gameLogic = useLetterMatcher();
15+
const gameConfig = createGamesConfig(locale, 'letterMatcher');
16+
17+
return (
18+
<MainLayout>
19+
{gameConfig && <LetterMatcherUI gameConfig={gameConfig} {...gameLogic} />}
20+
</MainLayout>
21+
);
22+
};
23+
24+
export default LetterMatcherPage;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import LetterMatcherUI from '@games/letterMatcher/UI';
5+
import useLetterMatcher from '@games/letterMatcher/useLetterMatcher';
6+
import { createGamesConfig } from '@hooks/useGamesConfig';
7+
import MainLayout from '@layouts/MainLayout';
8+
import { useWordsContext } from '@store/WordsContext';
9+
import '@styles/LetterMatcher.scss';
10+
11+
const LetterMatcherPage: React.FC = () => {
12+
const { locale } = useWordsContext();
13+
14+
const gameLogic = useLetterMatcher();
15+
const gameConfig = createGamesConfig(locale, 'letterMatcher');
16+
17+
return (
18+
<MainLayout>
19+
{gameConfig && <LetterMatcherUI gameConfig={gameConfig} {...gameLogic} />}
20+
</MainLayout>
21+
);
22+
};
23+
24+
export default LetterMatcherPage;

components/GameRules.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,40 @@ const GameRulesInfo: React.FC<GameRules> = ({ additionalRules, howToPlay, gameGo
2525
<li key={index}>{rule}</li>
2626
))}
2727
</ul>
28-
<p>
29-
<strong>
30-
<FormattedMessage id="gameRulesAdditionalRules" />
31-
</strong>
32-
</p>
3328
{additionalRules.length > 0 && (
34-
<p>
35-
{additionalRules.map((rule: string, index: number) => (
36-
<span key={index}>
37-
{rule}
38-
{index !== additionalRules.length - 1 && <br />}
39-
</span>
40-
))}
41-
</p>
29+
<>
30+
<p>
31+
<strong>
32+
<FormattedMessage id="gameRulesAdditionalRules" />
33+
</strong>
34+
</p>
35+
<p>
36+
{additionalRules.map((rule: string, index: number) => (
37+
<span key={index}>
38+
{rule}
39+
{index !== additionalRules.length - 1 && <br />}
40+
</span>
41+
))}
42+
</p>
43+
</>
44+
)}
45+
{tips.length > 0 && (
46+
<>
47+
<p>
48+
<strong>
49+
<FormattedMessage id="gameRulesTips" />
50+
</strong>
51+
</p>
52+
<p>
53+
{tips.map((tip: string, index: number) => (
54+
<span key={index}>
55+
{tip}
56+
{index !== tips.length - 1 && <br />}
57+
</span>
58+
))}
59+
</p>
60+
</>
4261
)}
43-
<p>
44-
<strong>
45-
<FormattedMessage id="gameRulesTips" />
46-
</strong>
47-
</p>
48-
<p>
49-
{tips.map((tip: string, index: number) => (
50-
<span key={index}>
51-
{tip}
52-
{index !== tips.length - 1 && <br />}
53-
</span>
54-
))}
55-
</p>
5662
</div>
5763
);
5864

components/Spinner.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import '@styles/Spinner.scss';
5+
6+
interface SpinnerProps {
7+
/** Optional caption shown under the spinner (pass a localized node). */
8+
label?: React.ReactNode;
9+
/** Diameter in px. Border thickness scales with it. */
10+
size?: number;
11+
}
12+
13+
const Spinner: React.FC<SpinnerProps> = ({ label, size = 40 }) => (
14+
<div className="spinner-wrapper">
15+
<span
16+
className="spinner-circle"
17+
style={{ width: size, height: size, borderWidth: Math.max(3, Math.round(size / 10)) }}
18+
/>
19+
{label != null && <p className="spinner-label">{label}</p>}
20+
</div>
21+
);
22+
23+
export default Spinner;

config/translations/Games.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const GamesRoutes: Translations = {
99
definitionMaster: '/juegos/maestro-de-las-definiciones',
1010
crossWordPuzzle: '/juegos/crucigramas',
1111
accentFixer: '/juegos/corregidor-de-acentos',
12+
letterMatcher: '/juegos/maestro-de-letras',
1213
},
1314
en: {
1415
spellTower: '/en/games/spell-tower',
@@ -19,6 +20,7 @@ const GamesRoutes: Translations = {
1920
crossWordPuzzle: '/en/games/crossword-puzzles',
2021
// ES-only game: no EN page exists, route points at the ES page and is unreachable via the EN UI.
2122
accentFixer: '/juegos/corregidor-de-acentos',
23+
letterMatcher: '/en/games/letter-matcher',
2224
},
2325
};
2426

@@ -337,12 +339,12 @@ const GamesTranslations: GamePreConfig[] = [
337339
es: 'Corregidor de acentos',
338340
},
339341
description: {
340-
en: 'A word is shown without its accent mark and you must tap the vowel that should carry it, or indicate that it has none. Put the rules of agudas, llanas, and esdrújulas to the test.',
341-
es: 'Se muestra una palabra sin tilde y debes pulsar la vocal que debería llevarla, o indicar que no lleva. Pon a prueba las reglas de agudas, llanas y esdrújulas.',
342+
en: 'Tap the vowel that should carry the accent mark, or say it has none. Test the rules of agudas, llanas and esdrújulas.',
343+
es: 'Pulsa la vocal que debería llevar la tilde, o indica que no lleva. Pon a prueba las agudas, llanas y esdrújulas.',
342344
},
343345
subtitle: {
344-
en: 'A game where you decide which vowel carries the accent mark.',
345-
es: 'Un juego donde decides qué vocal lleva la tilde.',
346+
en: 'Tap the vowel that should carry the accent mark, or say it has none. Test the agudas, llanas and esdrújulas.',
347+
es: 'Pulsa la vocal que debería llevar la tilde, o indica que no lleva. Pon a prueba las agudas, llanas y esdrújulas.',
346348
},
347349
gameRules: {
348350
en: {
@@ -375,6 +377,50 @@ const GamesTranslations: GamePreConfig[] = [
375377
},
376378
},
377379
},
380+
{
381+
id: 'letterMatcher',
382+
imgSrc: '/images/games/letterMatcher.png',
383+
title: {
384+
en: 'Letter Master',
385+
es: 'Maestro de letras',
386+
},
387+
description: {
388+
en: 'A word appears with a gap where a tricky letter is missing: choose the right one from the options to complete it. Each hit scores a point and each miss costs one.',
389+
es: 'Aparece una palabra con un hueco donde falta una letra difícil: elige la correcta entre las opciones para completarla. Cada acierto suma un punto y cada fallo resta uno.',
390+
},
391+
subtitle: {
392+
en: 'A word appears with a gap where a tricky letter is missing: choose the right one from the options to complete it. Each hit scores a point and each miss costs one.',
393+
es: 'Aparece una palabra con un hueco donde falta una letra difícil: elige la correcta entre las opciones para completarla. Cada acierto suma un punto y cada fallo resta uno.',
394+
},
395+
gameRules: {
396+
en: {
397+
gameGoal: 'Fill the gap with the correct letter in as many words as possible.',
398+
howToPlay: [
399+
'A word will appear with a gap.',
400+
'Tap the option that correctly completes the word.',
401+
'Correct answers add a point; mistakes subtract one.',
402+
'You have 60 seconds.',
403+
],
404+
additionalRules: [],
405+
tips: [
406+
'Think of words in the same family: they keep the same tricky letters.',
407+
],
408+
},
409+
es: {
410+
gameGoal: 'Rellena el hueco con la letra correcta en tantas palabras como puedas.',
411+
howToPlay: [
412+
'Aparecerá una palabra con un hueco.',
413+
'Pulsa la opción que completa correctamente la palabra.',
414+
'Los aciertos suman un punto; los fallos restan uno.',
415+
'Tienes 60 segundos.',
416+
],
417+
additionalRules: [],
418+
tips: [
419+
'Piensa en palabras de la misma familia: si «haber» lleva h, «habrá» también.',
420+
],
421+
},
422+
},
423+
},
378424
];
379425

380426
export { GamesTranslations, GamesRoutes };

config/translations/General.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ const GeneralTranslations: Translations = {
7575
gameCrossWordGenerating: 'Generating crossword...',
7676
gameCrossWordComplete: 'Congratulations! You completed the crossword!',
7777
gameAccentNoAccent: 'No accent mark',
78+
gameLetterNothing: '(nothing)',
79+
gameLoading: 'Loading…',
7880
gameAccentResultPerfect: 'Congratulations! No mistakes!',
79-
gameAccentResultFew: 'You only made {count} mistake(s)!',
81+
gameAccentResultFew: 'You only made {count, plural, one {# mistake} other {# mistakes}}!',
8082
gameAccentResultMany: 'You should study a bit more!',
8183
gameQuizFinishedScore: 'You got {score} out of {total} correct!',
8284
headerHome: 'Home',
@@ -141,8 +143,10 @@ const GeneralTranslations: Translations = {
141143
gameCrossWordGenerating: 'Generando crucigrama...',
142144
gameCrossWordComplete: '¡Enhorabuena! ¡Has completado el crucigrama!',
143145
gameAccentNoAccent: 'No lleva tilde',
146+
gameLetterNothing: '(nada)',
147+
gameLoading: 'Cargando…',
144148
gameAccentResultPerfect: '¡Enhorabuena! ¡Sin ningún error!',
145-
gameAccentResultFew: '¡Solo has fallado {count} vez/veces!',
149+
gameAccentResultFew: '¡Solo has fallado {count, plural, one {# vez} other {# veces}}!',
146150
gameAccentResultMany: '¡Deberías estudiar un poco más!',
147151
gameQuizFinishedScore: '¡Acertaste {score} de {total}!',
148152
headerHome: 'Inicio',

0 commit comments

Comments
 (0)