diff --git a/.agent/logs/refactor-journal.md b/.agent/logs/refactor-journal.md index b01133b..14dd516 100644 --- a/.agent/logs/refactor-journal.md +++ b/.agent/logs/refactor-journal.md @@ -66,3 +66,13 @@ - `src/hooks/progress/useProgressStats.ts` (created) - **๐Ÿงน Architectural Gain:** Strict adherence to the Single Responsibility Principle (SRP). The main hook now acts purely as an orchestrator composing specialized sub-hooks. State is isolated from business actions (`recordAttempt`) and derived getters (`stats`, `getWorldStats`), vastly improving testability and code readability. - **๐Ÿ”ฌ Verification:** Ran `tsc -b --noEmit` with zero errors. `npm test` successfully executed all 593 tests. Both `npm run lint` and `npm run build` completed successfully. +## 2026-10-12 โ€” Extract gamification data processing logic into pure utility + +- **๐Ÿ’ก What:** Extracted the inline `processLoadedGamificationData` business logic for processing loaded gamification data (handling daily resets, legacy streak migration, streak updates, and pet status checks) from the `useGamificationStore` hook into a pure TypeScript function in `src/utils/gamification/loadLogic.ts`. +- **๐ŸŽฏ Why:** Code smell identified: The `useGamificationStore.ts` hook contained heavy inline business logic within a `useCallback` block inside `loadGamification` function to manage multiple game states and legacy fallback rules, making the hook large and violating the Single Responsibility Principle. +- **๐Ÿ“ Files Changed:** + - `src/utils/gamification/loadLogic.ts` (created) โ€” encapsulated the logic for processing the loaded gamification data. + - `src/hooks/gamification/useGamificationStore.ts` โ€” updated to import and use the new pure function. + - `src/utils/gamificationState.ts` โ€” updated to export the new logic module. +- **๐Ÿงน Architectural Gain:** SRP โ€” The state store component now delegates gamification data validation, migration, and update logic to a pure, testable function inside `src/utils/gamification/`. +- **๐Ÿ”ฌ Verification:** `tsc`, `npm test`, `npm run lint`, and `npm run build` all passed successfully. diff --git a/public/sitemap.xml b/public/sitemap.xml index d99d255..7362e61 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,97 +2,97 @@ https://pyexplorer.com.br - 2026-10-05 + 2026-10-09 weekly 1.0 https://pyexplorer.com.br/login - 2026-10-05 + 2026-10-09 monthly 0.5 https://pyexplorer.com.br/register - 2026-10-05 + 2026-10-09 monthly 0.5 https://pyexplorer.com.br/learn - 2026-10-05 + 2026-10-09 weekly 0.9 https://pyexplorer.com.br/python-para-criancas - 2026-10-05 + 2026-10-09 monthly 0.5 https://pyexplorer.com.br/aprender-python-jogando - 2026-10-05 + 2026-10-09 monthly 0.5 https://pyexplorer.com.br/about - 2026-10-05 + 2026-10-09 monthly 0.5 https://pyexplorer.com.br/learn/o-que-e-python - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/por-que-aprender-python - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/python-para-criancas - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/como-ensinar-python-criancas - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/primeiros-passos-python - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/jogos-aprender-programacao - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/exercicios-python-criancas - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/scratch-vs-python - 2026-10-05 + 2026-10-09 monthly 0.8 https://pyexplorer.com.br/learn/projetos-python-criancas - 2026-10-05 + 2026-10-09 monthly 0.8 diff --git a/src/hooks/gamification/useGamificationStore.ts b/src/hooks/gamification/useGamificationStore.ts index 8c8e378..7e6cc4c 100644 --- a/src/hooks/gamification/useGamificationStore.ts +++ b/src/hooks/gamification/useGamificationStore.ts @@ -2,14 +2,10 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import type { UserGamification, Achievement } from '../../types/gamification'; import type { UserData } from '../../types/question'; import { - checkDailyAndWeeklyReset, - applyStreakUpdate, - migrateLegacyStreak, checkAndUnlockAchievements, ensureEndgameMissions, + processLoadedGamificationData, } from '../../utils/gamificationState'; -import { calculateStreak } from '../../utils/gamificationUtils'; -import { checkPetStatus, getInitialPet } from '../../utils/petLogic'; import { getGamification, saveGamificationWithFallback } from '../../firebase/firestore'; const GUEST_GAMIFICATION_KEY = 'pyexplorer_guest_gamification'; @@ -107,66 +103,36 @@ export function useGamificationStore({ if (isGuest) { const stored = localStorage.getItem(GUEST_GAMIFICATION_KEY); if (stored) { - let data = JSON.parse(stored); - const resetted = checkDailyAndWeeklyReset(data); - if (resetted) { - data = resetted; - localStorage.setItem(GUEST_GAMIFICATION_KEY, JSON.stringify(data)); + const data = JSON.parse(stored); + const { finalData, hasUpdates, shieldUsed } = processLoadedGamificationData(data); + + if (hasUpdates) { + localStorage.setItem(GUEST_GAMIFICATION_KEY, JSON.stringify(finalData)); } - const { newState: updatedData, changed, shieldUsed } = applyStreakUpdate(data, calculateStreak); - if (changed) { - data = updatedData; - localStorage.setItem(GUEST_GAMIFICATION_KEY, JSON.stringify(data)); - if (shieldUsed) { - setMissionNotification({ - title: '๐Ÿ›ก๏ธ Escudo de Streak Ativado! Seu streak de ofensivas foi protegido contra 1 dia de inatividade!', - rewards: { stars: 0, xp: 0 } - }); - } + if (shieldUsed) { + setMissionNotification({ + title: '๐Ÿ›ก๏ธ Escudo de Streak Ativado! Seu streak de ofensivas foi protegido contra 1 dia de inatividade!', + rewards: { stars: 0, xp: 0 } + }); } - setGamification(data); + setGamification(finalData); } } else if (currentUserData) { const remoteData = await getGamification(currentUserData.uid); if (remoteData) { - let finalData = remoteData; - let hasUpdates = false; - - const resetted = checkDailyAndWeeklyReset(remoteData); - if (resetted) { - finalData = resetted; - hasUpdates = true; - } - const legacyUser = currentUserData as typeof currentUserData & LegacyStreakUserFields; - const legacyMigration = migrateLegacyStreak(finalData, legacyUser.streak, legacyUser.longestStreak, legacyUser.lastActiveDate); - if (legacyMigration.changed) { - finalData = legacyMigration.newState; - hasUpdates = true; - } - - const { newState: updatedFinalData, changed: streakChanged, shieldUsed: streakShieldUsed } = applyStreakUpdate(finalData, calculateStreak); - if (streakChanged) { - finalData = updatedFinalData; - hasUpdates = true; - if (streakShieldUsed) { - setMissionNotification({ - title: '๐Ÿ›ก๏ธ Escudo de Streak Ativado! Seu streak de ofensivas foi protegido contra 1 dia de inatividade!', - rewards: { stars: 0, xp: 0 } - }); - } - } - - if (!finalData.pet) { - finalData.pet = getInitialPet(); - hasUpdates = true; - } else { - const updatedPet = checkPetStatus(finalData.pet); - if (JSON.stringify(updatedPet) !== JSON.stringify(finalData.pet)) { - finalData.pet = updatedPet; - hasUpdates = true; - } + const { finalData, hasUpdates, shieldUsed } = processLoadedGamificationData(remoteData, { + streak: legacyUser.streak, + longestStreak: legacyUser.longestStreak, + lastActiveDate: legacyUser.lastActiveDate, + }); + + if (shieldUsed) { + setMissionNotification({ + title: '๐Ÿ›ก๏ธ Escudo de Streak Ativado! Seu streak de ofensivas foi protegido contra 1 dia de inatividade!', + rewards: { stars: 0, xp: 0 } + }); } setGamification(finalData); @@ -191,7 +157,15 @@ export function useGamificationStore({ } else { const initial = getInitialGamification(); const legacyUser = currentUserData as typeof currentUserData & LegacyStreakUserFields; - const { newState: initialWithLegacy } = migrateLegacyStreak(initial, legacyUser.streak, legacyUser.longestStreak, legacyUser.lastActiveDate); + const { finalData: initialWithLegacy } = processLoadedGamificationData( + initial, + { + streak: legacyUser.streak, + longestStreak: legacyUser.longestStreak, + lastActiveDate: legacyUser.lastActiveDate, + }, + true // isInitial flag to prevent immediate streak set to 1 + ); setGamification(initialWithLegacy); try { diff --git a/src/utils/gamification/loadLogic.ts b/src/utils/gamification/loadLogic.ts new file mode 100644 index 0000000..540827a --- /dev/null +++ b/src/utils/gamification/loadLogic.ts @@ -0,0 +1,76 @@ +import type { UserGamification } from '../../types/gamification'; +import { checkDailyAndWeeklyReset } from './missionLogic'; +import { migrateLegacyStreak, applyStreakUpdate } from './streakLogic'; +import { calculateStreak } from '../gamificationUtils'; +import { checkPetStatus, getInitialPet } from '../petLogic'; + +type LegacyStreakUserFields = { + streak?: number; + longestStreak?: number; + lastActiveDate?: string; +}; + +export interface ProcessedGamificationData { + finalData: UserGamification; + hasUpdates: boolean; + shieldUsed: boolean; +} + +export function processLoadedGamificationData( + remoteData: UserGamification, + legacyUserFields?: LegacyStreakUserFields, + isInitial: boolean = false +): ProcessedGamificationData { + let finalData = remoteData; + let hasUpdates = false; + + // 1. Daily/Weekly Reset + const resetted = checkDailyAndWeeklyReset(remoteData); + if (resetted) { + finalData = resetted; + hasUpdates = true; + } + + // 2. Legacy Migration + if (legacyUserFields) { + const legacyMigration = migrateLegacyStreak( + finalData, + legacyUserFields.streak, + legacyUserFields.longestStreak, + legacyUserFields.lastActiveDate + ); + if (legacyMigration.changed) { + finalData = legacyMigration.newState; + hasUpdates = true; + } + } + + // 3. Streak Update (skip if it's the very first time initializing to avoid falsely setting streak to 1) + let streakShieldUsed = false; + if (!isInitial) { + const { newState: updatedFinalData, changed: streakChanged, shieldUsed } = applyStreakUpdate(finalData, calculateStreak); + if (streakChanged) { + finalData = updatedFinalData; + hasUpdates = true; + streakShieldUsed = shieldUsed; + } + } + + // 4. Pet Status Check + if (!finalData.pet) { + finalData.pet = getInitialPet(); + hasUpdates = true; + } else { + const updatedPet = checkPetStatus(finalData.pet); + if (JSON.stringify(updatedPet) !== JSON.stringify(finalData.pet)) { + finalData.pet = updatedPet; + hasUpdates = true; + } + } + + return { + finalData, + hasUpdates, + shieldUsed: streakShieldUsed, + }; +} diff --git a/src/utils/gamificationState.ts b/src/utils/gamificationState.ts index 6001386..4297fd4 100644 --- a/src/utils/gamificationState.ts +++ b/src/utils/gamificationState.ts @@ -9,3 +9,4 @@ export * from './gamification/shopLogic'; export * from './gamification/powerUpLogic'; export * from './gamification/achievementLogic'; export * from './gamification/streakLogic'; +export * from './gamification/loadLogic';