'use client'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { UserProgress, levelFromXP } from '@/types/user'; import { getAvailableNodes, skillNodes } from '@/data/skill-tree'; import { getChallengesForNode } from '@/data/challenges/math'; interface ProgressStore extends UserProgress { completeChallenge: (challengeId: string, timeSpent: number) => void; getNodeStatus: (nodeId: string) => 'locked' | 'available' | 'in-progress' | 'completed'; getCompletedNodeIds: () => string[]; updateStreak: () => void; reset: () => void; } const initialState: UserProgress = { completedChallenges: {}, unlockedNodes: [], totalXP: 0, level: 1, currentStreak: 0, longestStreak: 0, lastActiveDate: '', achievements: [], }; export const useProgressStore = create()( persist( (set, get) => ({ ...initialState, completeChallenge: (challengeId: string, timeSpent: number) => { const state = get(); if (state.completedChallenges[challengeId]) return; // Find challenge xp const allChallenges = skillNodes.flatMap((n) => getChallengesForNode(n.id) ); const challenge = allChallenges.find((c) => c.id === challengeId); const xpReward = challenge?.xpReward ?? 20; const newCompleted = { ...state.completedChallenges, [challengeId]: { bestScore: 100, completedAt: new Date().toISOString(), timeSpent, attempts: 1, }, }; const newXP = state.totalXP + xpReward; // Check which nodes are now completed const completedNodeIds: string[] = []; for (const node of skillNodes) { const allDone = node.challenges.every( (cId) => newCompleted[`${node.id}/${cId}`] ); if (allDone) completedNodeIds.push(node.id); } const today = new Date().toISOString().split('T')[0]; let streak = state.currentStreak; let longest = state.longestStreak; if (state.lastActiveDate !== today) { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().split('T')[0]; if (state.lastActiveDate === yesterdayStr) { streak += 1; } else if (state.lastActiveDate !== today) { streak = 1; } longest = Math.max(longest, streak); } // Check achievements const newAchievements = [...state.achievements]; const completedCount = Object.keys(newCompleted).length; if (completedCount >= 1 && !newAchievements.includes('first-step')) { newAchievements.push('first-step'); } if (completedCount >= 10 && !newAchievements.includes('getting-started')) { newAchievements.push('getting-started'); } if (completedNodeIds.length >= 3 && !newAchievements.includes('explorer')) { newAchievements.push('explorer'); } if (completedNodeIds.length >= 5 && !newAchievements.includes('scholar')) { newAchievements.push('scholar'); } if (streak >= 3 && !newAchievements.includes('consistent')) { newAchievements.push('consistent'); } if (streak >= 7 && !newAchievements.includes('dedicated')) { newAchievements.push('dedicated'); } // Cross-branch: check if completed nodes span multiple branches const branches = new Set( completedNodeIds .map((id) => skillNodes.find((n) => n.id === id)?.branch) .filter(Boolean) ); if (branches.size >= 3 && !newAchievements.includes('polymath')) { newAchievements.push('polymath'); } set({ completedChallenges: newCompleted, unlockedNodes: completedNodeIds, totalXP: newXP, level: levelFromXP(newXP), currentStreak: streak, longestStreak: longest, lastActiveDate: today, achievements: newAchievements, }); }, getCompletedNodeIds: () => { const state = get(); const completedNodeIds: string[] = []; for (const node of skillNodes) { const allDone = node.challenges.length > 0 && node.challenges.every( (cId) => state.completedChallenges[`${node.id}/${cId}`] ); if (allDone) completedNodeIds.push(node.id); } return completedNodeIds; }, getNodeStatus: (nodeId: string) => { const state = get(); const node = skillNodes.find((n) => n.id === nodeId); if (!node) return 'locked'; const completedNodeIds = get().getCompletedNodeIds(); const allDone = node.challenges.length > 0 && node.challenges.every( (cId) => state.completedChallenges[`${node.id}/${cId}`] ); if (allDone) return 'completed'; const someDone = node.challenges.some( (cId) => state.completedChallenges[`${node.id}/${cId}`] ); if (someDone) return 'in-progress'; const prereqsMet = node.prerequisites.every((p) => completedNodeIds.includes(p) ); if (prereqsMet || node.prerequisites.length === 0) return 'available'; return 'locked'; }, updateStreak: () => { const state = get(); const today = new Date().toISOString().split('T')[0]; if (state.lastActiveDate === today) return; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().split('T')[0]; if (state.lastActiveDate !== yesterdayStr) { set({ currentStreak: 0 }); } }, reset: () => set(initialState), }), { name: 'project-math-progress', } ) );