Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .agent/logs/refactor-journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
32 changes: 16 additions & 16 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,97 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://pyexplorer.com.br</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/login</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/register</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/python-para-criancas</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/aprender-python-jogando</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/about</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/o-que-e-python</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/por-que-aprender-python</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/python-para-criancas</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/como-ensinar-python-criancas</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/primeiros-passos-python</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/jogos-aprender-programacao</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/exercicios-python-criancas</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/scratch-vs-python</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://pyexplorer.com.br/learn/projetos-python-criancas</loc>
<lastmod>2026-10-05</lastmod>
<lastmod>2026-10-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
Expand Down
90 changes: 32 additions & 58 deletions src/hooks/gamification/useGamificationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down
76 changes: 76 additions & 0 deletions src/utils/gamification/loadLogic.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
1 change: 1 addition & 0 deletions src/utils/gamificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './gamification/shopLogic';
export * from './gamification/powerUpLogic';
export * from './gamification/achievementLogic';
export * from './gamification/streakLogic';
export * from './gamification/loadLogic';
Loading