← back to keeper92__liftlog

Function bodies 262 total

All specs Real LLM only Function bodies
getExerciseSummaries function · typescript · L231-L250 (20 LOC)
src/app/(app)/dashboard/page.tsx
  function getExerciseSummaries(workout: RecentWorkout) {
    return buildExerciseSetSummaries(
      (workout.sets || []).map((set) => {
        const exercise = Array.isArray(set.exercises) ? set.exercises[0] : set.exercises;
        return {
          exerciseId: set.exercise_id,
          exerciseName: exercise?.name || 'Exercise',
          setNumber: set.set_number,
          weight: set.weight,
          reps: set.reps,
          isSplitLR: set.is_split_lr,
          leftWeight: set.left_weight,
          leftReps: set.left_reps,
          rightWeight: set.right_weight,
          rightReps: set.right_reps,
        };
      }),
      unitSystem,
    );
  }
handleCreateTemplateWithAI function · typescript · L252-L256 (5 LOC)
src/app/(app)/dashboard/page.tsx
  function handleCreateTemplateWithAI() {
    setShowHistory(false);
    setShowPRFeed(false);
    router.push('/trainer?intent=create-template');
  }
openManualTemplateBuilder function · typescript · L258-L277 (20 LOC)
src/app/(app)/dashboard/page.tsx
  function openManualTemplateBuilder() {
    setShowHistory(false);
    setShowPRFeed(false);
    const suggestedName = `Template ${new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
    const enteredName = window.prompt('Template name', suggestedName);
    if (enteredName === null) return;
    const templateName = enteredName.trim() || suggestedName;

    hydrateWorkoutSession({
      workoutName: templateName,
      startTime: new Date().toISOString(),
      exercises: [],
      builderMode: 'template_builder',
    });

    const state = useActiveWorkoutStore.getState();
    if (state.workoutId) {
      router.push(`/workout/${state.workoutId}`);
    }
  }
closeManualTemplateBuilder function · typescript · L279-L284 (6 LOC)
src/app/(app)/dashboard/page.tsx
  function closeManualTemplateBuilder() {
    if (savingManualTemplate) return;
    setShowManualTemplateModal(false);
    setShowTemplateExercisePicker(false);
    setManualTemplateError(null);
  }
addExerciseToManualTemplate function · typescript · L286-L303 (18 LOC)
src/app/(app)/dashboard/page.tsx
  function addExerciseToManualTemplate(exercise: { id: string; name: string; category: string }) {
    setManualTemplateError(null);
    setManualTemplateExercises((prev) => {
      if (prev.some((item) => item.exerciseId === exercise.id)) {
        setManualTemplateError('Exercise already added to this template.');
        return prev;
      }
      return [
        ...prev,
        {
          exerciseId: exercise.id,
          name: exercise.name,
          category: exercise.category,
          defaultSets: 3,
        },
      ];
    });
  }
updateManualTemplateSets function · typescript · L305-L316 (12 LOC)
src/app/(app)/dashboard/page.tsx
  function updateManualTemplateSets(index: number, value: string) {
    const parsed = Number.parseInt(value, 10);
    setManualTemplateExercises((prev) => {
      const next = [...prev];
      if (!next[index]) return prev;
      next[index] = {
        ...next[index],
        defaultSets: Number.isFinite(parsed) ? Math.max(1, Math.min(12, parsed)) : 1,
      };
      return next;
    });
  }
removeManualTemplateExercise function · typescript · L318-L320 (3 LOC)
src/app/(app)/dashboard/page.tsx
  function removeManualTemplateExercise(index: number) {
    setManualTemplateExercises((prev) => prev.filter((_, i) => i !== index));
  }
Repobility · MCP-ready · https://repobility.com
moveManualTemplateExercise function · typescript · L322-L330 (9 LOC)
src/app/(app)/dashboard/page.tsx
  function moveManualTemplateExercise(index: number, direction: 'up' | 'down') {
    setManualTemplateExercises((prev) => {
      const target = direction === 'up' ? index - 1 : index + 1;
      if (target < 0 || target >= prev.length) return prev;
      const next = [...prev];
      [next[index], next[target]] = [next[target], next[index]];
      return next;
    });
  }
handleSaveManualTemplate function · typescript · L332-L391 (60 LOC)
src/app/(app)/dashboard/page.tsx
  async function handleSaveManualTemplate() {
    const trimmedName = manualTemplateName.trim();
    if (!trimmedName) {
      setManualTemplateError('Template name is required.');
      return;
    }
    if (manualTemplateExercises.length === 0) {
      setManualTemplateError('Add at least one exercise.');
      return;
    }

    setSavingManualTemplate(true);
    setManualTemplateError(null);
    const { data: { user } } = await supabase.auth.getUser();
    if (!user) {
      setManualTemplateError('Please sign in again.');
      setSavingManualTemplate(false);
      return;
    }

    const { data: template, error: templateError } = await supabase
      .from('workout_templates')
      .insert({ user_id: user.id, name: trimmedName })
      .select('id')
      .single();

    if (templateError || !template) {
      setManualTemplateError(templateError?.message || 'Could not create template.');
      setSavingManualTemplate(false);
      return;
    }

    const templateRows = manual
handleStartFromTemplate function · typescript · L393-L406 (14 LOC)
src/app/(app)/dashboard/page.tsx
  function handleStartFromTemplate(template: TemplateSummary) {
    const sortedExercises = [...template.template_exercises].sort(
      (a, b) => a.order_index - b.order_index
    );
    startWorkout(template.name, template.id);
    for (const ex of sortedExercises) {
      addExerciseWithSets(
        { id: ex.exercise_id, name: ex.exercises.name, category: ex.exercises.category },
        ex.default_sets || 3
      );
    }
    const state = useActiveWorkoutStore.getState();
    router.push(`/workout/${state.workoutId}`);
  }
ExercisesPage function · typescript · L21-L27 (7 LOC)
src/app/(app)/exercises/page.tsx
export default function ExercisesPage() {
  return (
    <Suspense fallback={<div className="flex items-center justify-center min-h-dvh text-muted-foreground">Loading...</div>}>
      <ExercisesContent />
    </Suspense>
  );
}
handleSelectExercise function · typescript · L92-L97 (6 LOC)
src/app/(app)/exercises/page.tsx
  function handleSelectExercise(exercise: ExerciseRow) {
    if (isSelecting) {
      addExercise({ id: exercise.id, name: exercise.name, category: exercise.category });
      router.back();
    }
  }
findPotentialDuplicates function · typescript · L99-L114 (16 LOC)
src/app/(app)/exercises/page.tsx
  async function findPotentialDuplicates(name: string) {
    const { data, error } = await supabase
      .from('exercises')
      .select('id, name, category, primary_muscles, equipment, is_custom, user_id')
      .order('name')
      .limit(2000);

    if (error || !data) {
      return { matches: [] as ExerciseRow[], error: 'Could not run duplicate check.' };
    }

    return {
      matches: rankExercisesBySearch(data as ExerciseRow[], name).slice(0, 12),
      error: null,
    };
  }
openCreateModal function · typescript · L116-L129 (14 LOC)
src/app/(app)/exercises/page.tsx
  function openCreateModal(initialName?: string) {
    setEditingExercise(null);
    const initial = initialName ?? search ?? '';
    setExerciseForm({
      name: initial,
      category: inferExerciseCategoryFromName(initial),
      primaryMuscle: inferPrimaryMuscleFromName(initial),
    });
    setCategoryManuallyChanged(false);
    setPrimaryMuscleManuallyChanged(false);
    setActionError(null);
    setAiDetails(null);
    setShowCreateModal(true);
  }
startCreateFlow function · typescript · L131-L148 (18 LOC)
src/app/(app)/exercises/page.tsx
  async function startCreateFlow() {
    const candidate = search.trim();
    if (!candidate) {
      openCreateModal('');
      return;
    }

    setDuplicateCheckName(candidate);
    setDuplicateResults([]);
    setDuplicateError(null);
    setShowDuplicateCheckModal(true);
    setDuplicateChecking(true);

    const { matches, error } = await findPotentialDuplicates(candidate);
    setDuplicateResults(matches);
    setDuplicateError(error);
    setDuplicateChecking(false);
  }
All rows above produced by Repobility · https://repobility.com
closeDuplicateCheckModal function · typescript · L150-L156 (7 LOC)
src/app/(app)/exercises/page.tsx
  function closeDuplicateCheckModal() {
    if (duplicateChecking) return;
    setShowDuplicateCheckModal(false);
    setDuplicateCheckName('');
    setDuplicateResults([]);
    setDuplicateError(null);
  }
handleUsePotentialDuplicate function · typescript · L158-L165 (8 LOC)
src/app/(app)/exercises/page.tsx
  function handleUsePotentialDuplicate(exercise: ExerciseRow) {
    if (isSelecting) {
      handleSelectExercise(exercise);
    } else {
      setSearch(exercise.name);
    }
    closeDuplicateCheckModal();
  }
handleCreateAnyway function · typescript · L167-L171 (5 LOC)
src/app/(app)/exercises/page.tsx
  function handleCreateAnyway() {
    const initial = duplicateCheckName || search;
    closeDuplicateCheckModal();
    openCreateModal(initial);
  }
openEditModal function · typescript · L173-L185 (13 LOC)
src/app/(app)/exercises/page.tsx
  function openEditModal(exercise: ExerciseRow) {
    setEditingExercise(exercise);
    setExerciseForm({
      name: exercise.name,
      category: exercise.category,
      primaryMuscle: exercise.primary_muscles[0] || 'chest',
    });
    setCategoryManuallyChanged(true);
    setPrimaryMuscleManuallyChanged(true);
    setActionError(null);
    setAiDetails(null);
    setShowCreateModal(true);
  }
closeCreateModal function · typescript · L187-L193 (7 LOC)
src/app/(app)/exercises/page.tsx
  function closeCreateModal() {
    if (saving || deleting) return;
    setShowCreateModal(false);
    setCategoryManuallyChanged(false);
    setPrimaryMuscleManuallyChanged(false);
    setActionError(null);
  }
openMergeModal function · typescript · L195-L203 (9 LOC)
src/app/(app)/exercises/page.tsx
  function openMergeModal(exercise: ExerciseRow) {
    setShowCreateModal(false);
    setMergeSourceExercise(exercise);
    setMergeSearch(exercise.name);
    setMergeResults([]);
    setMergeTarget(null);
    setMergeError(null);
    setShowMergeModal(true);
  }
closeMergeModal function · typescript · L205-L213 (9 LOC)
src/app/(app)/exercises/page.tsx
  function closeMergeModal() {
    if (mergeSaving) return;
    setShowMergeModal(false);
    setMergeSourceExercise(null);
    setMergeSearch('');
    setMergeResults([]);
    setMergeTarget(null);
    setMergeError(null);
  }
handleAiFill function · typescript · L253-L289 (37 LOC)
src/app/(app)/exercises/page.tsx
  async function handleAiFill() {
    const trimmedName = exerciseForm.name.trim();
    if (!trimmedName) {
      setActionError('Enter an exercise name first.');
      return;
    }
    setAiLoading(true);
    setActionError(null);
    try {
      const res = await fetch('/api/exercise-details', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: trimmedName, category: exerciseForm.category }),
      });
      if (!res.ok) throw new Error('Failed to generate details');
      const details = await res.json();
      setAiDetails({
        instructions: details.instructions || [],
        secondaryMuscles: details.secondaryMuscles || [],
        force: details.force || null,
        level: details.level || null,
        mechanic: details.mechanic || null,
        equipment: details.equipment || null,
      });
      // Update form fields from AI if user hasn't manually changed them
      if (!categoryManuallyChanged && d
Repobility (the analyzer behind this table) · https://repobility.com
handleSaveExercise function · typescript · L291-L425 (135 LOC)
src/app/(app)/exercises/page.tsx
  async function handleSaveExercise() {
    const trimmedName = exerciseForm.name.trim();
    if (!trimmedName) {
      setActionError('Exercise name is required.');
      return;
    }

    setSaving(true);
    setActionError(null);
    const inferredEquipment = inferEquipmentFromCategory(exerciseForm.category);

    const isOwnCustom = editingExercise?.is_custom && editingExercise?.user_id === currentUserId;

    if (editingExercise && isOwnCustom) {
      // Update existing custom exercise (user owns it)
      const { error } = await supabase
        .from('exercises')
        .update({
          name: trimmedName,
          category: exerciseForm.category,
          primary_muscles: [exerciseForm.primaryMuscle],
          equipment: aiDetails?.equipment || inferredEquipment,
          ...(aiDetails && {
            secondary_muscles: aiDetails.secondaryMuscles,
            instructions: aiDetails.instructions,
            force: aiDetails.force,
            level: aiDetails.level,
handleDeleteExercise function · typescript · L427-L489 (63 LOC)
src/app/(app)/exercises/page.tsx
  async function handleDeleteExercise() {
    if (!editingExercise) return;

    const isOwnCustom = editingExercise.is_custom && editingExercise.user_id === currentUserId;
    if (!isOwnCustom) return;

    setActionError(null);

    const [{ count: setCount }, { count: templateCount }] = await Promise.all([
      supabase.from('sets').select('*', { count: 'exact', head: true }).eq('exercise_id', editingExercise.id),
      supabase.from('template_exercises').select('*', { count: 'exact', head: true }).eq('exercise_id', editingExercise.id),
    ]);

    const usageWarnings: string[] = [];
    if ((setCount || 0) > 0) {
      usageWarnings.push(`${setCount} logged set${setCount === 1 ? '' : 's'}`);
    }
    if ((templateCount || 0) > 0) {
      usageWarnings.push(`${templateCount} template entr${templateCount === 1 ? 'y' : 'ies'}`);
    }

    const warningText = usageWarnings.length
      ? `This will also delete ${usageWarnings.join(' and ')} tied to this exercise.`
      : 'This can
handleMergeExercises function · typescript · L491-L591 (101 LOC)
src/app/(app)/exercises/page.tsx
  async function handleMergeExercises() {
    if (!mergeSourceExercise || !mergeTarget) return;

    const confirmed = window.confirm(
      `Merge "${mergeSourceExercise.name}" into "${mergeTarget.name}"?\n\nThis will move all logged history and template references, then delete "${mergeSourceExercise.name}".`
    );
    if (!confirmed) return;

    setMergeSaving(true);
    setMergeError(null);

    const { error: moveSetsError } = await supabase
      .from('sets')
      .update({ exercise_id: mergeTarget.id })
      .eq('exercise_id', mergeSourceExercise.id);
    if (moveSetsError) {
      setMergeError(moveSetsError.message || 'Could not move exercise history.');
      setMergeSaving(false);
      return;
    }

    type TemplateExerciseRef = {
      template_id: string;
      order_index: number;
      default_sets: number;
    };

    const { data: sourceTemplateRefs, error: sourceTemplateRefsError } = await supabase
      .from('template_exercises')
      .select('template_id, o
AppLayout function · typescript · L9-L24 (16 LOC)
src/app/(app)/layout.tsx
export default function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <DesktopMobileFrame>
      <div className="relative flex min-h-dvh flex-col bg-background lg:min-h-full">
        <WorkoutOutboxSync />
        <MainContent>{children}</MainContent>
        <ActiveWorkoutRibbon />
        <BottomNav />
      </div>
    </DesktopMobileFrame>
  );
}
ProgressPage function · typescript · L34-L274 (241 LOC)
src/app/(app)/progress/page.tsx
export default function ProgressPage() {
  const router = useRouter();
  const supabase = useMemo(() => createClient(), []);
  const unitSystem = useSettingsStore((s) => s.unitSystem);
  const startWorkout = useActiveWorkoutStore((s) => s.startWorkout);
  const [exercises, setExercises] = useState<ExerciseOption[]>([]);
  const [selectedExercise, setSelectedExercise] = useState<string>('');
  const [progressData, setProgressData] = useState<ProgressPoint[]>([]);
  const [loading, setLoading] = useState(true);

  const unit = weightUnit(unitSystem);
  const selectedExerciseName = exercises.find((ex) => ex.id === selectedExercise)?.name ?? 'this exercise';
  const chartUnlockThreshold: number = 3;
  const chartConfig: ChartConfig = {
    maxWeight: {
      label: `Max Weight (${unit})`,
      color: 'var(--primary)',
    },
  };

  // Load exercises the user has done
  useEffect(() => {
    async function load() {
      const { data: { user } } = await supabase.auth.getUser();
      if (
load function · typescript · L56-L79 (24 LOC)
src/app/(app)/progress/page.tsx
    async function load() {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return;

      // Get exercises the user has logged sets for
      const { data: setData } = await supabase
        .from('sets')
        .select('exercise_id, exercises(id, name), workouts!inner(user_id)')
        .eq('workouts.user_id', user.id);

      if (setData) {
        const exerciseMap = new Map<string, string>();
        for (const s of setData) {
          const ex = s.exercises as unknown as { id: string; name: string };
          if (ex) exerciseMap.set(ex.id, ex.name);
        }
        const opts = Array.from(exerciseMap.entries()).map(([id, name]) => ({ id, name }));
        opts.sort((a, b) => a.name.localeCompare(b.name));
        setExercises(opts);
        if (opts.length > 0) setSelectedExercise(opts[0].id);
      }

      setLoading(false);
    }
loadProgress function · typescript · L86-L96 (11 LOC)
src/app/(app)/progress/page.tsx
    async function loadProgress() {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return;

      const { data } = await supabase.rpc('get_exercise_progress', {
        user_uuid: user.id,
        exercise_uuid: selectedExercise,
      });
      if (data) setProgressData(data as ProgressPoint[]);
      else setProgressData([]);
    }
handleStartWorkout function · typescript · L120-L126 (7 LOC)
src/app/(app)/progress/page.tsx
  function handleStartWorkout() {
    startWorkout();
    const state = useActiveWorkoutStore.getState();
    if (state.workoutId) {
      router.push(`/workout/${state.workoutId}`);
    }
  }
Repobility · code-quality intelligence · https://repobility.com
SettingsPage function · typescript · L21-L137 (117 LOC)
src/app/(app)/settings/page.tsx
export default function SettingsPage() {
  const router = useRouter();
  const supabase = createClient();
  const {
    unitSystem,
    defaultRestTimer,
    autoStartRestTimer,
    setUnitSystem,
    setDefaultRestTimer,
    setAutoStartRestTimer,
  } = useSettingsStore();
  const trainerProfile = useTrainerProfileStore((s) => s.profile);

  async function handleSignOut() {
    await supabase.auth.signOut();
    router.push('/login');
  }

  return (
    <div className="pb-24">
      <div className="mx-auto w-full max-w-3xl space-y-4 px-4 pt-4 sm:px-6">
        <header className="space-y-1">
          <h1 className="text-xl font-semibold tracking-tight">Settings</h1>
          <p className="text-sm text-muted-foreground">Manage your app preferences and training defaults.</p>
        </header>

        <Card>
          <CardHeader className="pb-3">
            <CardTitle className="text-base">Unit System</CardTitle>
            <CardDescription>Choose your preferred weight display form
handleSignOut function · typescript · L34-L37 (4 LOC)
src/app/(app)/settings/page.tsx
  async function handleSignOut() {
    await supabase.auth.signOut();
    router.push('/login');
  }
ensureSuggestionsLine function · typescript · L24-L31 (8 LOC)
src/app/(app)/trainer/page.tsx
function ensureSuggestionsLine(content: string, suggestionsLine: string): string {
  const trimmed = content.trim();
  if (!trimmed) return `Here is a draft template. Want any changes?\n${suggestionsLine}`;
  const lines = trimmed.split('\n');
  const lastLine = lines[lines.length - 1]?.trim() || '';
  if (lastLine.startsWith('suggestions:')) return trimmed;
  return `${trimmed}\n${suggestionsLine}`;
}
TrainerPage function · typescript · L112-L118 (7 LOC)
src/app/(app)/trainer/page.tsx
export default function TrainerPage() {
  return (
    <Suspense fallback={<div className="flex items-center justify-center min-h-dvh text-muted-foreground">Loading...</div>}>
      <TrainerContent />
    </Suspense>
  );
}
loadContext function · typescript · L176-L313 (138 LOC)
src/app/(app)/trainer/page.tsx
    async function loadContext() {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return;

      const ctx: WorkoutContext = {
        unitSystem,
        recentWorkouts: [],
        personalRecords: [],
        weeklyStats: null,
      };

      // Include trainer profile in context if available
      if (trainerProfile) {
        ctx.trainerProfile = {
          experienceLevel: trainerProfile.experienceLevel,
          trainingFrequency: trainerProfile.trainingFrequency || undefined,
          sessionDuration: trainerProfile.sessionDuration || undefined,
          goals: trainerProfile.goals,
          gymAccess: trainerProfile.gymAccess || undefined,
          availableEquipment: trainerProfile.availableEquipment.length > 0 ? trainerProfile.availableEquipment : undefined,
          favoriteExercises: trainerProfile.favoriteExercises.length > 0 ? trainerProfile.favoriteExercises : undefined,
          dislikedOrAvoidedExercises: trainerProfile.disli
startProfileSetup function · typescript · L327-L334 (8 LOC)
src/app/(app)/trainer/page.tsx
  function startProfileSetup() {
    setProfileMode(true);
    createConversation();
    setTimeout(() => {
      addMessage('assistant', "Let's get to know your training style so I can personalize things for you! How would you describe your experience level?\nsuggestions:Beginner|Intermediate|Advanced");
    }, 0);
    inputRef.current?.focus();
  }
handleFileUpload function · typescript · L336-L338 (3 LOC)
src/app/(app)/trainer/page.tsx
  function handleFileUpload(content: string, filename: string) {
    handleSend(`[file: ${filename}]\n\n${content}`);
  }
handleSend function · typescript · L340-L482 (143 LOC)
src/app/(app)/trainer/page.tsx
  async function handleSend(text?: string) {
    const messageText = text || input.trim();
    if (!messageText || isLoading || !context) return;

    // Auto-detect profile update requests
    if (!profileMode && /update.*profile|edit.*profile|change.*profile|modify.*profile|redo.*profile|set up.*profile|setup.*profile/i.test(messageText)) {
      startProfileSetup();
      return;
    }

    setInput('');
    addMessage('user', messageText);
    setIsLoading(true);

    const assistantId = addMessage('assistant', '');

    try {
      const chatMessages = [
        ...messages.map((m) => ({ role: m.role, content: m.content })),
        { role: 'user' as const, content: messageText },
      ];

      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages: chatMessages,
          context,
          ...(profileMode && { mode: 'profile-setup' }),
        }),
      });

Repobility · MCP-ready · https://repobility.com
handleConfirmImport function · typescript · L495-L521 (27 LOC)
src/app/(app)/trainer/page.tsx
  async function handleConfirmImport(messageId: string, importData: ImportData) {
    setImportingMessageId(messageId);
    try {
      const response = await fetch('/api/import', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'import',
          workouts: importData.workouts,
          unitSystem: context?.unitSystem || 'imperial',
        }),
      });

      if (!response.ok) {
        throw new Error('Import failed');
      }

      const result = await response.json();
      updateImportStatus(messageId, 'imported');
      addMessage('assistant', `Imported ${result.imported} workout${result.imported !== 1 ? 's' : ''} with ${result.summary.totalSets} sets. Check your training log to see them!`);
    } catch {
      updateImportStatus(messageId, 'pending');
      addMessage('assistant', 'Sorry, the import failed. Please try again.');
    } finally {
      setImportingMessageId(null);
    }
  }
handleCancelImport function · typescript · L523-L526 (4 LOC)
src/app/(app)/trainer/page.tsx
  function handleCancelImport(messageId: string) {
    updateImportStatus(messageId, 'cancelled');
    addMessage('assistant', 'Import cancelled. Let me know if you want to try again with different data.');
  }
handleConfirmTemplate function · typescript · L528-L556 (29 LOC)
src/app/(app)/trainer/page.tsx
  async function handleConfirmTemplate(messageId: string, templateData: TemplateData) {
    setSavingTemplateId(messageId);
    try {
      const response = await fetch('/api/templates', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: templateData.name,
          exercises: templateData.exercises,
        }),
      });

      if (!response.ok) {
        throw new Error('Failed to save template');
      }

      const result = await response.json();
      updateTemplateStatus(messageId, 'saved');
      const unmatchedMsg = result.unmatched && result.unmatched.length > 0
        ? ` (${result.unmatched.length} exercise${result.unmatched.length !== 1 ? 's' : ''} couldn't be matched: ${result.unmatched.join(', ')})`
        : '';
      addMessage('assistant', `Saved "${result.name}" with ${result.exerciseCount} exercises! You can find it in your Saved Templates on the home screen.${unmatchedMsg}`);
    } catc
handleCancelTemplate function · typescript · L558-L561 (4 LOC)
src/app/(app)/trainer/page.tsx
  function handleCancelTemplate(messageId: string) {
    updateTemplateStatus(messageId, 'cancelled');
    addMessage('assistant', 'No problem! Let me know if you want me to create a different template.');
  }
buildWorkoutUploadSnapshot function · typescript · L66-L91 (26 LOC)
src/app/(app)/workout/[id]/page.tsx
function buildWorkoutUploadSnapshot(state: ActiveWorkoutState): WorkoutUploadSnapshot | null {
  if (!state.workoutId || !state.startTime) return null;
  return {
    workoutId: state.workoutId,
    workoutName: state.workoutName,
    startTime: state.startTime,
    templateId: state.templateId,
    exercises: state.exercises.map((exercise) => ({
      exerciseId: exercise.exerciseId,
      logMode: exercise.logMode,
      sets: exercise.sets.map((set) => ({
        setNumber: set.setNumber,
        weight: set.weight,
        reps: set.reps,
        leftWeight: set.leftWeight,
        leftReps: set.leftReps,
        rightWeight: set.rightWeight,
        rightReps: set.rightReps,
        time: set.time,
        distance: set.distance,
        isWarmup: set.isWarmup,
        isCompleted: set.isCompleted,
      })),
    })),
  };
}
normalizePreviousPerformanceRow function · typescript · L128-L141 (14 LOC)
src/app/(app)/workout/[id]/page.tsx
function normalizePreviousPerformanceRow(row: PreviousPerformanceRow): PerformanceSet | null {
  const splitWeight = Math.max(row.left_weight ?? 0, row.right_weight ?? 0);
  const splitReps = Math.max(row.left_reps ?? 0, row.right_reps ?? 0);
  const weight = row.weight ?? (row.is_split_lr ? splitWeight : 0);
  const reps = row.reps ?? (row.is_split_lr ? splitReps : 0);

  if (weight <= 0 && reps <= 0) return null;

  return {
    weight,
    reps,
    setNumber: row.set_number || undefined,
  };
}
getPreviousSetForNumber function · typescript · L143-L145 (3 LOC)
src/app/(app)/workout/[id]/page.tsx
function getPreviousSetForNumber(sets: PerformanceSet[], setNumber: number) {
  return sets.find((s) => s.setNumber === setNumber) ?? sets[setNumber - 1];
}
prefillEmptySetsWithPreviousPerformance function · typescript · L147-L178 (32 LOC)
src/app/(app)/workout/[id]/page.tsx
function prefillEmptySetsWithPreviousPerformance(
  exerciseId: string,
  previousSets: PerformanceSet[],
) {
  const state = useActiveWorkoutStore.getState();
  const exerciseIndex = state.exercises.findIndex((exercise) => exercise.exerciseId === exerciseId);
  if (exerciseIndex === -1) return;

  const exercise = state.exercises[exerciseIndex];
  for (let setIndex = 0; setIndex < exercise.sets.length; setIndex += 1) {
    const currentSet = exercise.sets[setIndex];
    if (currentSet.isCompleted || currentSet.timestamp) continue;

    const previousSet = getPreviousSetForNumber(previousSets, currentSet.setNumber);
    if (!previousSet) continue;

    const nextSetData: Partial<ActiveSet> = {};
    if (currentSet.weight === null) nextSetData.weight = previousSet.weight;
    if (currentSet.reps === null) nextSetData.reps = previousSet.reps;

    if (exercise.logMode === 'split_lr') {
      if (currentSet.leftWeight === null) nextSetData.leftWeight = previousSet.weight;
      if (curren
All rows above produced by Repobility · https://repobility.com
WarmupToggle function · typescript · L180-L214 (35 LOC)
src/app/(app)/workout/[id]/page.tsx
function WarmupToggle({ isWarmup, setNumber, onToggle }: { isWarmup: boolean; setNumber: number; onToggle: () => void }) {
  const pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const didLongPress = useRef(false);

  function startPress() {
    didLongPress.current = false;
    pressTimer.current = setTimeout(() => {
      didLongPress.current = true;
      onToggle();
      if (navigator.vibrate) navigator.vibrate(40);
    }, 500);
  }

  function cancelPress() {
    if (pressTimer.current) {
      clearTimeout(pressTimer.current);
      pressTimer.current = null;
    }
  }

  return (
    <Button variant="ghost"
      className={`w-10 h-10 flex items-center justify-center rounded-lg text-sm font-medium select-none touch-none ${
        isWarmup ? 'bg-primary/10 text-primary' : 'text-muted-foreground'
      }`}
      onPointerDown={startPress}
      onPointerUp={cancelPress}
      onPointerLeave={cancelPress}
      onPointerCancel={cancelPress}
      title="Hold to
startPress function · typescript · L184-L191 (8 LOC)
src/app/(app)/workout/[id]/page.tsx
  function startPress() {
    didLongPress.current = false;
    pressTimer.current = setTimeout(() => {
      didLongPress.current = true;
      onToggle();
      if (navigator.vibrate) navigator.vibrate(40);
    }, 500);
  }
cancelPress function · typescript · L193-L198 (6 LOC)
src/app/(app)/workout/[id]/page.tsx
  function cancelPress() {
    if (pressTimer.current) {
      clearTimeout(pressTimer.current);
      pressTimer.current = null;
    }
  }
‹ prevpage 2 / 6next ›