Skip to content

Commit 2be5edf

Browse files
authored
Merge pull request #49 from jonaypelluz/feature/accent-fixer-game
feature/accent-fixer-game
2 parents 13f3500 + 4102c63 commit 2be5edf

22 files changed

Lines changed: 1170 additions & 75 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 AccentFixerUI from '@games/accentFixer/UI';
5+
import useAccentFixer from '@games/accentFixer/useAccentFixer';
6+
import { createGamesConfig } from '@hooks/useGamesConfig';
7+
import MainLayout from '@layouts/MainLayout';
8+
import { useWordsContext } from '@store/WordsContext';
9+
import '@styles/AccentFixer.scss';
10+
11+
const AccentFixerPage: React.FC = () => {
12+
const { locale } = useWordsContext();
13+
14+
const gameLogic = useAccentFixer();
15+
const gameConfig = createGamesConfig(locale, 'accentFixer');
16+
17+
return (
18+
<MainLayout>
19+
{gameConfig && <AccentFixerUI gameConfig={gameConfig} {...gameLogic} />}
20+
</MainLayout>
21+
);
22+
};
23+
24+
export default AccentFixerPage;

components/Header.tsx

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const Head: React.FC = () => {
1818
const { locale, wordOfTheDay, gameLevel, currentRoutes, generalLoading, isLoading } =
1919
useWordsContext();
2020
useDailyWord();
21-
const { selectLevel } = useLevelLoader();
21+
const { selectLevel, loadAllLevels } = useLevelLoader();
2222
const [navOpen, setNavOpen] = useState(false);
2323
const [levelOpen, setLevelOpen] = useState(false);
2424

@@ -38,6 +38,11 @@ const Head: React.FC = () => {
3838
setLevelOpen(false);
3939
};
4040

41+
const handleLoadLevels = () => {
42+
loadAllLevels();
43+
setLevelOpen(false);
44+
};
45+
4146
return (
4247
<header className="header">
4348
<Link href={currentRoutes.home} className="logo">
@@ -66,30 +71,43 @@ const Head: React.FC = () => {
6671
</nav>
6772
<div className="header-right">
6873
<div className="level-selector">
69-
<button
70-
className={`level-selector-toggle${gameLevel ? ' has-level' : ''}`}
71-
onClick={() => setLevelOpen((o) => !o)}
72-
aria-expanded={levelOpen}
73-
>
74-
{gameLevel
75-
? levelTranslations[gameLevel]
76-
: intl.formatMessage({ id: 'homeChoseLevel' })}
77-
{(generalLoading || isLoading) && (
78-
<span className="level-selector-spinner" aria-hidden="true" />
79-
)}
80-
</button>
81-
{levelOpen && (
82-
<div className="level-selector-dropdown">
83-
{LevelsConfig.map((level: LevelConfig) => (
84-
<button
85-
key={level.level}
86-
className={`level-option btn-${level.level}${gameLevel === level.level ? ' selected' : ''}`}
87-
onClick={() => handleLevelSelect(level.level)}
88-
>
89-
{levelTranslations[level.level]}
90-
</button>
91-
))}
92-
</div>
74+
{!gameLevel ? (
75+
<button
76+
className="level-selector-toggle"
77+
onClick={handleLoadLevels}
78+
disabled={isLoading}
79+
>
80+
<FormattedMessage id="homeLoadLevels" />
81+
{isLoading && (
82+
<span className="level-selector-spinner" aria-hidden="true" />
83+
)}
84+
</button>
85+
) : (
86+
<>
87+
<button
88+
className="level-selector-toggle has-level"
89+
onClick={() => setLevelOpen((o) => !o)}
90+
aria-expanded={levelOpen}
91+
>
92+
{levelTranslations[gameLevel]}
93+
{(generalLoading || isLoading) && (
94+
<span className="level-selector-spinner" aria-hidden="true" />
95+
)}
96+
</button>
97+
{levelOpen && (
98+
<div className="level-selector-dropdown">
99+
{LevelsConfig.map((level: LevelConfig) => (
100+
<button
101+
key={level.level}
102+
className={`level-option btn-${level.level}${gameLevel === level.level ? ' selected' : ''}`}
103+
onClick={() => handleLevelSelect(level.level)}
104+
>
105+
{levelTranslations[level.level]}
106+
</button>
107+
))}
108+
</div>
109+
)}
110+
</>
93111
)}
94112
</div>
95113
{wordOfTheDay && (

components/HomeContent.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const mainImageArray: string[] = [
2222

2323
const HomeContent: React.FC = () => {
2424
const intl = useIntl();
25-
const { error, gameLevel, hydrated } = useWordsContext();
26-
const { selectLevel } = useLevelLoader();
25+
const { error, gameLevel, isLoading, hydrated, generalLoading } = useWordsContext();
26+
const { selectLevel, loadAllLevels } = useLevelLoader();
2727
// Keep the random hero image stable across renders. Picking it inside a
2828
// useState initializer would re-randomize between server and client; this
2929
// effect picks it once after mount to avoid hydration mismatch.
@@ -36,6 +36,10 @@ const HomeContent: React.FC = () => {
3636
selectLevel(level);
3737
};
3838

39+
const handleLoadAllClick = () => {
40+
loadAllLevels();
41+
};
42+
3943
// General (initial) load: on first hydration, if the user has a stored
4044
// level but the word-group caches have expired, reload them through the
4145
// same serialized level-load chain used by level switches. Runs once.
@@ -67,7 +71,7 @@ const HomeContent: React.FC = () => {
6771
subtitle={intl.formatMessage({ id: 'mainDescription' })}
6872
styles={{ border: '1px solid #000' }}
6973
/>
70-
<LevelList handlePopulateDBClick={handlePopulateDBClick} gameLevel={gameLevel} />
74+
<LevelList handlePopulateDBClick={handlePopulateDBClick} handleLoadAllClick={handleLoadAllClick} gameLevel={gameLevel} isLoading={isLoading || generalLoading} hydrated={hydrated} />
7175
<Games />
7276
</MainLayout>
7377
);

components/LevelList.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
'use client';
22

33
import React, { useState } from 'react';
4-
import { useIntl } from 'react-intl';
4+
import { FormattedMessage, useIntl } from 'react-intl';
55
import LevelsConfig from '@config/LevelConfig';
66
import { LevelConfig } from '@models/types';
77

88
interface LevelListProps {
99
handlePopulateDBClick: (level: string) => void;
10+
handleLoadAllClick: () => void;
1011
gameLevel: string | null;
12+
isLoading: boolean;
13+
hydrated: boolean;
1114
}
1215

13-
const LevelList: React.FC<LevelListProps> = ({ handlePopulateDBClick, gameLevel }) => {
16+
const LevelList: React.FC<LevelListProps> = ({
17+
handlePopulateDBClick,
18+
handleLoadAllClick,
19+
gameLevel,
20+
isLoading,
21+
hydrated,
22+
}) => {
1423
const intl = useIntl();
1524
const [isOpen, setIsOpen] = useState(gameLevel === null);
1625

@@ -20,9 +29,24 @@ const LevelList: React.FC<LevelListProps> = ({ handlePopulateDBClick, gameLevel
2029
advanced: intl.formatMessage({ id: 'levelAdvanced' }),
2130
};
2231

23-
const summaryLabel = gameLevel
24-
? `${intl.formatMessage({ id: 'homeLevel' })} ${levelTranslations[gameLevel]}`
25-
: intl.formatMessage({ id: 'homeChoseLevel' });
32+
if (!hydrated) return <div className="level-wrapper" />;
33+
34+
if (!gameLevel) {
35+
return (
36+
<div className="level-wrapper">
37+
<button
38+
className="btn-primary level-load-btn"
39+
onClick={handleLoadAllClick}
40+
disabled={isLoading}
41+
>
42+
<FormattedMessage id="homeLoadLevels" />
43+
{isLoading && <span className="level-selector-spinner" aria-hidden="true" />}
44+
</button>
45+
</div>
46+
);
47+
}
48+
49+
const summaryLabel = `${intl.formatMessage({ id: 'homeLevel' })} ${levelTranslations[gameLevel]}`;
2650

2751
return (
2852
<div className="level-wrapper">
@@ -33,6 +57,7 @@ const LevelList: React.FC<LevelListProps> = ({ handlePopulateDBClick, gameLevel
3357
aria-expanded={isOpen}
3458
>
3559
{summaryLabel}
60+
{isLoading && <span className="level-selector-spinner" aria-hidden="true" />}
3661
</button>
3762
{isOpen && (
3863
<div className="level-content">
@@ -41,7 +66,7 @@ const LevelList: React.FC<LevelListProps> = ({ handlePopulateDBClick, gameLevel
4166
key={idx}
4267
onClick={() => handlePopulateDBClick(level.level)}
4368
className={`btn-${level.level} btn-levels ${
44-
gameLevel && gameLevel === level.level ? 'selected' : ''
69+
gameLevel === level.level ? 'selected' : ''
4570
}`}
4671
>
4772
{/* eslint-disable-next-line @next/next/no-img-element */}

config/translations/Games.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const GamesRoutes: Translations = {
88
wordFinder: '/juegos/buscador-de-palabras',
99
definitionMaster: '/juegos/maestro-de-las-definiciones',
1010
crossWordPuzzle: '/juegos/crucigramas',
11+
accentFixer: '/juegos/corregidor-de-acentos',
1112
},
1213
en: {
1314
spellTower: '/en/games/spell-tower',
@@ -16,6 +17,8 @@ const GamesRoutes: Translations = {
1617
wordFinder: '/en/games/word-finder',
1718
definitionMaster: '/en/games/definition-master',
1819
crossWordPuzzle: '/en/games/crossword-puzzles',
20+
// ES-only game: no EN page exists, route points at the ES page and is unreachable via the EN UI.
21+
accentFixer: '/juegos/corregidor-de-acentos',
1922
},
2023
};
2124

@@ -325,6 +328,53 @@ const GamesTranslations: GamePreConfig[] = [
325328
},
326329
},
327330
},
331+
{
332+
id: 'accentFixer',
333+
imgSrc: '/images/games/accentFixer.png',
334+
availableLocales: ['es'],
335+
title: {
336+
en: 'Accent Fixer',
337+
es: 'Corregidor de acentos',
338+
},
339+
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+
},
343+
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+
},
347+
gameRules: {
348+
en: {
349+
gameGoal: 'Guess where the accent mark goes in as many words as possible.',
350+
howToPlay: [
351+
'A word without its accent mark will appear.',
352+
'Tap the vowel that should carry the accent mark.',
353+
'If the word has no accent mark, tap the "No accent mark" button.',
354+
'Each correct answer adds one point; each mistake subtracts one.',
355+
],
356+
additionalRules: [],
357+
tips: [
358+
'Remember: agudas words are accented if they end in a vowel, n, or s.',
359+
'Esdrújulas words always carry an accent mark.',
360+
],
361+
},
362+
es: {
363+
gameGoal: 'Acierta dónde va la tilde en el mayor número de palabras posible.',
364+
howToPlay: [
365+
'Aparecerá una palabra sin tilde.',
366+
'Pulsa la vocal que debería llevar tilde.',
367+
'Si la palabra no lleva tilde, pulsa el botón "No lleva tilde".',
368+
'Cada acierto suma un punto; cada fallo resta uno.',
369+
],
370+
additionalRules: [],
371+
tips: [
372+
'Recuerda: las agudas se acentúan si terminan en vocal, n o s.',
373+
'Las esdrújulas siempre llevan tilde.',
374+
],
375+
},
376+
},
377+
},
328378
];
329379

330380
export { GamesTranslations, GamesRoutes };

config/translations/General.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,28 @@ import { LoadingMessagesType } from '@models/types';
33

44
const LoadingMessages: LoadingMessagesType = {
55
es: [
6-
'Oops, las letras se han rebelado. ¡Algo ha ido mal!',
7-
'Parece que nuestras palabras están jugando al escondite. ¡Error detectado!',
8-
'¡Vaya! Alguien derramó café en el código. Estamos arreglándolo.',
9-
'Las tildes se han escapado, y con ellas, la estabilidad del sistema. Error en proceso.',
10-
'Estamos experimentando una tormenta de ideas... y de errores. Por favor, espera.',
11-
'Error en el sistema. Las letras están bailando salsa en vez de trabajar.',
12-
'Algo se ha torcido... posiblemente sean las eses. Trabajando para solucionarlo.',
13-
'Nuestros puntos y comas hicieron una pausa demasiado larga. Error encontrado.',
14-
'El alfabeto ha decidido tomarse un descanso. Error inesperado.',
15-
'Las palabras están haciendo huelga. Nos disculpamos por este error técnico.',
6+
'Preparando las letras para ti...',
7+
'Ordenando el abecedario, un momento...',
8+
'Las palabras están tomando asiento, ya casi...',
9+
'Cargando el diccionario, paciencia...',
10+
'Las tildes se están colocando en su sitio...',
11+
'Reuniendo todas las vocales y consonantes...',
12+
'Organizando las palabras por orden alfabético...',
13+
'Llenando el tablero de letras, casi listo...',
14+
'Los niveles se están preparando para el juego...',
15+
'Un segundo, estamos afinando la ortografía...',
1616
],
1717
en: [
18-
'Oops, the letters have rebelled. Something has gone wrong!',
19-
'It seems our words are playing hide and seek. Error detected!',
20-
// prettier-ignore
21-
'Oops! Someone spilled coffee on the code. We\'re fixing it.',
22-
// prettier-ignore
23-
'The accent marks have escaped, and with them, the system\'s stability. Error in process.',
24-
'We are experiencing a brainstorm... and errors. Please wait.',
25-
'System error. The letters are dancing salsa instead of working.',
26-
// prettier-ignore
27-
'Something has gone wrong... possibly the S\'s. Working to solve it.',
28-
'Our semicolons took a pause that was too long. Error found.',
29-
'The alphabet has decided to take a break. Unexpected error.',
30-
'The words are on strike. We apologize for this technical error.',
18+
'Getting the letters ready for you...',
19+
'Sorting the alphabet, one moment...',
20+
'Words are taking their seats, almost there...',
21+
'Loading the dictionary, hang tight...',
22+
'Accent marks are finding their place...',
23+
'Gathering all the vowels and consonants...',
24+
'Arranging words in alphabetical order...',
25+
'Filling the board with letters, almost done...',
26+
'Levels are getting ready for the game...',
27+
'One second, fine-tuning the spelling...',
3128
],
3229
};
3330

@@ -77,6 +74,10 @@ const GeneralTranslations: Translations = {
7774
gameWordBuilderClearAll: 'Clear',
7875
gameCrossWordGenerating: 'Generating crossword...',
7976
gameCrossWordComplete: 'Congratulations! You completed the crossword!',
77+
gameAccentNoAccent: 'No accent mark',
78+
gameAccentResultPerfect: 'Congratulations! No mistakes!',
79+
gameAccentResultFew: 'You only made {count} mistake(s)!',
80+
gameAccentResultMany: 'You should study a bit more!',
8081
gameQuizFinishedScore: 'You got {score} out of {total} correct!',
8182
headerHome: 'Home',
8283
headerGames: 'Games',
@@ -87,6 +88,7 @@ const GeneralTranslations: Translations = {
8788
headerWordOfTheDayUrl: 'https://www.dictionary.com/browse/',
8889
homeLevel: 'Level:',
8990
homeChoseLevel: 'Choose the level',
91+
homeLoadLevels: 'Load levels',
9092
incorrectWords: 'Incorrect words:',
9193
levelBeginner: 'Beginner',
9294
levelIntermediate: 'Intermediate',
@@ -138,6 +140,10 @@ const GeneralTranslations: Translations = {
138140
gameWordBuilderClearAll: 'Limpiar',
139141
gameCrossWordGenerating: 'Generando crucigrama...',
140142
gameCrossWordComplete: '¡Enhorabuena! ¡Has completado el crucigrama!',
143+
gameAccentNoAccent: 'No lleva tilde',
144+
gameAccentResultPerfect: '¡Enhorabuena! ¡Sin ningún error!',
145+
gameAccentResultFew: '¡Solo has fallado {count} vez/veces!',
146+
gameAccentResultMany: '¡Deberías estudiar un poco más!',
141147
gameQuizFinishedScore: '¡Acertaste {score} de {total}!',
142148
headerHome: 'Inicio',
143149
headerGames: 'Juegos',
@@ -148,6 +154,7 @@ const GeneralTranslations: Translations = {
148154
headerWordOfTheDayUrl: 'https://dle.rae.es/',
149155
homeLevel: 'Nivel:',
150156
homeChoseLevel: 'Elige el nivel',
157+
homeLoadLevels: 'Cargar niveles',
151158
incorrectWords: 'Palabras incorrectas:',
152159
levelBeginner: 'Principiante',
153160
levelIntermediate: 'Intermedio',

0 commit comments

Comments
 (0)