Spaces:
Running
Running
| import { useEffect, useMemo, useState } from 'react'; | |
| import { | |
| createSession, | |
| deleteSession, | |
| loadSessions, | |
| updateSession, | |
| } from './utils/storage'; | |
| import { | |
| buildChartData, | |
| buildGradeProgressData, | |
| buildPainChartData, | |
| computeMaxRoutesThreshold, | |
| computeRecommendation, | |
| computeWeeklySummary, | |
| getWeekKey, | |
| groupByWeek, | |
| } from './utils/weekUtils'; | |
| import ClimbForm from './components/ClimbForm'; | |
| import WeeklySummary from './components/WeeklySummary'; | |
| import Charts from './components/Charts'; | |
| import SessionLog from './components/SessionLog'; | |
| import './App.css'; | |
| function App() { | |
| const [sessions, setSessions] = useState([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [syncError, setSyncError] = useState(''); | |
| useEffect(() => { | |
| let active = true; | |
| async function bootstrapSessions() { | |
| try { | |
| const loaded = await loadSessions(); | |
| if (active) { | |
| setSessions(Array.isArray(loaded) ? loaded : []); | |
| setSyncError(''); | |
| } | |
| } catch { | |
| if (active) { | |
| setSyncError('Could not load shared session data.'); | |
| } | |
| } finally { | |
| if (active) setIsLoading(false); | |
| } | |
| } | |
| bootstrapSessions(); | |
| return () => { | |
| active = false; | |
| }; | |
| }, []); | |
| const today = new Date().toISOString().split('T')[0]; | |
| const currentWeekKey = getWeekKey(today); | |
| const weeklyGroups = useMemo(() => groupByWeek(sessions), [sessions]); | |
| const chartData = useMemo(() => buildChartData(sessions), [sessions]); | |
| const gradeProgressData = useMemo(() => buildGradeProgressData(sessions), [sessions]); | |
| const painData = useMemo(() => buildPainChartData(sessions), [sessions]); | |
| const allWeekKeys = useMemo( | |
| () => [...new Set([...Object.keys(weeklyGroups), currentWeekKey])].sort(), | |
| [weeklyGroups, currentWeekKey] | |
| ); | |
| const [selectedWeekKey, setSelectedWeekKey] = useState(currentWeekKey); | |
| useEffect(() => { | |
| if (!allWeekKeys.includes(selectedWeekKey)) { | |
| setSelectedWeekKey(currentWeekKey); | |
| } | |
| }, [allWeekKeys, selectedWeekKey, currentWeekKey]); | |
| const selectedIdx = allWeekKeys.indexOf(selectedWeekKey); | |
| const hasPrevWeek = selectedIdx > 0; | |
| const hasNextWeek = selectedIdx >= 0 && selectedIdx < allWeekKeys.length - 1; | |
| const selectedWeekSessions = weeklyGroups[selectedWeekKey] || []; | |
| const selectedWeekSummary = computeWeeklySummary(selectedWeekSessions); | |
| const weeksBeforeSelected = allWeekKeys.slice(0, Math.max(selectedIdx + 1, 1)).slice(-4); | |
| const recentSummaries = weeksBeforeSelected.map((key) => computeWeeklySummary(weeklyGroups[key] || [])); | |
| const recommendation = computeRecommendation(recentSummaries); | |
| const maxRoutes = computeMaxRoutesThreshold(sessions, today); | |
| async function handleAddSession(newSession) { | |
| const sessionWithId = { ...newSession, id: crypto.randomUUID() }; | |
| try { | |
| const created = await createSession(sessionWithId); | |
| setSessions((prev) => [created, ...prev.filter((session) => session.id !== created.id)]); | |
| setSyncError(''); | |
| } catch { | |
| setSyncError('Could not save session.'); | |
| } | |
| } | |
| async function handleEditSession(id, updatedFields) { | |
| try { | |
| const updated = await updateSession(id, updatedFields); | |
| setSessions((prev) => prev.map((session) => (session.id === id ? updated : session))); | |
| setSyncError(''); | |
| } catch { | |
| setSyncError('Could not update session.'); | |
| } | |
| } | |
| async function handleDeleteSession(id) { | |
| try { | |
| await deleteSession(id); | |
| setSessions((prev) => prev.filter((session) => session.id !== id)); | |
| setSyncError(''); | |
| } catch { | |
| setSyncError('Could not delete session.'); | |
| } | |
| } | |
| return ( | |
| <div className="dashboard"> | |
| <header className="dashboard-header"> | |
| <h1>Climbing Dashboard</h1> | |
| <p>Log each climbing session and track routes, load, and RPE over time.</p> | |
| {syncError && <p className="sync-error">{syncError}</p>} | |
| </header> | |
| <main className="dashboard-main"> | |
| {isLoading ? ( | |
| <p className="loading-message">Loading shared sessions...</p> | |
| ) : ( | |
| <> | |
| <div className="dashboard-top"> | |
| <ClimbForm onAddSession={handleAddSession} /> | |
| <WeeklySummary | |
| summary={selectedWeekSummary} | |
| recommendation={recommendation} | |
| weekKey={selectedWeekKey} | |
| isCurrentWeek={selectedWeekKey === currentWeekKey} | |
| weeksOfData={weeksBeforeSelected.length} | |
| maxRoutes={maxRoutes} | |
| hasPrevWeek={hasPrevWeek} | |
| hasNextWeek={hasNextWeek} | |
| onPrevWeek={() => hasPrevWeek && setSelectedWeekKey(allWeekKeys[selectedIdx - 1])} | |
| onNextWeek={() => hasNextWeek && setSelectedWeekKey(allWeekKeys[selectedIdx + 1])} | |
| /> | |
| </div> | |
| <Charts data={chartData} gradeData={gradeProgressData} painData={painData} /> | |
| <SessionLog | |
| sessions={sessions} | |
| onEditSession={handleEditSession} | |
| onDeleteSession={handleDeleteSession} | |
| /> | |
| </> | |
| )} | |
| </main> | |
| </div> | |
| ); | |
| } | |
| export default App; | |