Skip to content

Commit 669804d

Browse files
authored
Merge pull request #59 from albertoivo/bolt-perf-worldmap-memo-18193955327161379147
2 parents 28744f4 + 9895bd5 commit 669804d

File tree

2 files changed

+91
-81
lines changed

2 files changed

+91
-81
lines changed

src/components/game/WorldMap.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useMemo } from 'react';
1+
import { useState, useCallback, useMemo, memo } from 'react';
22
import type { World } from '../../types/question';
33
import { useAuth } from '../../hooks/useAuth';
44
import { TutorialModal, FlashcardDeck } from '../education';
@@ -22,7 +22,7 @@ interface WorldMapProps {
2222
/**
2323
* Mapa de mundos do jogo
2424
*/
25-
export function WorldMap({ onSelectWorld, worldProgress }: WorldMapProps) {
25+
export const WorldMap = memo(function WorldMap({ onSelectWorld, worldProgress }: WorldMapProps) {
2626
const { userData } = useAuth();
2727
const userScore = userData?.totalScore || 0;
2828
const unlockedWorlds = useMemo(() => userData?.unlockedWorlds || ['basic_commands'], [userData?.unlockedWorlds]);
@@ -297,6 +297,6 @@ export function WorldMap({ onSelectWorld, worldProgress }: WorldMapProps) {
297297
)}
298298
</div>
299299
);
300-
}
300+
});
301301

302302
export default WorldMap;

src/pages/GamePage.tsx

Lines changed: 88 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, useEffect, useRef } from 'react';
1+
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import confetti from 'canvas-confetti';
44
import type { World, QuestionDocument, UserProgress } from '../types/question';
@@ -15,6 +15,27 @@ import './GamePage.css';
1515

1616
type GameView = 'world-map' | 'world-questions' | 'playing' | 'reviewing';
1717

18+
/**
19+
* Obtém o nome amigável do mundo
20+
*/
21+
const getWorldName = (world: World): string => {
22+
const names: Record<World, string> = {
23+
basic_commands: 'Primeiros Passos',
24+
numbers: 'Números Mágicos',
25+
variables: 'Mundo das Variáveis',
26+
conditions: 'Terra das Decisões',
27+
decisions: 'Terra das Decisões',
28+
loops: 'Ilha da Repetição',
29+
functions: 'Vale das Funções',
30+
lists: 'Floresta das Listas',
31+
strings: 'Reino das Palavras',
32+
user_input: 'Conversando com o Usuário',
33+
dictionaries: 'Agenda Mágica',
34+
error_handling: 'Caçando Bugs',
35+
};
36+
return names[world] || world;
37+
};
38+
1839
/**
1940
* Página principal do jogo
2041
*/
@@ -76,24 +97,62 @@ export function GamePage() {
7697
return progress;
7798
}, [allProgress, allQuestions]);
7899

100+
/**
101+
* Toca um som de celebração usando AudioContext (sem arquivos externos)
102+
*/
103+
const playSuccessSound = useCallback(() => {
104+
try {
105+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
106+
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
107+
if (!AudioContext) return;
108+
109+
const ctx = new AudioContext();
110+
const oscillators = [
111+
{ freq: 523.25, type: 'sine', start: 0, dur: 0.2 }, // C5
112+
{ freq: 659.25, type: 'sine', start: 0.1, dur: 0.2 }, // E5
113+
{ freq: 783.99, type: 'sine', start: 0.2, dur: 0.4 }, // G5
114+
{ freq: 1046.50, type: 'sine', start: 0.3, dur: 0.6 } // C6
115+
];
116+
117+
oscillators.forEach(({ freq, type, start, dur }) => {
118+
const osc = ctx.createOscillator();
119+
const gain = ctx.createGain();
120+
121+
osc.type = type as OscillatorType;
122+
osc.frequency.setValueAtTime(freq, ctx.currentTime);
123+
124+
gain.gain.setValueAtTime(0.3, ctx.currentTime);
125+
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + dur);
126+
127+
osc.connect(gain);
128+
gain.connect(ctx.destination);
129+
130+
osc.start(ctx.currentTime + start);
131+
osc.stop(ctx.currentTime + start + dur);
132+
});
133+
} catch (e) {
134+
console.error('Audio play failed', e);
135+
}
136+
}, []);
137+
79138
/**
80139
* Seleciona um mundo para ver suas questões
81140
*/
82-
const handleSelectWorld = (world: World) => {
141+
const handleSelectWorld = useCallback((world: World) => {
83142
const questions = getQuestionsByWorld(world);
84143
setSelectedWorld(world);
85144
setWorldQuestions(questions);
86145
setView('world-questions');
87146
// Reset perfect run state when entering a world
88147
setIsPerfectRun(true);
89-
};
148+
}, [getQuestionsByWorld]);
90149

91150

92151
/**
93152
* Começa a jogar uma questão
94153
* Se a questão já foi completada, mostra modal com opções
95154
*/
96-
const handleStartQuestion = (question: QuestionDocument) => {
155+
const handleStartQuestion = useCallback((question: QuestionDocument) => {
97156
const progress = getQuestionProgress(question.id);
98157
const index = worldQuestions.findIndex(q => q.id === question.id);
99158

@@ -111,38 +170,38 @@ export function GamePage() {
111170
// eslint-disable-next-line react-hooks/purity
112171
questionStartTime.current = Date.now();
113172
}
114-
};
173+
}, [getQuestionProgress, worldQuestions]);
115174

