1. Tailwind migretion. 2. Backend Type Safety. 3. Context Refactoring.

This commit is contained in:
AG
2025-12-07 21:54:32 +02:00
parent e893336d46
commit 57f7ad077e
27 changed files with 1536 additions and 580 deletions

View File

@@ -8,27 +8,12 @@ import AICoach from './components/AICoach';
import Plans from './components/Plans';
import Login from './components/Login';
import Profile from './components/Profile';
import { Language, User } from './types'; // Removed unused imports
import { Language, User } from './types';
import { getSystemLanguage } from './services/i18n';
import { useAuth } from './context/AuthContext';
import { useData } from './context/DataContext';
function App() {
const { currentUser, updateUser, logout } = useAuth();
const {
sessions,
plans,
activeSession,
activePlan,
startSession,
endSession,
quitSession,
addSet,
removeSet,
updateSet,
updateSession,
deleteSessionById
} = useData();
const [language, setLanguage] = useState<Language>('en');
const navigate = useNavigate();
@@ -70,36 +55,19 @@ function App() {
)
} />
<Route path="/" element={
<Tracker
userId={currentUser?.id || ''}
userWeight={currentUser?.profile?.weight}
activeSession={activeSession}
activePlan={activePlan}
onSessionStart={startSession}
onSessionEnd={endSession}
onSessionQuit={quitSession}
onSetAdded={addSet}
onRemoveSet={removeSet}
onUpdateSet={updateSet}
lang={language}
/>
<Tracker lang={language} />
} />
<Route path="/plans" element={
<Plans userId={currentUser?.id || ''} onStartPlan={startSession} lang={language} />
<Plans lang={language} />
} />
<Route path="/history" element={
<History
sessions={sessions}
onUpdateSession={updateSession}
onDeleteSession={deleteSessionById}
lang={language}
/>
<History lang={language} />
} />
<Route path="/stats" element={
<Stats sessions={sessions} lang={language} />
<Stats lang={language} />
} />
<Route path="/coach" element={
<AICoach history={sessions} userProfile={currentUser?.profile} plans={plans} lang={language} />
<AICoach lang={language} />
} />
<Route path="/profile" element={
<Profile

View File

@@ -6,11 +6,10 @@ import { WorkoutSession, Language, UserProfile, WorkoutPlan } from '../types';
import { Chat, GenerateContentResponse } from '@google/genai';
import { t } from '../services/i18n';
import { generateId } from '../utils/uuid';
import { useAuth } from '../context/AuthContext';
import { useSession } from '../context/SessionContext';
interface AICoachProps {
history: WorkoutSession[];
userProfile?: UserProfile;
plans?: WorkoutPlan[];
lang: Language;
}
@@ -20,7 +19,11 @@ interface Message {
text: string;
}
const AICoach: React.FC<AICoachProps> = ({ history, userProfile, plans, lang }) => {
const AICoach: React.FC<AICoachProps> = ({ lang }) => {
const { currentUser } = useAuth();
const { sessions: history, plans } = useSession();
const userProfile = currentUser?.profile;
const [messages, setMessages] = useState<Message[]>([
{ id: 'intro', role: 'model', text: t('ai_intro', lang) }
]);

View File

@@ -3,16 +3,16 @@ import React, { useState } from 'react';
import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react';
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
import { t } from '../services/i18n';
import { useSession } from '../context/SessionContext';
interface HistoryProps {
sessions: WorkoutSession[];
onUpdateSession?: (session: WorkoutSession) => void;
onDeleteSession?: (sessionId: string) => void;
lang: Language;
}
const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSession, lang }) => {
const History: React.FC<HistoryProps> = ({ lang }) => {
const { sessions, updateSession, deleteSession } = useSession();
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
@@ -60,10 +60,15 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
return `${minutes}m`;
};
const handleSaveEdit = () => {
if (editingSession && onUpdateSession) {
onUpdateSession(editingSession);
setEditingSession(null);
const handleSaveEdit = async () => {
if (editingSession) {
try {
await updateSession(editingSession);
setEditingSession(null);
} catch (e) {
console.error("Failed to update session", e);
}
}
};
@@ -83,11 +88,15 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
});
};
const handleConfirmDelete = () => {
if (deletingId && onDeleteSession) {
onDeleteSession(deletingId);
setDeletingId(null);
} else if (deletingSetInfo && onUpdateSession) {
const handleConfirmDelete = async () => {
if (deletingId) {
try {
await deleteSession(deletingId);
setDeletingId(null);
} catch (e) {
console.error("Failed to delete session", e);
}
} else if (deletingSetInfo) {
// Find the session
const session = sessions.find(s => s.id === deletingSetInfo.sessionId);
if (session) {
@@ -96,7 +105,11 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
...session,
sets: session.sets.filter(s => s.id !== deletingSetInfo.setId)
};
onUpdateSession(updatedSession);
try {
await updateSession(updatedSession);
} catch (e) {
console.error("Failed to update session after set delete", e);
}
}
setDeletingSetInfo(null);
}
@@ -104,6 +117,7 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
if (sessions.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full text-on-surface-variant p-8 text-center">

View File

@@ -2,21 +2,26 @@
import React, { useState, useEffect } from 'react';
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical } from 'lucide-react';
import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types';
import { getPlans, savePlan, deletePlan, getExercises, saveExercise } from '../services/storage';
import { getExercises, saveExercise } from '../services/storage';
import { t } from '../services/i18n';
import { generateId } from '../utils/uuid';
import { useAuth } from '../context/AuthContext';
import { useSession } from '../context/SessionContext';
import { useActiveWorkout } from '../context/ActiveWorkoutContext';
import FilledInput from './FilledInput';
import { toTitleCase } from '../utils/text';
interface PlansProps {
userId: string;
onStartPlan: (plan: WorkoutPlan) => void;
lang: Language;
}
const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const Plans: React.FC<PlansProps> = ({ lang }) => {
const { currentUser } = useAuth();
const userId = currentUser?.id || '';
const { plans, savePlan, deletePlan } = useSession();
const { startSession } = useActiveWorkout();
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
@@ -39,9 +44,6 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
useEffect(() => {
const loadData = async () => {
const fetchedPlans = await getPlans(userId);
setPlans(fetchedPlans);
const fetchedExercises = await getExercises(userId);
// Filter out archived exercises
if (Array.isArray(fetchedExercises)) {
@@ -50,7 +52,7 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
setAvailableExercises([]);
}
};
loadData();
if (userId) loadData();
}, [userId]);
const handleCreateNew = () => {
@@ -72,18 +74,14 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
const handleSave = async () => {
if (!name.trim() || !editId) return;
const newPlan: WorkoutPlan = { id: editId, name, description, steps };
await savePlan(userId, newPlan);
const updated = await getPlans(userId);
setPlans(updated);
await savePlan(newPlan);
setIsEditing(false);
};
const handleDelete = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (confirm(t('delete_confirm', lang))) {
await deletePlan(userId, id);
const updated = await getPlans(userId);
setPlans(updated);
await deletePlan(id);
}
};
@@ -391,7 +389,7 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
{plan.steps.length} {t('exercises_count', lang)}
</div>
<button
onClick={() => onStartPlan(plan)}
onClick={() => startSession(plan)}
className="flex items-center gap-2 bg-primary text-on-primary px-5 py-2 rounded-full text-sm font-medium hover:shadow-elevation-2 transition-all"
>
<PlayCircle size={18} />

View File

@@ -1,16 +1,16 @@
import React, { useMemo, useState, useEffect } from 'react';
import { WorkoutSession, ExerciseType, Language, BodyWeightRecord } from '../types';
import { getWeightHistory } from '../services/weight';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { t } from '../services/i18n';
import { useSession } from '../context/SessionContext';
interface StatsProps {
sessions: WorkoutSession[];
lang: Language;
}
const Stats: React.FC<StatsProps> = ({ sessions, lang }) => {
const Stats: React.FC<StatsProps> = ({ lang }) => {
const { sessions } = useSession();
const [weightRecords, setWeightRecords] = useState<BodyWeightRecord[]>([]);
useEffect(() => {

View File

@@ -1,30 +1,18 @@
import React from 'react';
import { WorkoutSession, WorkoutSet, WorkoutPlan, Language } from '../../types';
import { Language } from '../../types';
import { useTracker } from './useTracker';
import IdleView from './IdleView';
import SporadicView from './SporadicView';
import ActiveSessionView from './ActiveSessionView';
interface TrackerProps {
userId: string;
userWeight?: number;
activeSession: WorkoutSession | null;
activePlan: WorkoutPlan | null;
onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void;
onSessionEnd: () => void;
onSessionQuit: () => void;
onSetAdded: (set: WorkoutSet) => void;
onRemoveSet: (setId: string) => void;
onUpdateSet: (set: WorkoutSet) => void;
onSporadicSetAdded?: () => void;
lang: Language;
}
const Tracker: React.FC<TrackerProps> = (props) => {
const tracker = useTracker(props);
const { isSporadicMode } = tracker;
const { activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet } = props;
const Tracker: React.FC<TrackerProps> = ({ lang }) => {
const tracker = useTracker({}); // No props needed, hook uses context
const { activeSession, isSporadicMode, onSessionEnd, onSessionQuit, onRemoveSet } = tracker;
if (activeSession) {
return (

View File

@@ -1,36 +1,32 @@
import { useState, useEffect } from 'react';
import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, WorkoutPlan } from '../../types';
import { WorkoutSession, WorkoutSet, ExerciseDef, WorkoutPlan } from '../../types';
import { getExercises, saveExercise, getPlans } from '../../services/storage';
import { api } from '../../services/api';
import { useSessionTimer } from '../../hooks/useSessionTimer';
import { useWorkoutForm } from '../../hooks/useWorkoutForm';
import { usePlanExecution } from '../../hooks/usePlanExecution';
import { useAuth } from '../../context/AuthContext';
import { useActiveWorkout } from '../../context/ActiveWorkoutContext';
import { useSession } from '../../context/SessionContext';
interface UseTrackerProps {
userId: string;
userWeight?: number;
activeSession: WorkoutSession | null;
activePlan: WorkoutPlan | null;
onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void;
onSessionEnd: () => void;
onSessionQuit: () => void;
onSetAdded: (set: WorkoutSet) => void;
onRemoveSet: (setId: string) => void;
onUpdateSet: (set: WorkoutSet) => void;
onSporadicSetAdded?: () => void;
}
export const useTracker = (props: any) => { // Props ignored/removed
const { currentUser } = useAuth();
const userId = currentUser?.id || '';
const userWeight = currentUser?.profile?.weight;
const {
activeSession,
activePlan,
startSession,
addSet,
updateSet,
quitSession,
endSession,
removeSet
} = useActiveWorkout();
const { refreshData: refreshHistory } = useSession();
export const useTracker = ({
userId,
userWeight,
activeSession,
activePlan,
onSessionStart,
onSessionEnd,
onSetAdded,
onUpdateSet,
onSporadicSetAdded
}: UseTrackerProps) => {
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [selectedExercise, setSelectedExercise] = useState<ExerciseDef | null>(null);
@@ -56,11 +52,18 @@ export const useTracker = ({
// Hooks
const elapsedTime = useSessionTimer(activeSession);
const form = useWorkoutForm({ userId, onUpdateSet });
// useWorkoutForm needs onUpdateSet. But context updateSet signature might be different?
// context: updateSet(setId, updates). useWorkoutForm expects onUpdateSet(set).
// We can adaptor.
const handleUpdateSetWrapper = (set: WorkoutSet) => {
updateSet(set.id, set);
};
const form = useWorkoutForm({ userId, onUpdateSet: handleUpdateSetWrapper });
const planExec = usePlanExecution({ activeSession, activePlan, exercises });
// Initial Data Load
useEffect(() => {
if (!userId) return;
const loadData = async () => {
const exList = await getExercises(userId);
exList.sort((a, b) => a.name.localeCompare(b.name));
@@ -82,7 +85,7 @@ export const useTracker = ({
// Function to reload Quick Log session
const loadQuickLogSession = async () => {
try {
const response = await api.get('/sessions/quick-log');
const response = await api.get<{ success: boolean; session?: WorkoutSession }>('/sessions/quick-log');
if (response.success && response.session) {
setQuickLogSession(response.session);
}
@@ -125,40 +128,21 @@ export const useTracker = ({
if (plan && plan.description) {
planExec.setShowPlanPrep(plan);
} else {
onSessionStart(plan, parseFloat(userBodyWeight));
startSession(plan, parseFloat(userBodyWeight));
}
};
const confirmPlanStart = () => {
if (planExec.showPlanPrep) {
onSessionStart(planExec.showPlanPrep, parseFloat(userBodyWeight));
startSession(planExec.showPlanPrep, parseFloat(userBodyWeight));
planExec.setShowPlanPrep(null);
}
}
const handleAddSet = async () => {
if (!activeSession || !selectedExercise) return;
const setData = form.prepareSetData(selectedExercise);
try {
const response = await api.post('/sessions/active/log-set', setData);
if (response.success) {
const { newSet, activeExerciseId } = response;
onSetAdded(newSet);
if (activePlan && activeExerciseId) {
const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId);
if (nextStepIndex !== -1) {
planExec.setCurrentStepIndex(nextStepIndex);
}
} else if (activePlan && !activeExerciseId) {
planExec.setCurrentStepIndex(activePlan.steps.length);
}
}
} catch (error) {
console.error("Failed to log set:", error);
}
await addSet(setData);
};
const handleLogSporadicSet = async () => {
@@ -172,7 +156,7 @@ export const useTracker = ({
setTimeout(() => setSporadicSuccess(false), 2000);
loadQuickLogSession();
form.resetForm();
if (onSporadicSetAdded) onSporadicSetAdded();
refreshHistory();
}
} catch (error) {
console.error("Failed to log quick log set:", error);
@@ -255,6 +239,14 @@ export const useTracker = ({
handleCancelEdit,
resetForm,
quickLogSession,
loadQuickLogSession
loadQuickLogSession,
// Pass through context methods for UI to use
onSessionEnd: endSession,
onSessionQuit: quitSession,
onRemoveSet: removeSet,
activeSession // Need this in view
};
};

View File

@@ -0,0 +1,225 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { WorkoutSession, WorkoutPlan, WorkoutSet } from '../types';
import { useAuth } from './AuthContext';
import { useSession } from './SessionContext';
import {
getActiveSession,
updateActiveSession,
deleteActiveSession,
addSetToActiveSession,
deleteSetFromActiveSession,
updateSetInActiveSession,
saveSession
} from '../services/sessions';
import { getPlans } from '../services/plans';
import { generateId } from '../utils/uuid';
import { logWeight } from '../services/weight';
import { useNavigate } from 'react-router-dom';
interface ActiveWorkoutContextType {
activeSession: WorkoutSession | null;
activePlan: WorkoutPlan | null;
isLoading: boolean;
startSession: (plan?: WorkoutPlan, startWeight?: number) => Promise<void>;
endSession: () => Promise<void>;
quitSession: () => Promise<void>;
addSet: (set: Partial<WorkoutSet>) => Promise<void>;
removeSet: (setId: string) => Promise<void>;
updateSet: (setId: string, updates: Partial<WorkoutSet>) => Promise<void>;
updateSessionNote: (note: string) => Promise<void>;
}
const ActiveWorkoutContext = createContext<ActiveWorkoutContextType | undefined>(undefined);
export const ActiveWorkoutProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentUser, updateUser } = useAuth();
const { refreshData: refreshHistory } = useSession(); // Access session history refresh
const navigate = useNavigate();
const [activeSession, setActiveSession] = useState<WorkoutSession | null>(null);
const [activePlan, setActivePlan] = useState<WorkoutPlan | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
// Restore active session on mount
useEffect(() => {
const restoreActive = async () => {
if (currentUser) {
try {
const session = await getActiveSession(currentUser.id);
if (session) {
setActiveSession(session);
if (session.planId) {
// Ideally fetch specific plan, but fetching all for now to find it
// Or session could include plan details? Backend stores planName/ID.
// We need the plan object for steps.
const plans = await getPlans(currentUser.id);
const plan = plans.find(p => p.id === session.planId);
if (plan) setActivePlan(plan);
}
}
} catch (e) {
console.error("Failed to restore active session", e);
}
}
setIsLoading(false);
};
restoreActive();
}, [currentUser]);
const startSession = async (plan?: WorkoutPlan, startWeight?: number) => {
if (!currentUser || activeSession) return;
const newSession: WorkoutSession = {
id: generateId(),
startTime: Date.now(),
type: 'STANDARD',
userBodyWeight: startWeight,
sets: [],
planId: plan?.id,
planName: plan?.name
};
// Optimistic update
setActivePlan(plan || null);
setActiveSession(newSession);
navigate('/');
try {
await saveSession(currentUser.id, newSession);
if (startWeight) {
await logWeight(startWeight);
}
} catch (error) {
console.error("Failed to start session", error);
// Revert state?
setActiveSession(null);
setActivePlan(null);
}
};
const endSession = async () => {
if (activeSession && currentUser) {
const finishedSession = { ...activeSession, endTime: Date.now() };
// Optimistic clear
setActiveSession(null);
setActivePlan(null);
try {
await updateActiveSession(currentUser.id, finishedSession);
await refreshHistory(); // Refresh history in SessionContext
} catch (error) {
console.error("Failed to end session", error);
// Restore state? This is tricky.
setActiveSession(activeSession);
}
}
};
const quitSession = async () => {
if (currentUser) {
// Optimistic clear
setActiveSession(null);
setActivePlan(null);
try {
await deleteActiveSession(currentUser.id);
} catch (error) {
console.error("Failed to quit session", error);
}
}
};
const addSet = async (setData: Partial<WorkoutSet>) => {
if (activeSession && currentUser) {
try {
// Call API first to get ID and calculated fields if any
// The API expects: exerciseId, reps, weight, etc.
const response = await addSetToActiveSession(currentUser.id, setData);
// Response should contain the new set or updated session
// Our backend returns { success: true, newSet: ..., activeExerciseId: ... }
// or similar. I need to type the response properly or cast it.
// Assuming response.newSet needs to be added.
if (response.success && response.newSet) {
setActiveSession(prev => prev ? ({
...prev,
sets: [...prev.sets, response.newSet]
}) : null);
}
} catch (error) {
console.error("Failed to add set", error);
}
}
};
const removeSet = async (setId: string) => {
if (activeSession && currentUser) {
// Optimistic
setActiveSession(prev => prev ? ({
...prev,
sets: prev.sets.filter(s => s.id !== setId)
}) : null);
try {
await deleteSetFromActiveSession(currentUser.id, setId);
} catch (error) {
console.error("Failed to delete set", error);
// Revert?
}
}
};
const updateSet = async (setId: string, updates: Partial<WorkoutSet>) => {
if (activeSession && currentUser) {
// Optimistic
setActiveSession(prev => prev ? ({
...prev,
sets: prev.sets.map(s => s.id === setId ? { ...s, ...updates } : s)
}) : null);
try {
await updateSetInActiveSession(currentUser.id, setId, updates);
} catch (error) {
console.error("Failed to update set", error);
}
}
};
const updateSessionNote = async (note: string) => {
if (activeSession && currentUser) {
setActiveSession(prev => prev ? ({ ...prev, note }) : null);
try {
await updateActiveSession(currentUser.id, { ...activeSession, note });
} catch (error) {
console.error("Failed to update note", error);
}
}
}
return (
<ActiveWorkoutContext.Provider value={{
activeSession,
activePlan,
isLoading,
startSession,
endSession,
quitSession,
addSet,
removeSet,
updateSet,
updateSessionNote
}}>
{children}
</ActiveWorkoutContext.Provider>
);
};
export const useActiveWorkout = () => {
const context = useContext(ActiveWorkoutContext);
if (context === undefined) {
throw new Error('useActiveWorkout must be used within an ActiveWorkoutProvider');
}
return context;
};

View File

@@ -1,208 +0,0 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { WorkoutSession, WorkoutPlan, WorkoutSet } from '../types';
import { useAuth } from './AuthContext';
import {
getSessions,
getPlans,
getActiveSession,
saveSession,
deleteSession,
updateActiveSession,
deleteActiveSession,
deleteSetFromActiveSession,
updateSetInActiveSession
} from '../services/storage';
import { getCurrentUserProfile, getMe } from '../services/auth';
import { generateId } from '../utils/uuid';
import { logWeight } from '../services/weight';
import { useNavigate } from 'react-router-dom';
interface DataContextType {
sessions: WorkoutSession[];
plans: WorkoutPlan[];
activeSession: WorkoutSession | null;
activePlan: WorkoutPlan | null;
startSession: (plan?: WorkoutPlan, startWeight?: number) => Promise<void>;
endSession: () => Promise<void>;
quitSession: () => Promise<void>;
addSet: (set: WorkoutSet) => void;
removeSet: (setId: string) => Promise<void>;
updateSet: (updatedSet: WorkoutSet) => Promise<void>;
updateSession: (updatedSession: WorkoutSession) => void;
deleteSessionById: (sessionId: string) => void;
refreshData: () => Promise<void>;
}
const DataContext = createContext<DataContextType | undefined>(undefined);
export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentUser, updateUser } = useAuth();
const navigate = useNavigate();
const [sessions, setSessions] = useState<WorkoutSession[]>([]);
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [activeSession, setActiveSession] = useState<WorkoutSession | null>(null);
const [activePlan, setActivePlan] = useState<WorkoutPlan | null>(null);
const refreshData = async () => {
if (currentUser) {
const s = await getSessions(currentUser.id);
setSessions(s);
const p = await getPlans(currentUser.id);
setPlans(p);
} else {
setSessions([]);
setPlans([]);
}
};
useEffect(() => {
refreshData();
}, [currentUser]);
// Restore active session
useEffect(() => {
const restoreActive = async () => {
if (currentUser) {
const session = await getActiveSession(currentUser.id);
if (session) {
setActiveSession(session);
if (session.planId) {
// Ensure plans are loaded or fetch specifically
const currentPlans = plans.length > 0 ? plans : await getPlans(currentUser.id);
const plan = currentPlans.find(p => p.id === session.planId);
if (plan) setActivePlan(plan);
}
}
}
};
restoreActive();
}, [currentUser]); // Dependency logic might need tuning, but this matches App.tsx roughly
const startSession = async (plan?: WorkoutPlan, startWeight?: number) => {
if (!currentUser || activeSession) return;
const profile = getCurrentUserProfile(currentUser.id);
const currentWeight = startWeight || profile?.weight || 70;
const newSession: WorkoutSession = {
id: generateId(),
startTime: Date.now(),
type: 'STANDARD',
userBodyWeight: currentWeight,
sets: [],
planId: plan?.id,
planName: plan?.name
};
setActivePlan(plan || null);
setActiveSession(newSession);
navigate('/');
await saveSession(currentUser.id, newSession);
if (startWeight) {
await logWeight(startWeight);
}
};
const endSession = async () => {
if (activeSession && currentUser) {
const finishedSession = { ...activeSession, endTime: Date.now() };
await updateActiveSession(currentUser.id, finishedSession);
setSessions(prev => [finishedSession, ...prev]);
setActiveSession(null);
setActivePlan(null);
const res = await getMe();
if (res.success && res.user) {
updateUser(res.user);
}
}
};
const quitSession = async () => {
if (currentUser) {
await deleteActiveSession(currentUser.id);
setActiveSession(null);
setActivePlan(null);
}
};
const addSet = (set: WorkoutSet) => {
if (activeSession) {
const updatedSession = { ...activeSession, sets: [...activeSession.sets, set] };
setActiveSession(updatedSession);
// Context update is optimistic, actual save usually happens in hooks or components?
// In App.tsx handleAddSet only updated local state.
// Wait, useTracker usually handles saving sets via API?
// In App.tsx: handleAddSet just set state.
// useTracker.ts calls onSetAdded, but ALSO calls api to save it?
// Let's look at useTracker.ts.
// handleLogSet in useTracker calls API then onSetAdded.
// So this state update is mainly for UI sync in App.
}
};
const removeSet = async (setId: string) => {
if (activeSession && currentUser) {
await deleteSetFromActiveSession(currentUser.id, setId);
const updatedSession = {
...activeSession,
sets: activeSession.sets.filter(s => s.id !== setId)
};
setActiveSession(updatedSession);
}
};
const updateSet = async (updatedSet: WorkoutSet) => {
if (activeSession && currentUser) {
const response = await updateSetInActiveSession(currentUser.id, updatedSet.id, updatedSet);
const updatedSession = {
...activeSession,
sets: activeSession.sets.map(s => s.id === updatedSet.id ? response : s)
};
setActiveSession(updatedSession);
}
};
const updateSession = (updatedSession: WorkoutSession) => {
if (!currentUser) return;
saveSession(currentUser.id, updatedSession);
setSessions(prev => prev.map(s => s.id === updatedSession.id ? updatedSession : s));
};
const deleteSessionById = (sessionId: string) => {
if (!currentUser) return;
deleteSession(currentUser.id, sessionId);
setSessions(prev => prev.filter(s => s.id !== sessionId));
};
return (
<DataContext.Provider value={{
sessions,
plans,
activeSession,
activePlan,
startSession,
endSession,
quitSession,
addSet,
removeSet,
updateSet,
updateSession,
deleteSessionById,
refreshData
}}>
{children}
</DataContext.Provider>
);
};
export const useData = () => {
const context = useContext(DataContext);
if (context === undefined) {
throw new Error('useData must be used within a DataProvider');
}
return context;
};

View File

@@ -0,0 +1,133 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { WorkoutSession, WorkoutPlan } from '../types';
import { useAuth } from './AuthContext';
import { getSessions, saveSession, deleteSession as apiDeleteSession } from '../services/sessions';
import { getPlans, savePlan as apiSavePlan, deletePlan as apiDeletePlan } from '../services/plans';
interface SessionContextType {
sessions: WorkoutSession[];
plans: WorkoutPlan[];
isLoading: boolean;
refreshData: () => Promise<void>;
updateSession: (session: WorkoutSession) => Promise<void>;
deleteSession: (id: string) => Promise<void>;
savePlan: (plan: WorkoutPlan) => Promise<void>;
deletePlan: (id: string) => Promise<void>;
}
const SessionContext = createContext<SessionContextType | undefined>(undefined);
export const SessionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentUser } = useAuth();
const [sessions, setSessions] = useState<WorkoutSession[]>([]);
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const refreshData = useCallback(async () => {
if (!currentUser) {
setSessions([]);
setPlans([]);
setIsLoading(false);
return;
}
try {
setIsLoading(true);
const [fetchedSessions, fetchedPlans] = await Promise.all([
getSessions(currentUser.id),
getPlans(currentUser.id)
]);
setSessions(fetchedSessions);
setPlans(fetchedPlans);
} catch (error) {
console.error('Failed to fetch data', error);
} finally {
setIsLoading(false);
}
}, [currentUser]);
useEffect(() => {
refreshData();
}, [refreshData]);
const updateSession = async (session: WorkoutSession) => {
if (!currentUser) return;
try {
await saveSession(currentUser.id, session);
setSessions(prev => {
const existing = prev.find(s => s.id === session.id);
if (existing) {
return prev.map(s => s.id === session.id ? session : s);
} else {
return [session, ...prev];
}
});
} catch (error) {
console.error('Failed to update session', error);
throw error;
}
};
const deleteSession = async (id: string) => {
if (!currentUser) return;
try {
await apiDeleteSession(currentUser.id, id);
setSessions(prev => prev.filter(s => s.id !== id));
} catch (error) {
console.error('Failed to delete session', error);
throw error;
}
};
const savePlan = async (plan: WorkoutPlan) => {
if (!currentUser) return;
try {
await apiSavePlan(currentUser.id, plan);
setPlans(prev => {
const existing = prev.find(p => p.id === plan.id);
if (existing) {
return prev.map(p => p.id === plan.id ? plan : p);
} else {
return [...prev, plan];
}
});
} catch (error) {
console.error('Failed to save plan', error);
throw error;
}
};
const deletePlan = async (id: string) => {
if (!currentUser) return;
try {
await apiDeletePlan(currentUser.id, id);
setPlans(prev => prev.filter(p => p.id !== id));
} catch (error) {
console.error('Failed to delete plan', error);
throw error;
}
};
return (
<SessionContext.Provider value={{
sessions,
plans,
isLoading,
refreshData,
updateSession,
deleteSession,
savePlan,
deletePlan
}}>
{children}
</SessionContext.Provider>
);
};
export const useSession = () => {
const context = useContext(SessionContext);
if (context === undefined) {
throw new Error('useSession must be used within a SessionProvider');
}
return context;
};

View File

@@ -2,7 +2,8 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { DataProvider } from './context/DataContext';
import { SessionProvider } from './context/SessionContext';
import { ActiveWorkoutProvider } from './context/ActiveWorkoutContext';
import App from './App';
import './index.css';
@@ -16,9 +17,11 @@ root.render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<DataProvider>
<App />
</DataProvider>
<SessionProvider>
<ActiveWorkoutProvider>
<App />
</ActiveWorkoutProvider>
</SessionProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>

View File

@@ -80,6 +80,11 @@ export const deleteSession = async (userId: string, id: string): Promise<void> =
await api.delete(`/sessions/${id}`);
};
export const addSetToActiveSession = async (userId: string, setData: any): Promise<any> => {
return await api.post('/sessions/active/log-set', setData);
};
export const deleteAllUserData = (userId: string) => {
// Not implemented in frontend
};