Spaces:
Runtime error
Runtime error
| import { create } from "zustand"; | |
| import { persist } from "zustand/middleware"; | |
| /* βββββββββββββββββββ Types βββββββββββββββββββ */ | |
| export interface UserPreferences { | |
| soundOn: boolean; | |
| darkGlass: boolean; | |
| difficulty: number; // 0-100 | |
| } | |
| export interface UserState { | |
| /* ββ Identity ββ */ | |
| name: string; | |
| title: string; | |
| /* ββ Progression ββ */ | |
| xp: number; | |
| level: number; | |
| xpToNextLevel: number; | |
| streak: number; | |
| longestStreak: number; | |
| gems: number; | |
| coursesCompleted: number; | |
| /* ββ Navigation ββ */ | |
| lastActiveCourseId: string | null; | |
| lastActiveNodeId: string | null; | |
| /* ββ Preferences ββ */ | |
| preferences: UserPreferences; | |
| /* ββ Onboarding ββ */ | |
| onboarded: boolean; | |
| selectedTopics: string[]; | |
| dailyGoal: string; | |
| } | |
| export interface UserActions { | |
| /* ββ Identity ββ */ | |
| setName: (name: string) => void; | |
| setTitle: (title: string) => void; | |
| /* ββ XP & Leveling ββ */ | |
| addXp: (amount: number) => void; | |
| /* ββ Gems ββ */ | |
| addGems: (amount: number) => void; | |
| spendGems: (amount: number) => boolean; // returns false if insufficient | |
| /* ββ Streak ββ */ | |
| incrementStreak: () => void; | |
| resetStreak: () => void; | |
| /* ββ Courses ββ */ | |
| incrementCoursesCompleted: () => void; | |
| /* ββ Navigation ββ */ | |
| setLastActiveCourse: (courseId: string) => void; | |
| setLastActiveNode: (nodeId: string) => void; | |
| /* ββ Preferences ββ */ | |
| setPreferences: (prefs: Partial<UserPreferences>) => void; | |
| /* ββ Onboarding ββ */ | |
| completeOnboarding: (data: { | |
| name: string; | |
| topics: string[]; | |
| goal: string; | |
| }) => void; | |
| /* ββ Reset ββ */ | |
| logout: () => void; | |
| } | |
| /* βββββββββββββββββββ Helpers βββββββββββββββββββ */ | |
| /** XP required for a given level */ | |
| function xpForLevel(level: number): number { | |
| return 100 + (level - 1) * 50; // Level 1 = 100, Level 2 = 150, etc. | |
| } | |
| const INITIAL_STATE: UserState = { | |
| name: "Explorer", | |
| title: "Novice Learner", | |
| xp: 0, | |
| level: 1, | |
| xpToNextLevel: xpForLevel(1), | |
| streak: 12, | |
| longestStreak: 28, | |
| gems: 1500, | |
| coursesCompleted: 3, | |
| lastActiveCourseId: null, | |
| lastActiveNodeId: null, | |
| preferences: { | |
| soundOn: true, | |
| darkGlass: true, | |
| difficulty: 50, | |
| }, | |
| onboarded: false, | |
| selectedTopics: [], | |
| dailyGoal: "", | |
| }; | |
| /* βββββββββββββββββββ Store βββββββββββββββββββ */ | |
| const useUserStore = create<UserState & UserActions>()( | |
| persist( | |
| (set, get) => ({ | |
| ...INITIAL_STATE, | |
| /* ββ Identity ββ */ | |
| setName: (name) => set({ name }), | |
| setTitle: (title) => set({ title }), | |
| /* ββ XP & Leveling ββ */ | |
| addXp: (amount) => { | |
| const state = get(); | |
| let newXp = state.xp + amount; | |
| let newLevel = state.level; | |
| let xpNeeded = state.xpToNextLevel; | |
| let newTitle = state.title; | |
| // Level-up loop (handle multi-level jumps) | |
| while (newXp >= xpNeeded) { | |
| newXp -= xpNeeded; | |
| newLevel += 1; | |
| xpNeeded = xpForLevel(newLevel); | |
| // Update title based on level | |
| if (newLevel >= 20) newTitle = "Grandmaster Scholar"; | |
| else if (newLevel >= 15) newTitle = "Master Scholar"; | |
| else if (newLevel >= 10) newTitle = "Expert Scholar"; | |
| else if (newLevel >= 5) newTitle = "Quantum Scholar"; | |
| else if (newLevel >= 3) newTitle = "Apprentice Scholar"; | |
| else newTitle = "Novice Learner"; | |
| } | |
| set({ | |
| xp: newXp, | |
| level: newLevel, | |
| xpToNextLevel: xpNeeded, | |
| title: newTitle, | |
| }); | |
| }, | |
| /* ββ Gems ββ */ | |
| addGems: (amount) => set((s) => ({ gems: s.gems + amount })), | |
| spendGems: (amount) => { | |
| const state = get(); | |
| if (state.gems < amount) return false; | |
| set({ gems: state.gems - amount }); | |
| return true; | |
| }, | |
| /* ββ Streak ββ */ | |
| incrementStreak: () => | |
| set((s) => ({ | |
| streak: s.streak + 1, | |
| longestStreak: Math.max(s.longestStreak, s.streak + 1), | |
| })), | |
| resetStreak: () => set({ streak: 0 }), | |
| /* ββ Courses ββ */ | |
| incrementCoursesCompleted: () => | |
| set((s) => ({ coursesCompleted: s.coursesCompleted + 1 })), | |
| /* ββ Navigation ββ */ | |
| setLastActiveCourse: (courseId) => | |
| set({ lastActiveCourseId: courseId }), | |
| setLastActiveNode: (nodeId) => set({ lastActiveNodeId: nodeId }), | |
| /* ββ Preferences ββ */ | |
| setPreferences: (prefs) => | |
| set((s) => ({ | |
| preferences: { ...s.preferences, ...prefs }, | |
| })), | |
| /* ββ Onboarding ββ */ | |
| completeOnboarding: ({ name, topics, goal }) => | |
| set({ | |
| name, | |
| selectedTopics: topics, | |
| dailyGoal: goal, | |
| onboarded: true, | |
| }), | |
| /* ββ Reset ββ */ | |
| logout: () => set(INITIAL_STATE), | |
| }), | |
| { | |
| name: "learn8-user", | |
| } | |
| ) | |
| ); | |
| export default useUserStore; | |