283 lines
9.4 KiB
TypeScript
283 lines
9.4 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import Navbar from './components/Navbar';
|
|
import Tracker from './components/Tracker/index';
|
|
import History from './components/History';
|
|
import Stats from './components/Stats';
|
|
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, SporadicSet } from './types';
|
|
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage';
|
|
import { getSporadicSets, updateSporadicSet, deleteSporadicSet } from './services/sporadicSets';
|
|
import { getCurrentUserProfile, getMe } from './services/auth';
|
|
import { getSystemLanguage } from './services/i18n';
|
|
import { logWeight } from './services/weight';
|
|
import { generateId } from './utils/uuid';
|
|
|
|
function App() {
|
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
const [currentTab, setCurrentTab] = useState<TabView>('TRACK');
|
|
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 [sporadicSets, setSporadicSets] = useState<SporadicSet[]>([]);
|
|
|
|
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);
|
|
// Load sporadic sets
|
|
const sporadicSets = await getSporadicSets();
|
|
setSporadicSets(sporadicSets);
|
|
} else {
|
|
setSessions([]);
|
|
setPlans([]);
|
|
setSporadicSets([]);
|
|
}
|
|
};
|
|
loadSessions();
|
|
}, [currentUser]);
|
|
|
|
const handleLogin = (user: User) => {
|
|
setCurrentUser(user);
|
|
setCurrentTab('TRACK');
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('token');
|
|
setCurrentUser(null);
|
|
setActiveSession(null);
|
|
setActivePlan(null);
|
|
};
|
|
|
|
const handleLanguageChange = (lang: Language) => {
|
|
setLanguage(lang);
|
|
};
|
|
|
|
const handleUserUpdate = (updatedUser: User) => {
|
|
setCurrentUser(updatedUser);
|
|
};
|
|
|
|
const handleStartSession = async (plan?: WorkoutPlan, startWeight?: number) => {
|
|
if (!currentUser) 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(),
|
|
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));
|
|
};
|
|
|
|
const handleSporadicSetAdded = async () => {
|
|
const sets = await getSporadicSets();
|
|
setSporadicSets(sets);
|
|
};
|
|
|
|
const handleUpdateSporadicSet = async (set: SporadicSet) => {
|
|
const updated = await updateSporadicSet(set.id, set);
|
|
if (updated) {
|
|
setSporadicSets(prev => prev.map(s => s.id === set.id ? updated : s));
|
|
}
|
|
};
|
|
|
|
const handleDeleteSporadicSet = async (id: string) => {
|
|
const success = await deleteSporadicSet(id);
|
|
if (success) {
|
|
setSporadicSets(prev => prev.filter(s => s.id !== id));
|
|
}
|
|
};
|
|
|
|
if (!currentUser) {
|
|
return <Login onLogin={handleLogin} language={language} onLanguageChange={handleLanguageChange} />;
|
|
}
|
|
|
|
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} />
|
|
|
|
{/* 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}
|
|
sporadicSets={sporadicSets}
|
|
onSessionStart={handleStartSession}
|
|
onSessionEnd={handleEndSession}
|
|
onSessionQuit={handleQuitSession}
|
|
onSetAdded={handleAddSet}
|
|
onRemoveSet={handleRemoveSetFromActive}
|
|
onUpdateSet={handleUpdateSetInActive}
|
|
onSporadicSetAdded={handleSporadicSetAdded}
|
|
lang={language}
|
|
/>
|
|
)}
|
|
{currentTab === 'PLANS' && (
|
|
<Plans userId={currentUser.id} onStartPlan={handleStartSession} lang={language} />
|
|
)}
|
|
{currentTab === 'HISTORY' && (
|
|
<History
|
|
sessions={sessions}
|
|
sporadicSets={sporadicSets}
|
|
onUpdateSession={handleUpdateSession}
|
|
onDeleteSession={handleDeleteSession}
|
|
onUpdateSporadicSet={handleUpdateSporadicSet}
|
|
onDeleteSporadicSet={handleDeleteSporadicSet}
|
|
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}
|
|
/>
|
|
)}
|
|
</div>
|
|
</main>
|
|
|
|
{/* Mobile Navigation (rendered inside Navbar component, fixed to bottom) */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|