116175
/**
117176
* Usuário escolheu "Ver minha resposta" no modal
118177
*/
119-
const handleViewAnswer = () => {
178+
const handleViewAnswer = useCallback(() => {
120179
setShowCompletedModal(false);
121180
setView('reviewing');
122-
};
181+
}, []);
123182

124183
/**
125184
* Usuário escolheu "Refazer" no modal (modo prática, sem pontos)
126185
*/
127-
const handleRedoQuestion = () => {
186+
const handleRedoQuestion = useCallback(() => {
128187
setShowCompletedModal(false);
129188
setView('playing');
130189
questionStartTime.current = Date.now();
131-
};
190+
}, []);
132191

133192
/**
134193
* Fecha o modal de questão completada
135194
*/
136-
const handleCloseCompletedModal = () => {
195+
const handleCloseCompletedModal = useCallback(() => {
137196
setShowCompletedModal(false);
138197
setCurrentQuestion(null);
139198
setCompletedQuestionProgress(null);
140-
};
199+
}, []);
141200

142201
/**
143202
* Callback quando uma questão é completada
144203
*/
145-
const handleQuestionComplete = async (passed: boolean, score: number) => {
204+
const handleQuestionComplete = useCallback(async (passed: boolean, score: number) => {
146205
// Calcula tempo de resposta em segundos
147206
const responseTimeSeconds = (Date.now() - questionStartTime.current) / 1000;
148207

@@ -166,50 +225,12 @@ export function GamePage() {
166225
starsEarned
167226
});
168227
}
169-
};
170-
171-
/**
172-
* Toca um som de celebração usando AudioContext (sem arquivos externos)
173-
*/
174-
const playSuccessSound = () => {
175-
try {
176-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
177-
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
178-
if (!AudioContext) return;
179-
180-
const ctx = new AudioContext();
181-
const oscillators = [
182-
{ freq: 523.25, type: 'sine', start: 0, dur: 0.2 }, // C5
183-
{ freq: 659.25, type: 'sine', start: 0.1, dur: 0.2 }, // E5
184-
{ freq: 783.99, type: 'sine', start: 0.2, dur: 0.4 }, // G5
185-
{ freq: 1046.50, type: 'sine', start: 0.3, dur: 0.6 } // C6
186-
];
187-
188-
oscillators.forEach(({ freq, type, start, dur }) => {
189-
const osc = ctx.createOscillator();
190-
const gain = ctx.createGain();
191-
192-
osc.type = type as OscillatorType;
193-
osc.frequency.setValueAtTime(freq, ctx.currentTime);
194-
195-
gain.gain.setValueAtTime(0.3, ctx.currentTime);
196-
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + dur);
197-
198-
osc.connect(gain);
199-
gain.connect(ctx.destination);
200-
201-
osc.start(ctx.currentTime + start);
202-
osc.stop(ctx.currentTime + start + dur);
203-
});
204-
} catch (e) {
205-
console.error('Audio play failed', e);
206-
}
207-
};
228+
}, [currentQuestion, getQuestionProgress, recordAttempt, recordQuestionCompleted]);
208229

209230
/**
210231
* Vai para a próxima questão não resolvida ou volta para a lista
211232
*/
212-
const handleNext = () => {
233+
const handleNext = useCallback(() => {
213234
let nextIndex = currentQuestionIndex + 1;
214235
let foundUnresolved = false;
215236

@@ -274,45 +295,34 @@ export function GamePage() {
274295
setView('world-questions');
275296
setCurrentQuestion(null);
276297
}
277-
};
298+
}, [
299+
currentQuestionIndex,
300+
worldQuestions,
301+
getQuestionProgress,
302+
selectedWorld,
303+
worldProgress,
304+
playSuccessSound,
305+
checkWorldAchievements,
306+
isPerfectRun,
307+
navigate
308+
]);
278309

279310
/**
280311
* Volta para a lista de questões do mundo
281312
*/
282-
const handleBackToQuestions = () => {
313+
const handleBackToQuestions = useCallback(() => {
283314
setView('world-questions');
284315
setCurrentQuestion(null);
285-
};
316+
}, []);
286317

287318
/**
288319
* Volta para o mapa de mundos
289320
*/
290-
const handleBackToMap = () => {
321+
const handleBackToMap = useCallback(() => {
291322
setView('world-map');
292323
setSelectedWorld(null);
293324
setWorldQuestions([]);
294-
};
295-
296-
/**
297-
* Obtém o nome amigável do mundo
298-
*/
299-
const getWorldName = (world: World): string => {
300-
const names: Record<World, string> = {
301-
basic_commands: 'Primeiros Passos',
302-
numbers: 'Números Mágicos',
303-
variables: 'Mundo das Variáveis',
304-
conditions: 'Terra das Decisões',
305-
decisions: 'Terra das Decisões',
306-
loops: 'Ilha da Repetição',
307-
functions: 'Vale das Funções',
308-
lists: 'Floresta das Listas',
309-
strings: 'Reino das Palavras',
310-
user_input: 'Conversando com o Usuário',
311-
dictionaries: 'Agenda Mágica',
312-
error_handling: 'Caçando Bugs',
313-
};
314-
return names[world] || world;
315-
};
325+
}, []);
316326

317327
// Mostra loading do Pyodide ou questões
318328
if (!ready || questionsLoading) {

0 commit comments

Comments
 (0)