Spaces:
Running
Running
| import { useState, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| export const useProfileStats = (user) => { | |
| const [stats, setStats] = useState({ | |
| goalsMapped: 0, | |
| nodesCreated: 0, | |
| nodesCompleted: 0, | |
| dayStreak: 0, | |
| points: 0, | |
| xp: 0, | |
| maxXp: 1200, | |
| level: 1, | |
| achievements: [], | |
| loading: true | |
| }); | |
| const API_URL = import.meta.env.VITE_API_URL; | |
| useEffect(() => { | |
| if (!user) { | |
| setStats(prev => ({ ...prev, loading: false })); | |
| return; | |
| } | |
| const fetchStats = async () => { | |
| try { | |
| setStats(prev => ({ ...prev, loading: true })); | |
| console.log('[ProfileStats] fetchStats starting. API_URL =', API_URL, 'User =', user); | |
| // Fetch projects (goals) | |
| const graphsRes = await axios.get(`${API_URL}/graphs/user/${user.id}`); | |
| const graphs = graphsRes.data; | |
| console.log('[ProfileStats] Graphs fetched successfully:', graphs); | |
| const goalsMapped = graphs.length; | |
| // Fetch nodes for each project | |
| let nodesCreated = 0; | |
| let nodesCompleted = 0; | |
| let hasConditionalNodes = false; | |
| let hasFinalNodes = false; | |
| const nodesPromises = graphs.map(g => axios.get(`${API_URL}/graphs/${g.id}/full`).catch((err) => { | |
| console.warn(`[ProfileStats] Failed to fetch nodes for graph ${g.id}:`, err); | |
| return { data: { nodes: [] } }; | |
| })); | |
| const nodesResponses = await Promise.all(nodesPromises); | |
| nodesResponses.forEach(res => { | |
| const nodes = res.data?.nodes || []; | |
| nodesCreated += nodes.length; | |
| nodesCompleted += nodes.filter(n => n.state === 1 || n.State === 1 || n.state === 'Done' || n.State === 'Done').length; | |
| if (nodes.some(n => { | |
| const t = (n.type || n.Type || '').toLowerCase(); | |
| return t === 'conditional' || t === 'condition'; | |
| })) hasConditionalNodes = true; | |
| if (nodes.some(n => { | |
| const t = (n.type || n.Type || '').toLowerCase(); | |
| return t === 'final' || t === 'goal'; | |
| })) hasFinalNodes = true; | |
| }); | |
| console.log('[ProfileStats] Nodes count fetched:', nodesCreated, 'Completed:', nodesCompleted); | |
| // Fetch habits for streak | |
| let dayStreak = 0; | |
| try { | |
| const habitsRes = await axios.get(`${API_URL}/habits/user/${user.id}`); | |
| const habits = habitsRes.data; | |
| console.log('[ProfileStats] Habits fetched successfully:', habits); | |
| // Calculate overall day streak | |
| const uniqueDates = [...new Set(habits.map(h => new Date(h.checkedDate).toISOString().split('T')[0]))].sort().reverse(); | |
| let expectedDate = new Date(); | |
| for (const dateStr of uniqueDates) { | |
| const d = new Date(dateStr); | |
| if (d.toISOString().split('T')[0] === expectedDate.toISOString().split('T')[0]) { | |
| dayStreak++; | |
| expectedDate.setDate(expectedDate.getDate() - 1); | |
| } else if (d > expectedDate) { | |
| continue; | |
| } else { | |
| // If the most recent date was yesterday, that's fine, we might still have a streak | |
| if (dayStreak === 0 && d.toISOString().split('T')[0] === new Date(Date.now() - 86400000).toISOString().split('T')[0]) { | |
| dayStreak++; | |
| expectedDate = new Date(Date.now() - 86400000); | |
| expectedDate.setDate(expectedDate.getDate() - 1); | |
| } else { | |
| break; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.error('[ProfileStats] Failed to fetch habits:', e); | |
| } | |
| console.log('[ProfileStats] Day streak calculated:', dayStreak); | |
| // Calculate points, XP, level | |
| const points = (goalsMapped * 50) + (nodesCreated * 10) + (nodesCompleted * 20) + (dayStreak * 20); | |
| const level = Math.floor(points / 1200) + 1; | |
| const xp = points % 1200; | |
| console.log('[ProfileStats] Calculated totals: Points =', points, 'Level =', level, 'XP =', xp); | |
| // Fetch achievements from new API | |
| let achievements = []; | |
| try { | |
| console.log('[ProfileStats] Fetching achievements progress from API...'); | |
| const achRes = await axios.get(`${API_URL}/achievements/progress`); | |
| console.log('[ProfileStats] Achievements progress raw response:', achRes.data); | |
| const mapCategoryToColor = (category) => { | |
| switch (category) { | |
| case 'Projects': return 'bg-blue-100'; | |
| case 'Tasks': return 'bg-yellow-100'; | |
| case 'Habits': return 'bg-orange-100'; | |
| case 'System': return 'bg-gray-100'; | |
| default: return 'bg-green-100'; | |
| } | |
| }; | |
| const mapTitleToIcon = (title) => { | |
| if (title.includes('Canvas')) return 'Map'; | |
| if (title.includes('Connect') || title.includes('Decision')) return 'ShieldCheck'; | |
| if (title.includes('Zap')) return 'Zap'; | |
| if (title.includes('Daily')) return 'Flame'; | |
| if (title.includes('Clear Desk')) return 'Target'; | |
| return 'Trophy'; | |
| }; | |
| achievements = achRes.data.map(item => ({ | |
| icon: item.achievement.iconUrl || mapTitleToIcon(item.achievement.title), | |
| title: item.achievement.title, | |
| desc: item.achievement.description, | |
| unlocked: item.isUnlocked, | |
| color: mapCategoryToColor(item.achievement.category), | |
| currentValue: item.currentValue, | |
| targetValue: item.achievement.targetValue | |
| })); | |
| console.log('[ProfileStats] Achievements mapped successfully:', achievements); | |
| } catch (e) { | |
| console.error('[ProfileStats] Failed to fetch achievements:', e); | |
| // Fallback to empty if API fails | |
| achievements = []; | |
| } | |
| setStats({ | |
| goalsMapped, | |
| nodesCreated, | |
| nodesCompleted, | |
| dayStreak, | |
| points: points >= 1000 ? (points/1000).toFixed(1) + 'k' : points.toString(), | |
| xp, | |
| maxXp: 1200, | |
| level, | |
| achievements, | |
| loading: false | |
| }); | |
| } catch (err) { | |
| console.error('[ProfileStats] Critical error in fetchStats main loop:', err); | |
| setStats(prev => ({ ...prev, loading: false })); | |
| } | |
| }; | |
| fetchStats(); | |
| }, [user]); | |
| return stats; | |
| }; | |