1. Tailwind migretion. 2. Backend Type Safety. 3. Context Refactoring.
This commit is contained in:
@@ -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) }
|
||||
]);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user