← back to keeper92__liftlog

Function bodies 262 total

All specs Real LLM only Function bodies
middleware function · typescript · L4-L66 (63 LOC)
middleware.ts
export async function middleware(request: NextRequest) {
  const isAuthRoute = request.nextUrl.pathname.startsWith('/login') ||
    request.nextUrl.pathname.startsWith('/register');
  const isAppRoute = request.nextUrl.pathname.startsWith('/dashboard') ||
    request.nextUrl.pathname.startsWith('/workout') ||
    request.nextUrl.pathname.startsWith('/exercises') ||
    request.nextUrl.pathname.startsWith('/history') ||
    request.nextUrl.pathname.startsWith('/progress');

  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
  const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

  if (!supabaseUrl || !supabaseAnonKey) {
    if (isAuthRoute || isAppRoute) {
      const url = request.nextUrl.clone();
      url.pathname = '/';
      return NextResponse.redirect(url);
    }

    return NextResponse.next({ request });
  }

  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    supabaseUrl,
    supabaseAnonKey,
    {
      c
normalizeText function · typescript · L254-L256 (3 LOC)
src/app/api/chat/route.ts
function normalizeText(value: unknown): string {
  return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
}
countKeywordHits function · typescript · L258-L261 (4 LOC)
src/app/api/chat/route.ts
function countKeywordHits(text: string, keywords: string[]): number {
  const normalized = text.toLowerCase();
  return keywords.reduce((score, keyword) => score + (normalized.includes(keyword) ? 1 : 0), 0);
}
scoreExerciseNameForSplit function · typescript · L303-L313 (11 LOC)
src/app/api/chat/route.ts
function scoreExerciseNameForSplit(exerciseName: string, split: TemplateSplit): number {
  if (split === 'full') {
    const upperScore = countKeywordHits(exerciseName, UPPER_EXERCISE_KEYWORDS);
    const lowerScore = countKeywordHits(exerciseName, LOWER_EXERCISE_KEYWORDS);
    return Math.max(1, upperScore + lowerScore);
  }

  const name = exerciseName.toLowerCase();
  const keywords = split === 'upper' ? UPPER_EXERCISE_KEYWORDS : LOWER_EXERCISE_KEYWORDS;
  return keywords.reduce((score, keyword) => score + (name.includes(keyword) ? 1 : 0), 0);
}
inferTemplateSplitFromExercises function · typescript · L315-L328 (14 LOC)
src/app/api/chat/route.ts
function inferTemplateSplitFromExercises(exercises: string[]): TemplateSplit | null {
  if (exercises.length === 0) return null;
  let upper = 0;
  let lower = 0;
  for (const exercise of exercises) {
    upper += countKeywordHits(exercise, UPPER_EXERCISE_KEYWORDS);
    lower += countKeywordHits(exercise, LOWER_EXERCISE_KEYWORDS);
  }

  if (upper === 0 && lower === 0) return null;
  if (upper >= lower + 2) return 'upper';
  if (lower >= upper + 2) return 'lower';
  return null;
}
inferTemplateSplit function · typescript · L330-L341 (12 LOC)
src/app/api/chat/route.ts
function inferTemplateSplit(name: string, messages: ChatRequestMessage[], providedExercises: string[]): TemplateSplit {
  const recentText = messages.slice(-4).map((m) => m.content.toLowerCase()).join(' ');
  const combined = `${name.toLowerCase()} ${recentText}`;
  if (/\b(lower body|lower|leg day|legs)\b/.test(combined)) return 'lower';
  if (/\b(upper body|upper|push day|pull day|push\/pull)\b/.test(combined)) return 'upper';
  if (/\b(full body|full)\b/.test(combined)) return 'full';

  const inferredFromExercises = inferTemplateSplitFromExercises(providedExercises);
  if (inferredFromExercises) return inferredFromExercises;

  return 'full';
}
getTopRecentExercises function · typescript · L343-L359 (17 LOC)
src/app/api/chat/route.ts
function getTopRecentExercises(recentWorkouts: WorkoutContext['recentWorkouts']): string[] {
  const counts = new Map<string, number>();
  for (const workout of recentWorkouts) {
    for (const exerciseName of workout.exercises) {
      const normalized = normalizeText(exerciseName);
      if (!normalized) continue;
      counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
    }
  }

  return Array.from(counts.entries())
    .sort((a, b) => {
      if (b[1] !== a[1]) return b[1] - a[1];
      return a[0].localeCompare(b[0]);
    })
    .map(([name]) => name);
}
Repobility · code-quality intelligence platform · https://repobility.com
getRecentExercisesBySplit function · typescript · L361-L366 (6 LOC)
src/app/api/chat/route.ts
function getRecentExercisesBySplit(recentWorkouts: WorkoutContext['recentWorkouts'], split: TemplateSplit): string[] {
  const ranked = getTopRecentExercises(recentWorkouts);
  if (split === 'full') return ranked;

  return ranked.filter((name) => scoreExerciseNameForSplit(name, split) > 0);
}
wantsNovelExercise function · typescript · L368-L371 (4 LOC)
src/app/api/chat/route.ts
function wantsNovelExercise(messages: ChatRequestMessage[]): boolean {
  const lastMessage = messages[messages.length - 1]?.content.toLowerCase() || '';
  return /\b(new exercise|new move|something new|different exercise|try new|different one|new one)\b/.test(lastMessage);
}
isAvoidedExercise function · typescript · L373-L376 (4 LOC)
src/app/api/chat/route.ts
function isAvoidedExercise(exerciseName: string, avoidedTerms: string[]): boolean {
  const normalized = exerciseName.toLowerCase();
  return avoidedTerms.some((term) => term.length >= 3 && normalized.includes(term));
}
pickBestCandidateForSlot function · typescript · L378-L400 (23 LOC)
src/app/api/chat/route.ts
function pickBestCandidateForSlot(
  slot: TemplateSlot,
  candidates: TemplateCandidate[],
  usedKeys: Set<string>,
  split: TemplateSplit,
): TemplateCandidate | null {
  let best: TemplateCandidate | null = null;
  let bestScore = -Infinity;

  for (const candidate of candidates) {
    if (usedKeys.has(candidate.key)) continue;
    const slotHits = countKeywordHits(candidate.name, slot.keywords);
    if (slotHits <= 0) continue;
    const splitScore = scoreExerciseNameForSplit(candidate.name, split);
    const score = slotHits * 100 + splitScore * 10 - candidate.sourceRank * 8 - candidate.order * 0.001;
    if (score > bestScore) {
      best = candidate;
      bestScore = score;
    }
  }

  return best;
}
pickBestGeneralCandidate function · typescript · L402-L424 (23 LOC)
src/app/api/chat/route.ts
function pickBestGeneralCandidate(
  candidates: TemplateCandidate[],
  usedKeys: Set<string>,
  split: TemplateSplit,
  requireSplitMatch: boolean,
): TemplateCandidate | null {
  let best: TemplateCandidate | null = null;
  let bestScore = -Infinity;

  for (const candidate of candidates) {
    if (usedKeys.has(candidate.key)) continue;
    const splitScore = scoreExerciseNameForSplit(candidate.name, split);
    if (requireSplitMatch && split !== 'full' && splitScore <= 0) continue;

    const score = splitScore * 20 - candidate.sourceRank * 8 - candidate.order * 0.001;
    if (score > bestScore) {
      best = candidate;
      bestScore = score;
    }
  }

  return best;
}
normalizeTemplateToolInput function · typescript · L426-L557 (132 LOC)
src/app/api/chat/route.ts
function normalizeTemplateToolInput(input: unknown, context: WorkoutContext, messages: ChatRequestMessage[]): NormalizedTemplateData {
  const parsed = (input ?? {}) as TemplateToolInput;
  const providedName = normalizeText(parsed.name);
  const providedExercises = Array.isArray(parsed.exercises)
    ? parsed.exercises.map((exercise) => normalizeText(exercise?.name)).filter(Boolean)
    : [];
  const split = inferTemplateSplit(providedName, messages, providedExercises);
  const recentBySplit = getRecentExercisesBySplit(context.recentWorkouts, split);
  const novelRequest = wantsNovelExercise(messages);

  const fallback = TEMPLATE_SLOT_PLANS[split].map((slot) => slot.fallback);
  const globalFallback = [
    ...TEMPLATE_SLOT_PLANS.full.map((slot) => slot.fallback),
    ...TEMPLATE_SLOT_PLANS.upper.map((slot) => slot.fallback),
    ...TEMPLATE_SLOT_PLANS.lower.map((slot) => slot.fallback),
  ];

  const avoidedTerms = (context.trainerProfile?.dislikedOrAvoidedExercises || [])
    .map(
addCandidates function · typescript · L454-L475 (22 LOC)
src/app/api/chat/route.ts
  function addCandidates(names: string[], source: TemplateCandidateSource) {
    for (const rawName of names) {
      const name = normalizeText(rawName);
      if (!name) continue;
      if (isAvoidedExercise(name, avoidedTerms)) continue;

      const key = name.toLowerCase();
      const existing = candidatesMap.get(key);
      const candidate: TemplateCandidate = {
        name,
        key,
        source,
        sourceRank: sourceRanks[source],
        order,
      };
      order += 1;

      if (!existing || candidate.sourceRank < existing.sourceRank) {
        candidatesMap.set(key, candidate);
      }
    }
  }
buildProfileContextString function · typescript · L559-L569 (11 LOC)
src/app/api/chat/route.ts
function buildProfileContextString(profile: TrainerProfileData): string {
  const lines: string[] = [];
  lines.push(`- Experience: ${profile.experienceLevel}`);
  if (profile.trainingFrequency) lines.push(`- Frequency: ${profile.trainingFrequency}${profile.sessionDuration ? `, ${profile.sessionDuration} sessions` : ''}`);
  if (profile.goals.length > 0) lines.push(`- Goals: ${profile.goals.join(', ')}`);
  if (profile.gymAccess) lines.push(`- Gym: ${profile.gymAccess}${profile.availableEquipment && profile.availableEquipment.length > 0 ? ` (${profile.availableEquipment.join(', ')})` : ''}`);
  if (profile.favoriteExercises && profile.favoriteExercises.length > 0) lines.push(`- Favorite exercises: ${profile.favoriteExercises.join(', ')}`);
  if (profile.dislikedOrAvoidedExercises && profile.dislikedOrAvoidedExercises.length > 0) lines.push(`- Avoids: ${profile.dislikedOrAvoidedExercises.join(', ')}`);
  if (profile.additionalNotes) lines.push(`- Notes: ${profile.additionalNotes}`);
  r
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
POST function · typescript · L571-L807 (237 LOC)
src/app/api/chat/route.ts
export async function POST(request: Request) {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) {
    return new Response(
      JSON.stringify({ error: 'ANTHROPIC_API_KEY not configured' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }

  const { messages, context, mode } = (await request.json()) as {
    messages: ChatRequestMessage[];
    context: WorkoutContext;
    mode?: 'profile-setup' | 'chat';
  };

  // Profile setup mode — use profile-gathering system prompt + profile tool
  if (mode === 'profile-setup') {
    const response = await anthropic.messages.create({
      model: 'claude-haiku-4-5-20251001',
      max_tokens: 512,
      system: PROFILE_SETUP_SYSTEM_PROMPT,
      tools: [PROFILE_TOOL],
      messages: messages.map((m): MessageParam => ({ role: m.role, content: m.content })),
    });

    const toolUse = response.content.find((block): block is ToolUseBlock => block.type === 'tool_use');
    const textBlock = response
findUserByEmail function · typescript · L12-L29 (18 LOC)
src/app/api/demo/route.ts
async function findUserByEmail(supabase: SupabaseClient, email: string): Promise<AuthUserLite | null> {
  const target = email.toLowerCase();
  const perPage = 200;

  for (let page = 1; page <= 50; page += 1) {
    const { data, error } = await supabase.auth.admin.listUsers({ page, perPage });
    if (error) {
      throw new Error(error.message || 'Failed to list users');
    }

    const users = (data?.users || []) as AuthUserLite[];
    const match = users.find((u) => (u.email || '').toLowerCase() === target);
    if (match) return match;
    if (users.length < perPage) break;
  }

  return null;
}
getOrCreateDemoUserId function · typescript · L31-L81 (51 LOC)
src/app/api/demo/route.ts
async function getOrCreateDemoUserId(supabase: SupabaseClient): Promise<string> {
  const existingDemo = await findUserByEmail(supabase, DEMO_EMAIL);
  if (existingDemo) {
    const { error: updateError } = await supabase.auth.admin.updateUserById(existingDemo.id, {
      password: DEMO_PASSWORD,
      email_confirm: true,
    });
    if (updateError) {
      throw new Error(updateError.message || 'Failed to update existing demo user');
    }
    return existingDemo.id;
  }

  for (let attempt = 0; attempt < 2; attempt += 1) {
    const { data: newUser, error: createError } = await supabase.auth.admin.createUser({
      email: DEMO_EMAIL,
      password: DEMO_PASSWORD,
      email_confirm: true,
    });

    if (!createError && newUser.user) {
      return newUser.user.id;
    }

    const message = (createError?.message || '').toLowerCase();
    const duplicateUser =
      message.includes('already registered') ||
      message.includes('already exists') ||
      message.includes('dup
POST function · typescript · L83-L111 (29 LOC)
src/app/api/demo/route.ts
export async function POST() {
  try {
    const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
    const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;

    const supabase = createClient(supabaseUrl, serviceRoleKey, {
      auth: { autoRefreshToken: false, persistSession: false },
    });

    const userId = await getOrCreateDemoUserId(supabase);

    // Set demo user to imperial units
    await supabase
      .from('profiles')
      .update({ unit_system: 'imperial' })
      .eq('id', userId);

    return NextResponse.json({
      email: DEMO_EMAIL,
      password: DEMO_PASSWORD,
    });
  } catch (error) {
    const detail = error instanceof Error ? error.message : 'Unknown error';
    return NextResponse.json(
      { error: 'Failed to create demo user', detail },
      { status: 500 },
    );
  }
}
POST function · typescript · L5-L45 (41 LOC)
src/app/api/exercise-details/route.ts
export async function POST(req: Request) {
  try {
    const { name, category } = await req.json();

    if (!name || typeof name !== 'string') {
      return Response.json({ error: 'Exercise name is required.' }, { status: 400 });
    }

    const response = await anthropic.messages.create({
      model: 'claude-haiku-4-5-20251001',
      max_tokens: 1024,
      messages: [
        {
          role: 'user',
          content: `You are a fitness exercise database. Given the exercise "${name}"${category ? ` (category: ${category})` : ''}, return a JSON object with these fields:

- "instructions": array of 4-6 short step-by-step form instructions (strings)
- "secondaryMuscles": array of secondary muscle groups worked (use lowercase with underscores, e.g. "middle_back", "biceps", "triceps", "shoulders", "forearms", "hamstrings", "glutes", "calves", "abdominals", "quadriceps", "chest", "lats", "traps", "lower_back", "neck", "adductors", "abductors")
- "force": "push" or "pull" or "static" 
POST function · typescript · L39-L57 (19 LOC)
src/app/api/import/route.ts
export async function POST(request: Request) {
  const supabase = await createClient();

  // Check authentication
  const { data: { user }, error: authError } = await supabase.auth.getUser();
  if (authError || !user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json() as RequestBody;

  if (body.action === 'match') {
    return handleMatch(supabase, body.exerciseNames);
  } else if (body.action === 'import') {
    return handleImport(supabase, user.id, body.workouts, body.unitSystem);
  }

  return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
}
handleMatch function · typescript · L59-L65 (7 LOC)
src/app/api/import/route.ts
async function handleMatch(
  supabase: Awaited<ReturnType<typeof createClient>>,
  exerciseNames: string[]
): Promise<NextResponse> {
  const matches = await matchExercises(supabase, exerciseNames);
  return NextResponse.json({ matches });
}
handleImport function · typescript · L67-L191 (125 LOC)
src/app/api/import/route.ts
async function handleImport(
  supabase: Awaited<ReturnType<typeof createClient>>,
  userId: string,
  workouts: ImportWorkout[],
  unitSystem: 'metric' | 'imperial'
): Promise<NextResponse> {
  const importedWorkoutIds: string[] = [];
  let totalSets = 0;

  // First, collect all unique exercise names that need matching
  const exerciseNames = new Set<string>();
  for (const workout of workouts) {
    for (const exercise of workout.exercises) {
      if (!exercise.exerciseId) {
        exerciseNames.add(exercise.name);
      }
    }
  }

  // Match exercises if needed
  const exerciseIdMap = new Map<string, string>();
  if (exerciseNames.size > 0) {
    const matches = await matchExercises(supabase, Array.from(exerciseNames));
    for (const match of matches) {
      if (match.matchedExercise && match.confidence >= 0.5) {
        exerciseIdMap.set(match.inputName.toLowerCase(), match.matchedExercise.id);
      }
    }
  }

  // Add pre-matched exercise IDs
  for (const workout of work
Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
POST function · typescript · L15-L97 (83 LOC)
src/app/api/templates/route.ts
export async function POST(request: Request) {
  const supabase = await createClient();

  // Check authentication
  const { data: { user }, error: authError } = await supabase.auth.getUser();
  if (authError || !user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json() as CreateTemplateRequest;

  if (!body.name || !body.exercises || body.exercises.length === 0) {
    return NextResponse.json({ error: 'Name and exercises are required' }, { status: 400 });
  }

  // Resolve exercise names to IDs using fuzzy matching
  const exerciseNames = body.exercises.map(e => e.name);
  const matches = await matchExercises(supabase, exerciseNames);

  // Build the exercise ID map with confidence threshold
  const resolvedExercises: { exerciseId: string; name: string; defaultSets: number; orderIndex: number }[] = [];
  const unmatched: string[] = [];

  for (let i = 0; i < body.exercises.length; i++) {
    const match = matches[i];
   
POST function · typescript · L38-L95 (58 LOC)
src/app/api/training-notes/route.ts
export async function POST(request: Request) {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) {
    return new Response(
      JSON.stringify({ error: 'ANTHROPIC_API_KEY not configured' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }

  const { workoutName, exerciseCount, totalSets, comparisons, unitSystem, trainerProfile } =
    (await request.json()) as TrainingNotesRequest;

  const unit = unitSystem === 'imperial' ? 'lbs' : 'kg';

  let profileContext = '';
  if (trainerProfile) {
    const parts: string[] = [];
    parts.push(`Experience: ${trainerProfile.experienceLevel}`);
    if (trainerProfile.goals && trainerProfile.goals.length > 0) parts.push(`Goals: ${trainerProfile.goals.join(', ')}`);
    if (trainerProfile.additionalNotes) parts.push(`Notes: ${trainerProfile.additionalNotes}`);
    profileContext = `\nUser profile: ${parts.join('. ')}.\n`;
  }

  const userMessage = `Workout completed: "${workoutName}"
${exerciseCoun
StarterTemplates function · typescript · L24-L240 (217 LOC)
src/app/(app)/dashboard/_components/StarterTemplates.tsx
export default function StarterTemplates() {
  const router = useRouter();
  const supabase = createClient();
  const { startWorkout, addExerciseWithSets } = useActiveWorkoutStore();

  const [resolvedTemplates, setResolvedTemplates] = useState<ResolvedTemplate[]>([]);
  const [generatedExercises, setGeneratedExercises] = useState<ResolvedExercise[] | null>(null);
  const [showGenerateModal, setShowGenerateModal] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [loading, setLoading] = useState(true);

  // Resolve exercise IDs for pre-loaded templates on mount
  useEffect(() => {
    async function resolveExercises() {
      const allNames = PRELOADED_TEMPLATES.flatMap((t) => t.exercises.map((e) => e.name));
      const uniqueNames = [...new Set(allNames)];

      const { data } = await supabase
        .from('exercises')
        .select('id, name, category')
        .in('name', uniqueNames);

      if (!data) {
        setLoading(false);
        return
resolveExercises function · typescript · L37-L71 (35 LOC)
src/app/(app)/dashboard/_components/StarterTemplates.tsx
    async function resolveExercises() {
      const allNames = PRELOADED_TEMPLATES.flatMap((t) => t.exercises.map((e) => e.name));
      const uniqueNames = [...new Set(allNames)];

      const { data } = await supabase
        .from('exercises')
        .select('id, name, category')
        .in('name', uniqueNames);

      if (!data) {
        setLoading(false);
        return;
      }

      const nameToExercise = new Map(data.map((e) => [e.name, { id: e.id, category: e.category }]));

      const resolved = PRELOADED_TEMPLATES.map((template) => ({
        id: template.id,
        name: template.name,
        exercises: template.exercises
          .map((ex) => {
            const found = nameToExercise.get(ex.name);
            return {
              id: found?.id || '',
              name: ex.name,
              category: found?.category || 'other',
              defaultSets: ex.defaultSets,
            };
          })
          .filter((ex) => ex.id),
      }));

      setResolved
handleStartFromTemplate function · typescript · L75-L82 (8 LOC)
src/app/(app)/dashboard/_components/StarterTemplates.tsx
  function handleStartFromTemplate(template: ResolvedTemplate) {
    startWorkout(template.name);
    for (const ex of template.exercises) {
      addExerciseWithSets({ id: ex.id, name: ex.name, category: ex.category }, ex.defaultSets);
    }
    const state = useActiveWorkoutStore.getState();
    router.push(`/workout/${state.workoutId}`);
  }
handleGenerateTemplate function · typescript · L84-L138 (55 LOC)
src/app/(app)/dashboard/_components/StarterTemplates.tsx
  async function handleGenerateTemplate() {
    setGenerating(true);
    setShowGenerateModal(true);

    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      setGenerating(false);
      return;
    }

    // Query sets with exercise info, filtered by user's workouts
    const { data } = await supabase
      .from('sets')
      .select('exercise_id, exercises(id, name, category), workouts!inner(user_id)')
      .eq('workouts.user_id', user.id);

    if (!data || data.length === 0) {
      setGeneratedExercises([]);
      setGenerating(false);
      return;
    }

    // Count exercise frequencies
    const frequency = new Map<string, { id: string; name: string; category: string; count: number }>();
    for (const row of data) {
      const exerciseId = row.exercise_id;
      // exercises is returned as a single object (not array) due to FK relationship
      const exercise = row.exercises as unknown as { id: string; name: string; category: string 
handleStartGeneratedWorkout function · typescript · L140-L150 (11 LOC)
src/app/(app)/dashboard/_components/StarterTemplates.tsx
  function handleStartGeneratedWorkout() {
    if (!generatedExercises || generatedExercises.length === 0) return;

    startWorkout('Generated Workout');
    for (const ex of generatedExercises) {
      addExerciseWithSets({ id: ex.id, name: ex.name, category: ex.category }, ex.defaultSets);
    }
    setShowGenerateModal(false);
    const state = useActiveWorkoutStore.getState();
    router.push(`/workout/${state.workoutId}`);
  }
getRandomIndex function · typescript · L39-L45 (7 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
function getRandomIndex(length: number): number {
  if (length <= 1) return 0;

  const randomValues = new Uint32Array(1);
  globalThis.crypto.getRandomValues(randomValues);
  return randomValues[0] % length;
}
Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
pickRandomItem function · typescript · L47-L49 (3 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
function pickRandomItem<T>(items: T[]): T {
  return items[getRandomIndex(items.length)];
}
WorkoutBuilder function · typescript · L51-L393 (343 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
export default function WorkoutBuilder() {
  const router = useRouter();
  const supabase = createClient();
  const { startWorkout, addExerciseWithSets } = useActiveWorkoutStore();

  const [step, setStep] = useState<Step>('closed');
  const [selectedBodyParts, setSelectedBodyParts] = useState<string[]>([]);
  const [selectedEquipment, setSelectedEquipment] = useState<string[]>([]);
  const [exercises, setExercises] = useState<GeneratedExercise[]>([]);
  const [generating, setGenerating] = useState(false);
  const [replacingIndex, setReplacingIndex] = useState<number | null>(null);

  function toggleBodyPart(id: string) {
    setSelectedBodyParts((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }

  function toggleEquipment(id: string) {
    setSelectedEquipment((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }

  function handleOpenBuilder() {
    setStep('body_parts');
    setSelectedBodyParts([]);
   
toggleBodyPart function · typescript · L63-L67 (5 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function toggleBodyPart(id: string) {
    setSelectedBodyParts((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }
toggleEquipment function · typescript · L69-L73 (5 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function toggleEquipment(id: string) {
    setSelectedEquipment((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }
handleOpenBuilder function · typescript · L75-L80 (6 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function handleOpenBuilder() {
    setStep('body_parts');
    setSelectedBodyParts([]);
    setSelectedEquipment([]);
    setExercises([]);
  }
handleClose function · typescript · L82-L88 (7 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function handleClose() {
    setStep('closed');
    setSelectedBodyParts([]);
    setSelectedEquipment([]);
    setExercises([]);
    setReplacingIndex(null);
  }
generateWorkout function · typescript · L90-L125 (36 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  async function generateWorkout() {
    setGenerating(true);
    setStep('preview');

    // Get selected categories from equipment
    const categories = selectedEquipment.flatMap((eq) => {
      const option = EQUIPMENT_OPTIONS.find((o) => o.id === eq);
      return option?.categories || [];
    });

    // Query exercises matching criteria
    const { data } = await supabase
      .from('exercises')
      .select('id, name, category, primary_muscles')
      .in('category', categories)
      .overlaps('primary_muscles', selectedBodyParts);

    if (!data || data.length === 0) {
      setExercises([]);
      setGenerating(false);
      return;
    }

    // Pick 6 random exercises, prioritizing variety in muscle groups
    const selected = selectBalancedExercises(data, selectedBodyParts, 6);

    setExercises(
      selected.map((ex) => ({
        id: ex.id,
        name: ex.name,
        category: ex.category,
        primaryMuscles: ex.primary_muscles || [],
      }))
    );
    se
selectBalancedExercises function · typescript · L127-L188 (62 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function selectBalancedExercises(
    pool: { id: string; name: string; category: string; primary_muscles: string[] | null }[],
    targetMuscles: string[],
    count: number
  ) {
    const result: typeof pool = [];
    const usedIds = new Set<string>();
    const muscleCount = new Map<string, number>();

    // Initialize muscle counts
    targetMuscles.forEach((m) => muscleCount.set(m, 0));

    // Try to get at least one exercise per muscle group first
    for (const muscle of targetMuscles) {
      if (result.length >= count) break;

      const candidates = pool.filter(
        (ex) =>
          !usedIds.has(ex.id) &&
          ex.primary_muscles?.includes(muscle)
      );

      if (candidates.length > 0) {
        const picked = pickRandomItem(candidates);
        result.push(picked);
        usedIds.add(picked.id);
        picked.primary_muscles?.forEach((m) => {
          muscleCount.set(m, (muscleCount.get(m) || 0) + 1);
        });
      }
    }

    // Fill remaining slo
Repobility · code-quality intelligence platform · https://repobility.com
handleReplaceExercise function · typescript · L190-L228 (39 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  async function handleReplaceExercise(index: number) {
    setReplacingIndex(index);
    const exerciseToReplace = exercises[index];

    // Get selected categories from equipment
    const categories = selectedEquipment.flatMap((eq) => {
      const option = EQUIPMENT_OPTIONS.find((o) => o.id === eq);
      return option?.categories || [];
    });

    // Find similar exercises (same muscle group, same equipment types)
    const { data } = await supabase
      .from('exercises')
      .select('id, name, category, primary_muscles')
      .in('category', categories)
      .overlaps('primary_muscles', exerciseToReplace.primaryMuscles)
      .neq('id', exerciseToReplace.id)
      .limit(20);

    if (data && data.length > 0) {
      // Filter out already selected exercises
      const usedIds = new Set(exercises.map((e) => e.id));
      const candidates = data.filter((ex) => !usedIds.has(ex.id));

      if (candidates.length > 0) {
        const replacement = pickRandomItem(candidates);
handleStartWorkout function · typescript · L230-L240 (11 LOC)
src/app/(app)/dashboard/_components/WorkoutBuilder.tsx
  function handleStartWorkout() {
    if (exercises.length === 0) return;

    startWorkout('Custom Workout');
    for (const ex of exercises) {
      addExerciseWithSets({ id: ex.id, name: ex.name, category: ex.category }, 3);
    }
    handleClose();
    const state = useActiveWorkoutStore.getState();
    router.push(`/workout/${state.workoutId}`);
  }
load function · typescript · L103-L169 (67 LOC)
src/app/(app)/dashboard/page.tsx
    async function load() {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return;

      const { data: templatesData } = await supabase
        .from('workout_templates')
        .select('id, name, template_exercises(exercise_id, order_index, default_sets, exercises(name, category))')
        .eq('user_id', user.id)
        .order('updated_at', { ascending: false })
        .limit(10);
      if (templatesData) setTemplates(templatesData as unknown as TemplateSummary[]);

      // Get progress summary (streak + totals)
      const { data: summary } = await supabase.rpc('get_progress_summary', {
        user_uuid: user.id,
      });
      if (summary) {
        const s = summary as { weekWorkouts: number; currentStreak: number; longestStreak?: number; totalWorkouts: number };
        setStats({
          currentStreak: s.currentStreak,
          longestStreak: s.longestStreak ?? s.currentStreak,
          totalWorkouts: s.totalWorkouts,
          weekWo
closeTour function · typescript · L182-L185 (4 LOC)
src/app/(app)/dashboard/page.tsx
  function closeTour() {
    sessionStorage.removeItem(DEMO_TOUR_PENDING_KEY);
    setTourStage('idle');
  }
startTour function · typescript · L187-L190 (4 LOC)
src/app/(app)/dashboard/page.tsx
  function startTour() {
    sessionStorage.removeItem(DEMO_TOUR_PENDING_KEY);
    setTourStage('active');
  }
handleStartWorkout function · typescript · L192-L196 (5 LOC)
src/app/(app)/dashboard/page.tsx
  function handleStartWorkout() {
    startWorkout();
    const state = useActiveWorkoutStore.getState();
    router.push(`/workout/${state.workoutId}`);
  }
handleResumeWorkout function · typescript · L198-L200 (3 LOC)
src/app/(app)/dashboard/page.tsx
  function handleResumeWorkout() {
    router.push(`/workout/${workoutId}`);
  }
handleStartWorkoutFromOverlay function · typescript · L202-L207 (6 LOC)
src/app/(app)/dashboard/page.tsx
  function handleStartWorkoutFromOverlay() {
    setShowHistory(false);
    setHistoryInitialDateKey(null);
    setShowPRFeed(false);
    handleStartWorkout();
  }
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
formatWorkoutDate function · typescript · L209-L211 (3 LOC)
src/app/(app)/dashboard/page.tsx
  function formatWorkoutDate(date: string) {
    return new Date(date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
  }
toDateKey function · typescript · L213-L223 (11 LOC)
src/app/(app)/dashboard/page.tsx
  function toDateKey(value: string): string {
    const parsed = new Date(value);
    if (!Number.isNaN(parsed.getTime())) {
      const year = parsed.getFullYear();
      const month = String(parsed.getMonth() + 1).padStart(2, '0');
      const day = String(parsed.getDate()).padStart(2, '0');
      return `${year}-${month}-${day}`;
    }
    const [dateKey] = value.split('T');
    return dateKey;
  }
openHistory function · typescript · L225-L229 (5 LOC)
src/app/(app)/dashboard/page.tsx
  function openHistory(dateKey: string | null = null) {
    setShowPRFeed(false);
    setHistoryInitialDateKey(dateKey);
    setShowHistory(true);
  }
page 1 / 6next ›