Function bodies 262 total
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,
{
cnormalizeText 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}`);
rRepobility'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 = responsefindUserByEmail 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('dupPOST 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 workMethodology: 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}"
${exerciseCounStarterTemplates 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);
returnresolveExercises 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),
}));
setResolvedhandleStartFromTemplate 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 || [],
}))
);
seselectBalancedExercises 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 sloRepobility · 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,
weekWocloseTour 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 ›