Code maintainability fixes
This commit is contained in:
302
src/App.tsx
302
src/App.tsx
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Tracker from './components/Tracker/index';
|
||||
import History from './components/History';
|
||||
@@ -8,250 +8,112 @@ import AICoach from './components/AICoach';
|
||||
import Plans from './components/Plans';
|
||||
import Login from './components/Login';
|
||||
import Profile from './components/Profile';
|
||||
import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types';
|
||||
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage';
|
||||
import { getCurrentUserProfile, getMe } from './services/auth';
|
||||
import { Language, User } from './types'; // Removed unused imports
|
||||
import { getSystemLanguage } from './services/i18n';
|
||||
import { logWeight } from './services/weight';
|
||||
import { generateId } from './utils/uuid';
|
||||
import { useAuth } from './context/AuthContext';
|
||||
import { useData } from './context/DataContext';
|
||||
|
||||
function App() {
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [currentTab, setCurrentTab] = useState<TabView>('TRACK');
|
||||
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 [sessions, setSessions] = useState<WorkoutSession[]>([]);
|
||||
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
|
||||
const [activeSession, setActiveSession] = useState<WorkoutSession | null>(null);
|
||||
const [activePlan, setActivePlan] = useState<WorkoutPlan | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial language
|
||||
setLanguage(getSystemLanguage());
|
||||
|
||||
// Restore session
|
||||
const restoreSession = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const res = await getMe();
|
||||
if (res.success && res.user) {
|
||||
setCurrentUser(res.user);
|
||||
|
||||
// Restore active workout session from database
|
||||
const activeSession = await getActiveSession(res.user.id);
|
||||
if (activeSession) {
|
||||
setActiveSession(activeSession);
|
||||
// Restore plan if session has planId
|
||||
if (activeSession.planId) {
|
||||
const plans = await getPlans(res.user.id);
|
||||
const plan = plans.find(p => p.id === activeSession.planId);
|
||||
if (plan) {
|
||||
setActivePlan(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
};
|
||||
restoreSession();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSessions = async () => {
|
||||
if (currentUser) {
|
||||
const s = await getSessions(currentUser.id);
|
||||
setSessions(s);
|
||||
// Load plans
|
||||
const p = await getPlans(currentUser.id);
|
||||
setPlans(p);
|
||||
|
||||
} else {
|
||||
setSessions([]);
|
||||
setPlans([]);
|
||||
|
||||
}
|
||||
};
|
||||
loadSessions();
|
||||
}, [currentUser]);
|
||||
|
||||
const handleLogin = (user: User) => {
|
||||
setCurrentUser(user);
|
||||
setCurrentTab('TRACK');
|
||||
updateUser(user);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setCurrentUser(null);
|
||||
setActiveSession(null);
|
||||
setActivePlan(null);
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleLanguageChange = (lang: Language) => {
|
||||
setLanguage(lang);
|
||||
};
|
||||
|
||||
const handleUserUpdate = (updatedUser: User) => {
|
||||
setCurrentUser(updatedUser);
|
||||
};
|
||||
|
||||
const handleStartSession = async (plan?: WorkoutPlan, startWeight?: number) => {
|
||||
if (!currentUser) return;
|
||||
if (activeSession) return;
|
||||
|
||||
// Get latest weight from profile or default
|
||||
const profile = getCurrentUserProfile(currentUser.id);
|
||||
// Use provided startWeight, or profile weight, or default 70
|
||||
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);
|
||||
setCurrentTab('TRACK');
|
||||
|
||||
// Save to database immediately
|
||||
await saveSession(currentUser.id, newSession);
|
||||
|
||||
// If startWeight was provided (meaning user explicitly entered it), log it to weight history
|
||||
if (startWeight) {
|
||||
await logWeight(startWeight);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndSession = async () => {
|
||||
if (activeSession && currentUser) {
|
||||
const finishedSession = { ...activeSession, endTime: Date.now() };
|
||||
await updateActiveSession(currentUser.id, finishedSession);
|
||||
setSessions(prev => [finishedSession, ...prev]);
|
||||
setActiveSession(null);
|
||||
setActivePlan(null);
|
||||
|
||||
// Refetch user to get updated weight
|
||||
const res = await getMe();
|
||||
if (res.success && res.user) {
|
||||
setCurrentUser(res.user);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSet = (set: WorkoutSet) => {
|
||||
if (activeSession && currentUser) {
|
||||
const updatedSession = {
|
||||
...activeSession,
|
||||
sets: [...activeSession.sets, set]
|
||||
};
|
||||
setActiveSession(updatedSession);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSetFromActive = 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 handleUpdateSetInActive = 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 handleQuitSession = async () => {
|
||||
if (currentUser) {
|
||||
await deleteActiveSession(currentUser.id);
|
||||
setActiveSession(null);
|
||||
setActivePlan(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSession = (updatedSession: WorkoutSession) => {
|
||||
if (!currentUser) return;
|
||||
saveSession(currentUser.id, updatedSession);
|
||||
setSessions(prev => prev.map(s => s.id === updatedSession.id ? updatedSession : s));
|
||||
};
|
||||
|
||||
const handleDeleteSession = (sessionId: string) => {
|
||||
if (!currentUser) return;
|
||||
deleteSession(currentUser.id, sessionId);
|
||||
setSessions(prev => prev.filter(s => s.id !== sessionId));
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (!currentUser) {
|
||||
return <Login onLogin={handleLogin} language={language} onLanguageChange={handleLanguageChange} />;
|
||||
if (!currentUser && location.pathname !== '/login') {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-surface text-on-surface font-sans flex flex-col md:flex-row overflow-hidden">
|
||||
|
||||
{/* Desktop Navigation Rail (Left) */}
|
||||
<Navbar currentTab={currentTab} onTabChange={setCurrentTab} lang={language} />
|
||||
{currentUser && (
|
||||
<Navbar lang={language} />
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 h-full relative w-full max-w-5xl mx-auto md:px-4">
|
||||
<div className="h-full w-full pb-20 md:pb-0 bg-surface">
|
||||
{currentTab === 'TRACK' && (
|
||||
<Tracker
|
||||
userId={currentUser.id}
|
||||
userWeight={currentUser.profile?.weight}
|
||||
activeSession={activeSession}
|
||||
activePlan={activePlan}
|
||||
onSessionStart={handleStartSession}
|
||||
onSessionEnd={handleEndSession}
|
||||
onSessionQuit={handleQuitSession}
|
||||
onSetAdded={handleAddSet}
|
||||
onRemoveSet={handleRemoveSetFromActive}
|
||||
onUpdateSet={handleUpdateSetInActive}
|
||||
lang={language}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'PLANS' && (
|
||||
<Plans userId={currentUser.id} onStartPlan={handleStartSession} lang={language} />
|
||||
)}
|
||||
{currentTab === 'HISTORY' && (
|
||||
<History
|
||||
sessions={sessions}
|
||||
onUpdateSession={handleUpdateSession}
|
||||
onDeleteSession={handleDeleteSession}
|
||||
lang={language}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'STATS' && <Stats sessions={sessions} lang={language} />}
|
||||
{currentTab === 'AI_COACH' && <AICoach history={sessions} userProfile={currentUser.profile} plans={plans} lang={language} />}
|
||||
{currentTab === 'PROFILE' && (
|
||||
<Profile
|
||||
user={currentUser}
|
||||
onLogout={handleLogout}
|
||||
lang={language}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onUserUpdate={handleUserUpdate}
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route path="/login" element={
|
||||
!currentUser ? (
|
||||
<Login onLogin={handleLogin} language={language} onLanguageChange={setLanguage} />
|
||||
) : (
|
||||
<Navigate to="/" />
|
||||
)
|
||||
} />
|
||||
<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}
|
||||
/>
|
||||
} />
|
||||
<Route path="/plans" element={
|
||||
<Plans userId={currentUser?.id || ''} onStartPlan={startSession} lang={language} />
|
||||
} />
|
||||
<Route path="/history" element={
|
||||
<History
|
||||
sessions={sessions}
|
||||
onUpdateSession={updateSession}
|
||||
onDeleteSession={deleteSessionById}
|
||||
lang={language}
|
||||
/>
|
||||
} />
|
||||
<Route path="/stats" element={
|
||||
<Stats sessions={sessions} lang={language} />
|
||||
} />
|
||||
<Route path="/coach" element={
|
||||
<AICoach history={sessions} userProfile={currentUser?.profile} plans={plans} lang={language} />
|
||||
} />
|
||||
<Route path="/profile" element={
|
||||
<Profile
|
||||
user={currentUser}
|
||||
onLogout={handleLogout}
|
||||
lang={language}
|
||||
onLanguageChange={setLanguage}
|
||||
onUserUpdate={updateUser}
|
||||
/>
|
||||
} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile Navigation (rendered inside Navbar component, fixed to bottom) */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,48 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Dumbbell, History as HistoryIcon, BarChart2, MessageSquare, ClipboardList, User } from 'lucide-react';
|
||||
import { TabView, Language } from '../types';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { t } from '../services/i18n';
|
||||
import { Language } from '../types';
|
||||
|
||||
interface NavbarProps {
|
||||
currentTab: TabView;
|
||||
onTabChange: (tab: TabView) => void;
|
||||
lang: Language;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ currentTab, onTabChange, lang }) => {
|
||||
const Navbar: React.FC<NavbarProps> = ({ lang }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const navItems = [
|
||||
{ id: 'TRACK' as TabView, icon: Dumbbell, label: t('tab_tracker', lang) },
|
||||
{ id: 'PLANS' as TabView, icon: ClipboardList, label: t('tab_plans', lang) },
|
||||
{ id: 'HISTORY' as TabView, icon: HistoryIcon, label: t('tab_history', lang) },
|
||||
{ id: 'STATS' as TabView, icon: BarChart2, label: t('tab_stats', lang) },
|
||||
{ id: 'AI_COACH' as TabView, icon: MessageSquare, label: t('tab_ai', lang) },
|
||||
{ id: 'PROFILE' as TabView, icon: User, label: t('tab_profile', lang) },
|
||||
{ path: '/', icon: Dumbbell, label: t('tab_tracker', lang) },
|
||||
{ path: '/plans', icon: ClipboardList, label: t('tab_plans', lang) },
|
||||
{ path: '/history', icon: HistoryIcon, label: t('tab_history', lang) },
|
||||
{ path: '/stats', icon: BarChart2, label: t('tab_stats', lang) },
|
||||
{ path: '/coach', icon: MessageSquare, label: t('tab_ai', lang) },
|
||||
{ path: '/profile', icon: User, label: t('tab_profile', lang) },
|
||||
];
|
||||
|
||||
const currentPath = location.pathname;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* MOBILE: Bottom Navigation Bar (MD3) */}
|
||||
<div className="md:hidden fixed bottom-0 left-0 w-full bg-surface-container shadow-elevation-2 border-t border-white/5 pb-safe z-50 h-20">
|
||||
<div className="flex justify-evenly items-center h-full px-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentTab === item.id;
|
||||
const isActive = currentPath === item.path || (item.path !== '/' && currentPath.startsWith(item.path));
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onTabChange(item.id)}
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className="flex flex-col items-center justify-center w-full h-full gap-1 group min-w-0"
|
||||
>
|
||||
<div className={`px-4 py-1 rounded-full transition-all duration-200 ${
|
||||
isActive ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant group-hover:bg-surface-container-high'
|
||||
}`}>
|
||||
<item.icon size={22} strokeWidth={isActive ? 2.5 : 2} />
|
||||
<div className={`px-4 py-1 rounded-full transition-all duration-200 ${isActive ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant group-hover:bg-surface-container-high'
|
||||
}`}>
|
||||
<item.icon size={22} strokeWidth={isActive ? 2.5 : 2} />
|
||||
</div>
|
||||
<span className={`text-[10px] font-medium transition-colors truncate w-full text-center ${
|
||||
isActive ? 'text-on-surface' : 'text-on-surface-variant'
|
||||
}`}>
|
||||
{item.label}
|
||||
<span className={`text-[10px] font-medium transition-colors truncate w-full text-center ${isActive ? 'text-on-surface' : 'text-on-surface-variant'
|
||||
}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@@ -51,29 +52,27 @@ const Navbar: React.FC<NavbarProps> = ({ currentTab, onTabChange, lang }) => {
|
||||
|
||||
{/* DESKTOP: Navigation Rail (MD3) */}
|
||||
<div className="hidden md:flex flex-col w-20 h-full bg-surface-container border-r border-outline-variant items-center py-8 gap-8 z-50">
|
||||
<div className="flex flex-col gap-6 w-full px-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onTabChange(item.id)}
|
||||
className="flex flex-col items-center gap-1 group w-full"
|
||||
>
|
||||
<div className={`w-14 h-8 rounded-full flex items-center justify-center transition-colors duration-200 ${
|
||||
isActive ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant group-hover:bg-surface-container-high'
|
||||
}`}>
|
||||
<item.icon size={24} />
|
||||
</div>
|
||||
<span className={`text-[11px] font-medium text-center ${
|
||||
isActive ? 'text-on-surface' : 'text-on-surface-variant'
|
||||
}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 w-full px-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPath === item.path || (item.path !== '/' && currentPath.startsWith(item.path));
|
||||
return (
|
||||
<button
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className="flex flex-col items-center gap-1 group w-full"
|
||||
>
|
||||
<div className={`w-14 h-8 rounded-full flex items-center justify-center transition-colors duration-200 ${isActive ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant group-hover:bg-surface-container-high'
|
||||
}`}>
|
||||
<item.icon size={24} />
|
||||
</div>
|
||||
<span className={`text-[11px] font-medium text-center ${isActive ? 'text-on-surface' : 'text-on-surface-variant'
|
||||
}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, WorkoutPlan, Language } from '../../types';
|
||||
import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../../services/storage';
|
||||
import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, 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';
|
||||
|
||||
interface UseTrackerProps {
|
||||
userId: string;
|
||||
@@ -25,9 +27,7 @@ export const useTracker = ({
|
||||
activePlan,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
onSessionQuit,
|
||||
onSetAdded,
|
||||
onRemoveSet,
|
||||
onUpdateSet,
|
||||
onSporadicSetAdded
|
||||
}: UseTrackerProps) => {
|
||||
@@ -38,49 +38,28 @@ export const useTracker = ({
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
|
||||
// Timer State
|
||||
const [elapsedTime, setElapsedTime] = useState<string>('00:00:00');
|
||||
|
||||
// Form State
|
||||
const [weight, setWeight] = useState<string>('');
|
||||
const [reps, setReps] = useState<string>('');
|
||||
const [duration, setDuration] = useState<string>('');
|
||||
const [distance, setDistance] = useState<string>('');
|
||||
const [height, setHeight] = useState<string>('');
|
||||
const [bwPercentage, setBwPercentage] = useState<string>('100');
|
||||
|
||||
// User Weight State
|
||||
const [userBodyWeight, setUserBodyWeight] = useState<string>(userWeight ? userWeight.toString() : '70');
|
||||
|
||||
// Create Exercise State
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
// Plan Execution State
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [showPlanPrep, setShowPlanPrep] = useState<WorkoutPlan | null>(null);
|
||||
const [showPlanList, setShowPlanList] = useState(false);
|
||||
|
||||
// Confirmation State
|
||||
const [showFinishConfirm, setShowFinishConfirm] = useState(false);
|
||||
const [showQuitConfirm, setShowQuitConfirm] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
// Edit Set State
|
||||
const [editingSetId, setEditingSetId] = useState<string | null>(null);
|
||||
const [editWeight, setEditWeight] = useState<string>('');
|
||||
const [editReps, setEditReps] = useState<string>('');
|
||||
const [editDuration, setEditDuration] = useState<string>('');
|
||||
const [editDistance, setEditDistance] = useState<string>('');
|
||||
const [editHeight, setEditHeight] = useState<string>('');
|
||||
|
||||
// Quick Log State
|
||||
const [quickLogSession, setQuickLogSession] = useState<WorkoutSession | null>(null);
|
||||
const [isSporadicMode, setIsSporadicMode] = useState(false);
|
||||
const [sporadicSuccess, setSporadicSuccess] = useState(false);
|
||||
|
||||
// Unilateral Exercise State
|
||||
const [unilateralSide, setUnilateralSide] = useState<'LEFT' | 'RIGHT'>('LEFT');
|
||||
// Hooks
|
||||
const elapsedTime = useSessionTimer(activeSession);
|
||||
const form = useWorkoutForm({ userId, onUpdateSet });
|
||||
const planExec = usePlanExecution({ activeSession, activePlan, exercises });
|
||||
|
||||
// Initial Data Load
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const exList = await getExercises(userId);
|
||||
@@ -95,15 +74,7 @@ export const useTracker = ({
|
||||
setUserBodyWeight(userWeight.toString());
|
||||
}
|
||||
|
||||
// Load Quick Log Session
|
||||
try {
|
||||
const response = await api.get('/sessions/quick-log');
|
||||
if (response.success && response.session) {
|
||||
setQuickLogSession(response.session);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load quick log session:", error);
|
||||
}
|
||||
loadQuickLogSession();
|
||||
};
|
||||
loadData();
|
||||
}, [activeSession, userId, userWeight, activePlan]);
|
||||
@@ -120,107 +91,30 @@ export const useTracker = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Timer Logic
|
||||
// Auto-select exercise from plan step
|
||||
useEffect(() => {
|
||||
let interval: number;
|
||||
if (activeSession) {
|
||||
const updateTimer = () => {
|
||||
const diff = Math.floor((Date.now() - activeSession.startTime) / 1000);
|
||||
const h = Math.floor(diff / 3600);
|
||||
const m = Math.floor((diff % 3600) / 60);
|
||||
const s = diff % 60;
|
||||
setElapsedTime(`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`);
|
||||
};
|
||||
|
||||
updateTimer();
|
||||
interval = window.setInterval(updateTimer, 1000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [activeSession]);
|
||||
|
||||
// Recalculate current step when sets change
|
||||
useEffect(() => {
|
||||
if (activeSession && activePlan) {
|
||||
const performedCounts = new Map<string, number>();
|
||||
for (const set of activeSession.sets) {
|
||||
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
|
||||
}
|
||||
|
||||
let nextStepIndex = activePlan.steps.length; // Default to finished
|
||||
const plannedCounts = new Map<string, number>();
|
||||
for (let i = 0; i < activePlan.steps.length; i++) {
|
||||
const step = activePlan.steps[i];
|
||||
const exerciseId = step.exerciseId;
|
||||
plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1);
|
||||
const performedCount = performedCounts.get(exerciseId) || 0;
|
||||
|
||||
if (performedCount < plannedCounts.get(exerciseId)!) {
|
||||
nextStepIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setCurrentStepIndex(nextStepIndex);
|
||||
}
|
||||
}, [activeSession, activePlan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) {
|
||||
if (currentStepIndex < activePlan.steps.length) {
|
||||
const step = activePlan.steps[currentStepIndex];
|
||||
if (step) {
|
||||
const exDef = exercises.find(e => e.id === step.exerciseId);
|
||||
if (exDef) {
|
||||
setSelectedExercise(exDef);
|
||||
}
|
||||
}
|
||||
const step = planExec.getCurrentStep();
|
||||
if (step) {
|
||||
const exDef = exercises.find(e => e.id === step.exerciseId);
|
||||
if (exDef) {
|
||||
setSelectedExercise(exDef);
|
||||
}
|
||||
}
|
||||
}, [currentStepIndex, activePlan, exercises]);
|
||||
}, [planExec.currentStepIndex, activePlan, exercises]);
|
||||
|
||||
// Update form when exercise changes
|
||||
useEffect(() => {
|
||||
const updateSelection = async () => {
|
||||
if (selectedExercise) {
|
||||
setBwPercentage(selectedExercise.bodyWeightPercentage ? selectedExercise.bodyWeightPercentage.toString() : '100');
|
||||
setSearchQuery(selectedExercise.name);
|
||||
const set = await getLastSetForExercise(userId, selectedExercise.id);
|
||||
setLastSet(set);
|
||||
|
||||
if (set) {
|
||||
setWeight(set.weight?.toString() || '');
|
||||
setReps(set.reps?.toString() || '');
|
||||
setDuration(set.durationSeconds?.toString() || '');
|
||||
setDistance(set.distanceMeters?.toString() || '');
|
||||
setHeight(set.height?.toString() || '');
|
||||
} else {
|
||||
setWeight(''); setReps(''); setDuration(''); setDistance(''); setHeight('');
|
||||
}
|
||||
|
||||
// Clear fields not relevant to the selected exercise type
|
||||
if (selectedExercise.type !== ExerciseType.STRENGTH && selectedExercise.type !== ExerciseType.BODYWEIGHT) {
|
||||
setWeight('');
|
||||
}
|
||||
if (selectedExercise.type !== ExerciseType.STRENGTH && selectedExercise.type !== ExerciseType.BODYWEIGHT && selectedExercise.type !== ExerciseType.PLYOMETRIC) {
|
||||
setReps('');
|
||||
}
|
||||
if (selectedExercise.type !== ExerciseType.CARDIO && selectedExercise.type !== ExerciseType.STATIC) {
|
||||
setDuration('');
|
||||
}
|
||||
if (selectedExercise.type !== ExerciseType.CARDIO && selectedExercise.type !== ExerciseType.LONG_JUMP) {
|
||||
setDistance('');
|
||||
}
|
||||
if (selectedExercise.type !== ExerciseType.HIGH_JUMP) {
|
||||
setHeight('');
|
||||
}
|
||||
await form.updateFormFromLastSet(selectedExercise.id, selectedExercise.type, selectedExercise.bodyWeightPercentage);
|
||||
} else {
|
||||
setSearchQuery(''); // Clear search query if no exercise is selected
|
||||
setSearchQuery('');
|
||||
}
|
||||
};
|
||||
updateSelection();
|
||||
}, [selectedExercise, userId]);
|
||||
|
||||
|
||||
const filteredExercises = searchQuery === ''
|
||||
? exercises
|
||||
: exercises.filter(ex =>
|
||||
@@ -229,58 +123,23 @@ export const useTracker = ({
|
||||
|
||||
const handleStart = (plan?: WorkoutPlan) => {
|
||||
if (plan && plan.description) {
|
||||
setShowPlanPrep(plan);
|
||||
planExec.setShowPlanPrep(plan);
|
||||
} else {
|
||||
onSessionStart(plan, parseFloat(userBodyWeight));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPlanStart = () => {
|
||||
if (showPlanPrep) {
|
||||
onSessionStart(showPlanPrep, parseFloat(userBodyWeight));
|
||||
setShowPlanPrep(null);
|
||||
if (planExec.showPlanPrep) {
|
||||
onSessionStart(planExec.showPlanPrep, parseFloat(userBodyWeight));
|
||||
planExec.setShowPlanPrep(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddSet = async () => {
|
||||
if (!activeSession || !selectedExercise) return;
|
||||
|
||||
const setData: Partial<WorkoutSet> = {
|
||||
exerciseId: selectedExercise.id,
|
||||
};
|
||||
|
||||
if (selectedExercise.isUnilateral) {
|
||||
setData.side = unilateralSide;
|
||||
}
|
||||
|
||||
switch (selectedExercise.type) {
|
||||
case ExerciseType.STRENGTH:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
case ExerciseType.BODYWEIGHT:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.CARDIO:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.STATIC:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.HIGH_JUMP:
|
||||
if (height) setData.height = parseFloat(height);
|
||||
break;
|
||||
case ExerciseType.LONG_JUMP:
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.PLYOMETRIC:
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
}
|
||||
const setData = form.prepareSetData(selectedExercise);
|
||||
|
||||
try {
|
||||
const response = await api.post('/sessions/active/log-set', setData);
|
||||
@@ -291,11 +150,10 @@ export const useTracker = ({
|
||||
if (activePlan && activeExerciseId) {
|
||||
const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId);
|
||||
if (nextStepIndex !== -1) {
|
||||
setCurrentStepIndex(nextStepIndex);
|
||||
planExec.setCurrentStepIndex(nextStepIndex);
|
||||
}
|
||||
} else if (activePlan && !activeExerciseId) {
|
||||
// Plan is finished
|
||||
setCurrentStepIndex(activePlan.steps.length);
|
||||
planExec.setCurrentStepIndex(activePlan.steps.length);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -305,62 +163,15 @@ export const useTracker = ({
|
||||
|
||||
const handleLogSporadicSet = async () => {
|
||||
if (!selectedExercise) return;
|
||||
|
||||
const setData: any = {
|
||||
exerciseId: selectedExercise.id,
|
||||
};
|
||||
|
||||
if (selectedExercise.isUnilateral) {
|
||||
setData.side = unilateralSide;
|
||||
}
|
||||
|
||||
switch (selectedExercise.type) {
|
||||
case ExerciseType.STRENGTH:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
case ExerciseType.BODYWEIGHT:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.CARDIO:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.STATIC:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.HIGH_JUMP:
|
||||
if (height) setData.height = parseFloat(height);
|
||||
break;
|
||||
case ExerciseType.LONG_JUMP:
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.PLYOMETRIC:
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
}
|
||||
const setData = form.prepareSetData(selectedExercise);
|
||||
|
||||
try {
|
||||
const response = await api.post('/sessions/quick-log/set', setData);
|
||||
if (response.success) {
|
||||
setSporadicSuccess(true);
|
||||
setTimeout(() => setSporadicSuccess(false), 2000);
|
||||
|
||||
// Refresh quick log session
|
||||
const sessionRes = await api.get('/sessions/quick-log');
|
||||
if (sessionRes.success && sessionRes.session) {
|
||||
setQuickLogSession(sessionRes.session);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
setWeight('');
|
||||
setReps('');
|
||||
setDuration('');
|
||||
setDistance('');
|
||||
setHeight('');
|
||||
loadQuickLogSession();
|
||||
form.resetForm();
|
||||
if (onSporadicSetAdded) onSporadicSetAdded();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -376,44 +187,14 @@ export const useTracker = ({
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
const handleEditSet = (set: WorkoutSet) => {
|
||||
setEditingSetId(set.id);
|
||||
setEditWeight(set.weight?.toString() || '');
|
||||
setEditReps(set.reps?.toString() || '');
|
||||
setEditDuration(set.durationSeconds?.toString() || '');
|
||||
setEditDistance(set.distanceMeters?.toString() || '');
|
||||
setEditHeight(set.height?.toString() || '');
|
||||
};
|
||||
|
||||
const handleSaveEdit = (set: WorkoutSet) => {
|
||||
const updatedSet: WorkoutSet = {
|
||||
...set,
|
||||
...(editWeight && { weight: parseFloat(editWeight) }),
|
||||
...(editReps && { reps: parseInt(editReps) }),
|
||||
...(editDuration && { durationSeconds: parseInt(editDuration) }),
|
||||
...(editDistance && { distanceMeters: parseFloat(editDistance) }),
|
||||
...(editHeight && { height: parseFloat(editHeight) })
|
||||
};
|
||||
onUpdateSet(updatedSet);
|
||||
setEditingSetId(null);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingSetId(null);
|
||||
};
|
||||
|
||||
const jumpToStep = (index: number) => {
|
||||
if (!activePlan) return;
|
||||
setCurrentStepIndex(index);
|
||||
setShowPlanList(false);
|
||||
};
|
||||
// Forwarding form handlers from hook
|
||||
const handleEditSet = form.startEditing;
|
||||
const handleSaveEdit = form.saveEdit;
|
||||
const handleCancelEdit = form.cancelEdit;
|
||||
|
||||
// Reset override
|
||||
const resetForm = () => {
|
||||
setWeight('');
|
||||
setReps('');
|
||||
setDuration('');
|
||||
setDistance('');
|
||||
setHeight('');
|
||||
form.resetForm();
|
||||
setSelectedExercise(null);
|
||||
setSearchQuery('');
|
||||
setSporadicSuccess(false);
|
||||
@@ -431,46 +212,37 @@ export const useTracker = ({
|
||||
showSuggestions,
|
||||
setShowSuggestions,
|
||||
elapsedTime,
|
||||
weight,
|
||||
setWeight,
|
||||
reps,
|
||||
setReps,
|
||||
duration,
|
||||
setDuration,
|
||||
distance,
|
||||
setDistance,
|
||||
height,
|
||||
setHeight,
|
||||
bwPercentage,
|
||||
setBwPercentage,
|
||||
userBodyWeight,
|
||||
setUserBodyWeight,
|
||||
isCreating,
|
||||
setIsCreating,
|
||||
currentStepIndex,
|
||||
showPlanPrep,
|
||||
setShowPlanPrep,
|
||||
showPlanList,
|
||||
setShowPlanList,
|
||||
showFinishConfirm,
|
||||
setShowFinishConfirm,
|
||||
showQuitConfirm,
|
||||
setShowQuitConfirm,
|
||||
showMenu,
|
||||
setShowMenu,
|
||||
editingSetId,
|
||||
editWeight,
|
||||
setEditWeight,
|
||||
editReps,
|
||||
setEditReps,
|
||||
editDuration,
|
||||
setEditDuration,
|
||||
editDistance,
|
||||
setEditDistance,
|
||||
editHeight,
|
||||
setEditHeight,
|
||||
isSporadicMode,
|
||||
setIsSporadicMode,
|
||||
// Form Props
|
||||
weight: form.weight, setWeight: form.setWeight,
|
||||
reps: form.reps, setReps: form.setReps,
|
||||
duration: form.duration, setDuration: form.setDuration,
|
||||
distance: form.distance, setDistance: form.setDistance,
|
||||
height: form.height, setHeight: form.setHeight,
|
||||
bwPercentage: form.bwPercentage, setBwPercentage: form.setBwPercentage,
|
||||
unilateralSide: form.unilateralSide, setUnilateralSide: form.setUnilateralSide,
|
||||
|
||||
userBodyWeight, setUserBodyWeight,
|
||||
isCreating, setIsCreating,
|
||||
|
||||
// Plan Execution Props
|
||||
currentStepIndex: planExec.currentStepIndex,
|
||||
showPlanPrep: planExec.showPlanPrep, setShowPlanPrep: planExec.setShowPlanPrep,
|
||||
showPlanList: planExec.showPlanList, setShowPlanList: planExec.setShowPlanList,
|
||||
jumpToStep: planExec.jumpToStep,
|
||||
|
||||
showFinishConfirm, setShowFinishConfirm,
|
||||
showQuitConfirm, setShowQuitConfirm,
|
||||
showMenu, setShowMenu,
|
||||
|
||||
// Editing
|
||||
editingSetId: form.editingSetId,
|
||||
editWeight: form.editWeight, setEditWeight: form.setEditWeight,
|
||||
editReps: form.editReps, setEditReps: form.setEditReps,
|
||||
editDuration: form.editDuration, setEditDuration: form.setEditDuration,
|
||||
editDistance: form.editDistance, setEditDistance: form.setEditDistance,
|
||||
editHeight: form.editHeight, setEditHeight: form.setEditHeight,
|
||||
|
||||
isSporadicMode, setIsSporadicMode,
|
||||
sporadicSuccess,
|
||||
filteredExercises,
|
||||
handleStart,
|
||||
@@ -481,11 +253,8 @@ export const useTracker = ({
|
||||
handleEditSet,
|
||||
handleSaveEdit,
|
||||
handleCancelEdit,
|
||||
jumpToStep,
|
||||
resetForm,
|
||||
unilateralSide,
|
||||
setUnilateralSide,
|
||||
quickLogSession, // Export this
|
||||
loadQuickLogSession, // Export reload function
|
||||
quickLogSession,
|
||||
loadQuickLogSession
|
||||
};
|
||||
};
|
||||
|
||||
61
src/context/AuthContext.tsx
Normal file
61
src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { User } from '../types';
|
||||
import { getMe } from '../services/auth';
|
||||
|
||||
interface AuthContextType {
|
||||
currentUser: User | null;
|
||||
setCurrentUser: (user: User | null) => void;
|
||||
isLoading: boolean;
|
||||
logout: () => void;
|
||||
updateUser: (user: User) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const restoreSession = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
try {
|
||||
const res = await getMe();
|
||||
if (res.success && res.user) {
|
||||
setCurrentUser(res.user);
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
restoreSession();
|
||||
}, []);
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setCurrentUser(null);
|
||||
};
|
||||
|
||||
const updateUser = (user: User) => {
|
||||
setCurrentUser(user);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ currentUser, setCurrentUser, isLoading, logout, updateUser }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
208
src/context/DataContext.tsx
Normal file
208
src/context/DataContext.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
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;
|
||||
};
|
||||
65
src/hooks/usePlanExecution.ts
Normal file
65
src/hooks/usePlanExecution.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WorkoutSession, WorkoutPlan, ExerciseDef } from '../types';
|
||||
|
||||
interface UsePlanExecutionProps {
|
||||
activeSession: WorkoutSession | null;
|
||||
activePlan: WorkoutPlan | null;
|
||||
exercises: ExerciseDef[];
|
||||
}
|
||||
|
||||
export const usePlanExecution = ({ activeSession, activePlan, exercises }: UsePlanExecutionProps) => {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [showPlanPrep, setShowPlanPrep] = useState<WorkoutPlan | null>(null);
|
||||
const [showPlanList, setShowPlanList] = useState(false);
|
||||
|
||||
// Automatically determine current step based on logged sets vs plan
|
||||
useEffect(() => {
|
||||
if (activeSession && activePlan) {
|
||||
const performedCounts = new Map<string, number>();
|
||||
for (const set of activeSession.sets) {
|
||||
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
|
||||
}
|
||||
|
||||
let nextStepIndex = activePlan.steps.length; // Default to finished
|
||||
const plannedCounts = new Map<string, number>();
|
||||
for (let i = 0; i < activePlan.steps.length; i++) {
|
||||
const step = activePlan.steps[i];
|
||||
const exerciseId = step.exerciseId;
|
||||
plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1);
|
||||
const performedCount = performedCounts.get(exerciseId) || 0;
|
||||
|
||||
if (performedCount < plannedCounts.get(exerciseId)!) {
|
||||
nextStepIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setCurrentStepIndex(nextStepIndex);
|
||||
}
|
||||
}, [activeSession, activePlan]);
|
||||
|
||||
const getCurrentStep = () => {
|
||||
if (activeSession && activePlan && activePlan.steps.length > 0) {
|
||||
if (currentStepIndex < activePlan.steps.length) {
|
||||
return activePlan.steps[currentStepIndex];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const jumpToStep = (index: number) => {
|
||||
if (!activePlan) return;
|
||||
setCurrentStepIndex(index);
|
||||
setShowPlanList(false);
|
||||
};
|
||||
|
||||
return {
|
||||
currentStepIndex,
|
||||
setCurrentStepIndex,
|
||||
showPlanPrep,
|
||||
setShowPlanPrep,
|
||||
showPlanList,
|
||||
setShowPlanList,
|
||||
getCurrentStep,
|
||||
jumpToStep
|
||||
};
|
||||
};
|
||||
27
src/hooks/useSessionTimer.ts
Normal file
27
src/hooks/useSessionTimer.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WorkoutSession } from '../types';
|
||||
|
||||
export const useSessionTimer = (activeSession: WorkoutSession | null) => {
|
||||
const [elapsedTime, setElapsedTime] = useState<string>('00:00:00');
|
||||
|
||||
useEffect(() => {
|
||||
let interval: number;
|
||||
if (activeSession) {
|
||||
const updateTimer = () => {
|
||||
const diff = Math.floor((Date.now() - activeSession.startTime) / 1000);
|
||||
const h = Math.floor(diff / 3600);
|
||||
const m = Math.floor((diff % 3600) / 60);
|
||||
const s = diff % 60;
|
||||
setElapsedTime(`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`);
|
||||
};
|
||||
|
||||
updateTimer();
|
||||
interval = window.setInterval(updateTimer, 1000);
|
||||
} else {
|
||||
setElapsedTime('00:00:00');
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [activeSession]);
|
||||
|
||||
return elapsedTime;
|
||||
};
|
||||
147
src/hooks/useWorkoutForm.ts
Normal file
147
src/hooks/useWorkoutForm.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WorkoutSet, ExerciseDef, ExerciseType } from '../types';
|
||||
import { getLastSetForExercise } from '../services/storage';
|
||||
|
||||
interface UseWorkoutFormProps {
|
||||
userId: string;
|
||||
onSetAdded?: (set: WorkoutSet) => void;
|
||||
onUpdateSet?: (set: WorkoutSet) => void;
|
||||
}
|
||||
|
||||
export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFormProps) => {
|
||||
const [weight, setWeight] = useState<string>('');
|
||||
const [reps, setReps] = useState<string>('');
|
||||
const [duration, setDuration] = useState<string>('');
|
||||
const [distance, setDistance] = useState<string>('');
|
||||
const [height, setHeight] = useState<string>('');
|
||||
const [bwPercentage, setBwPercentage] = useState<string>('100');
|
||||
|
||||
// Unilateral State
|
||||
const [unilateralSide, setUnilateralSide] = useState<'LEFT' | 'RIGHT'>('LEFT');
|
||||
|
||||
// Editing State
|
||||
const [editingSetId, setEditingSetId] = useState<string | null>(null);
|
||||
const [editWeight, setEditWeight] = useState<string>('');
|
||||
const [editReps, setEditReps] = useState<string>('');
|
||||
const [editDuration, setEditDuration] = useState<string>('');
|
||||
const [editDistance, setEditDistance] = useState<string>('');
|
||||
const [editHeight, setEditHeight] = useState<string>('');
|
||||
|
||||
const resetForm = () => {
|
||||
setWeight('');
|
||||
setReps('');
|
||||
setDuration('');
|
||||
setDistance('');
|
||||
setHeight('');
|
||||
};
|
||||
|
||||
const updateFormFromLastSet = async (exerciseId: string, exerciseType: ExerciseType, bodyWeightPercentage?: number) => {
|
||||
setBwPercentage(bodyWeightPercentage ? bodyWeightPercentage.toString() : '100');
|
||||
|
||||
const set = await getLastSetForExercise(userId, exerciseId);
|
||||
if (set) {
|
||||
setWeight(set.weight?.toString() || '');
|
||||
setReps(set.reps?.toString() || '');
|
||||
setDuration(set.durationSeconds?.toString() || '');
|
||||
setDistance(set.distanceMeters?.toString() || '');
|
||||
setHeight(set.height?.toString() || '');
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// Clear irrelevant fields
|
||||
if (exerciseType !== ExerciseType.STRENGTH && exerciseType !== ExerciseType.BODYWEIGHT) setWeight('');
|
||||
if (exerciseType !== ExerciseType.STRENGTH && exerciseType !== ExerciseType.BODYWEIGHT && exerciseType !== ExerciseType.PLYOMETRIC) setReps('');
|
||||
if (exerciseType !== ExerciseType.CARDIO && exerciseType !== ExerciseType.STATIC) setDuration('');
|
||||
if (exerciseType !== ExerciseType.CARDIO && exerciseType !== ExerciseType.LONG_JUMP) setDistance('');
|
||||
if (exerciseType !== ExerciseType.HIGH_JUMP) setHeight('');
|
||||
};
|
||||
|
||||
const prepareSetData = (selectedExercise: ExerciseDef, isSporadic: boolean = false) => {
|
||||
const setData: Partial<WorkoutSet> = {
|
||||
exerciseId: selectedExercise.id,
|
||||
};
|
||||
|
||||
if (selectedExercise.isUnilateral) {
|
||||
setData.side = unilateralSide;
|
||||
}
|
||||
|
||||
switch (selectedExercise.type) {
|
||||
case ExerciseType.STRENGTH:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
case ExerciseType.BODYWEIGHT:
|
||||
if (weight) setData.weight = parseFloat(weight);
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.CARDIO:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.STATIC:
|
||||
if (duration) setData.durationSeconds = parseInt(duration);
|
||||
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||
break;
|
||||
case ExerciseType.HIGH_JUMP:
|
||||
if (height) setData.height = parseFloat(height);
|
||||
break;
|
||||
case ExerciseType.LONG_JUMP:
|
||||
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||
break;
|
||||
case ExerciseType.PLYOMETRIC:
|
||||
if (reps) setData.reps = parseInt(reps);
|
||||
break;
|
||||
}
|
||||
return setData;
|
||||
};
|
||||
|
||||
const startEditing = (set: WorkoutSet) => {
|
||||
setEditingSetId(set.id);
|
||||
setEditWeight(set.weight?.toString() || '');
|
||||
setEditReps(set.reps?.toString() || '');
|
||||
setEditDuration(set.durationSeconds?.toString() || '');
|
||||
setEditDistance(set.distanceMeters?.toString() || '');
|
||||
setEditHeight(set.height?.toString() || '');
|
||||
};
|
||||
|
||||
const saveEdit = (set: WorkoutSet) => {
|
||||
const updatedSet: WorkoutSet = {
|
||||
...set,
|
||||
...(editWeight && { weight: parseFloat(editWeight) }),
|
||||
...(editReps && { reps: parseInt(editReps) }),
|
||||
...(editDuration && { durationSeconds: parseInt(editDuration) }),
|
||||
...(editDistance && { distanceMeters: parseFloat(editDistance) }),
|
||||
...(editHeight && { height: parseFloat(editHeight) })
|
||||
};
|
||||
if (onUpdateSet) onUpdateSet(updatedSet);
|
||||
setEditingSetId(null);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingSetId(null);
|
||||
};
|
||||
|
||||
return {
|
||||
weight, setWeight,
|
||||
reps, setReps,
|
||||
duration, setDuration,
|
||||
distance, setDistance,
|
||||
height, setHeight,
|
||||
bwPercentage, setBwPercentage,
|
||||
unilateralSide, setUnilateralSide,
|
||||
editingSetId,
|
||||
editWeight, setEditWeight,
|
||||
editReps, setEditReps,
|
||||
editDuration, setEditDuration,
|
||||
editDistance, setEditDistance,
|
||||
editHeight, setEditHeight,
|
||||
resetForm,
|
||||
updateFormFromLastSet,
|
||||
prepareSetData,
|
||||
startEditing,
|
||||
saveEdit,
|
||||
cancelEdit
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,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 App from './App';
|
||||
import './index.css';
|
||||
|
||||
@@ -11,6 +14,12 @@ if (!rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<DataProvider>
|
||||
<App />
|
||||
</DataProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -13,12 +13,12 @@ const headers = () => {
|
||||
};
|
||||
|
||||
export const api = {
|
||||
get: async (endpoint: string) => {
|
||||
get: async <T = any>(endpoint: string): Promise<T> => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, { headers: headers() });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
post: async (endpoint: string, data: any) => {
|
||||
post: async <T = any>(endpoint: string, data: any): Promise<T> => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
@@ -27,7 +27,7 @@ export const api = {
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
put: async (endpoint: string, data: any) => {
|
||||
put: async <T = any>(endpoint: string, data: any): Promise<T> => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
@@ -36,7 +36,7 @@ export const api = {
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
delete: async (endpoint: string) => {
|
||||
delete: async <T = any>(endpoint: string): Promise<T> => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
headers: headers()
|
||||
@@ -44,7 +44,7 @@ export const api = {
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
patch: async (endpoint: string, data: any) => {
|
||||
patch: async <T = any>(endpoint: string, data: any): Promise<T> => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'PATCH',
|
||||
headers: headers(),
|
||||
|
||||
27
src/services/exercises.ts
Normal file
27
src/services/exercises.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ExerciseDef, WorkoutSet } from '../types';
|
||||
import { api } from './api';
|
||||
|
||||
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
|
||||
try {
|
||||
return await api.get<ExerciseDef[]>('/exercises');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveExercise = async (userId: string, exercise: ExerciseDef): Promise<void> => {
|
||||
await api.post('/exercises', exercise);
|
||||
};
|
||||
|
||||
export const getLastSetForExercise = async (userId: string, exerciseId: string): Promise<WorkoutSet | undefined> => {
|
||||
try {
|
||||
const response = await api.get<{ success: boolean; set?: WorkoutSet }>(`/exercises/${exerciseId}/last-set`);
|
||||
if (response.success && response.set) {
|
||||
return response.set;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch last set:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
18
src/services/plans.ts
Normal file
18
src/services/plans.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { WorkoutPlan } from '../types';
|
||||
import { api } from './api';
|
||||
|
||||
export const getPlans = async (userId: string): Promise<WorkoutPlan[]> => {
|
||||
try {
|
||||
return await api.get<WorkoutPlan[]>('/plans');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const savePlan = async (userId: string, plan: WorkoutPlan): Promise<void> => {
|
||||
await api.post('/plans', plan);
|
||||
};
|
||||
|
||||
export const deletePlan = async (userId: string, id: string): Promise<void> => {
|
||||
await api.delete(`/plans/${id}`);
|
||||
};
|
||||
85
src/services/sessions.ts
Normal file
85
src/services/sessions.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { WorkoutSession, WorkoutSet, ExerciseType } from '../types';
|
||||
import { api } from './api';
|
||||
|
||||
// Define the shape of session coming from API (Prisma include)
|
||||
interface ApiSession extends Omit<WorkoutSession, 'startTime' | 'endTime' | 'sets'> {
|
||||
startTime: string | number; // JSON dates are strings
|
||||
endTime?: string | number;
|
||||
sets: (Omit<WorkoutSet, 'exerciseName' | 'type'> & {
|
||||
exercise?: {
|
||||
name: string;
|
||||
type: ExerciseType;
|
||||
}
|
||||
})[];
|
||||
}
|
||||
|
||||
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
|
||||
try {
|
||||
const sessions = await api.get<ApiSession[]>('/sessions');
|
||||
// Convert ISO date strings to timestamps
|
||||
return sessions.map((session) => ({
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set) => ({
|
||||
...set,
|
||||
exerciseName: set.exercise?.name || 'Unknown',
|
||||
type: set.exercise?.type || ExerciseType.STRENGTH
|
||||
})) as WorkoutSet[]
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
|
||||
await api.post('/sessions', session);
|
||||
};
|
||||
|
||||
export const getActiveSession = async (userId: string): Promise<WorkoutSession | null> => {
|
||||
try {
|
||||
const response = await api.get<{ success: boolean; session?: ApiSession }>('/sessions/active');
|
||||
if (!response.success || !response.session) {
|
||||
return null;
|
||||
}
|
||||
const session = response.session;
|
||||
// Convert ISO date strings to timestamps
|
||||
return {
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set) => ({
|
||||
...set,
|
||||
exerciseName: set.exercise?.name || 'Unknown',
|
||||
type: set.exercise?.type || ExerciseType.STRENGTH
|
||||
})) as WorkoutSet[]
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateActiveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
|
||||
await api.put('/sessions/active', session);
|
||||
};
|
||||
|
||||
export const deleteSetFromActiveSession = async (userId: string, setId: string): Promise<void> => {
|
||||
await api.delete(`/sessions/active/set/${setId}`);
|
||||
};
|
||||
|
||||
export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial<WorkoutSet>): Promise<WorkoutSet> => {
|
||||
const response = await api.put<{ success: boolean; updatedSet: WorkoutSet }>(`/sessions/active/set/${setId}`, setData);
|
||||
return response.updatedSet;
|
||||
};
|
||||
|
||||
export const deleteActiveSession = async (userId: string): Promise<void> => {
|
||||
await api.delete('/sessions/active');
|
||||
};
|
||||
|
||||
export const deleteSession = async (userId: string, id: string): Promise<void> => {
|
||||
await api.delete(`/sessions/${id}`);
|
||||
};
|
||||
|
||||
export const deleteAllUserData = (userId: string) => {
|
||||
// Not implemented in frontend
|
||||
};
|
||||
@@ -1,114 +1,3 @@
|
||||
import { WorkoutSession, ExerciseDef, ExerciseType, WorkoutSet, WorkoutPlan } from '../types';
|
||||
import { api } from './api';
|
||||
|
||||
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
|
||||
try {
|
||||
const sessions = await api.get('/sessions');
|
||||
// Convert ISO date strings to timestamps
|
||||
return sessions.map((session: any) => ({
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set: any) => ({
|
||||
...set,
|
||||
exerciseName: set.exercise?.name || 'Unknown',
|
||||
type: set.exercise?.type || 'STRENGTH'
|
||||
}))
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
|
||||
await api.post('/sessions', session);
|
||||
};
|
||||
|
||||
export const getActiveSession = async (userId: string): Promise<WorkoutSession | null> => {
|
||||
try {
|
||||
const response = await api.get('/sessions/active');
|
||||
if (!response.success || !response.session) {
|
||||
return null;
|
||||
}
|
||||
const session = response.session;
|
||||
// Convert ISO date strings to timestamps
|
||||
return {
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set: any) => ({
|
||||
...set,
|
||||
exerciseName: set.exercise?.name || 'Unknown',
|
||||
type: set.exercise?.type || 'STRENGTH'
|
||||
}))
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateActiveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
|
||||
await api.put('/sessions/active', session);
|
||||
};
|
||||
|
||||
export const deleteSetFromActiveSession = async (userId: string, setId: string): Promise<void> => {
|
||||
await api.delete(`/sessions/active/set/${setId}`);
|
||||
};
|
||||
|
||||
export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial<WorkoutSet>): Promise<WorkoutSet> => {
|
||||
const response = await api.put(`/sessions/active/set/${setId}`, setData);
|
||||
return response.updatedSet;
|
||||
};
|
||||
|
||||
export const deleteActiveSession = async (userId: string): Promise<void> => {
|
||||
await api.delete('/sessions/active');
|
||||
};
|
||||
|
||||
export const deleteSession = async (userId: string, id: string): Promise<void> => {
|
||||
await api.delete(`/sessions/${id}`);
|
||||
};
|
||||
|
||||
export const deleteAllUserData = (userId: string) => {
|
||||
// Not implemented in frontend
|
||||
};
|
||||
|
||||
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
|
||||
try {
|
||||
return await api.get('/exercises');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveExercise = async (userId: string, exercise: ExerciseDef): Promise<void> => {
|
||||
await api.post('/exercises', exercise);
|
||||
};
|
||||
|
||||
export const getLastSetForExercise = async (userId: string, exerciseId: string): Promise<WorkoutSet | undefined> => {
|
||||
try {
|
||||
const response = await api.get(`/exercises/${exerciseId}/last-set`);
|
||||
if (response.success && response.set) {
|
||||
return response.set;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch last set:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPlans = async (userId: string): Promise<WorkoutPlan[]> => {
|
||||
try {
|
||||
return await api.get('/plans');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const savePlan = async (userId: string, plan: WorkoutPlan): Promise<void> => {
|
||||
await api.post('/plans', plan);
|
||||
};
|
||||
|
||||
export const deletePlan = async (userId: string, id: string): Promise<void> => {
|
||||
await api.delete(`/plans/${id}`);
|
||||
};
|
||||
export * from './exercises';
|
||||
export * from './sessions';
|
||||
export * from './plans';
|
||||
Reference in New Issue
Block a user