Initial commit for Start9 packaging

This commit is contained in:
MacPro
2026-02-28 09:27:26 -06:00
commit 1b64c45c52
124 changed files with 15671 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { getCurrentUser } from "@/lib/auth";
import { prisma, setCaloriesBurned, getCaloriesBurnedBulk } from "@/lib/prisma";
// Schema now supports creating empty workouts (just date) or with sets
const createWorkoutSchema = z.object({
name: z.string().optional(),
notes: z.string().optional(),
durationMinutes: z.number().int().positive().optional(),
difficulty: z.number().int().min(1).max(10).optional(),
caloriesBurned: z.number().int().positive().optional(),
date: z.string().optional(), // ISO date string or date-only string
sets: z
.array(
z.object({
exerciseId: z.string(),
setNumber: z.number().int().positive(),
reps: z.number().int().positive().optional(),
weight: z.number().positive().optional(),
weightUnit: z.string().default("lbs"),
rpe: z.number().int().min(1).max(10).optional(),
durationSeconds: z.number().int().positive().optional(),
distance: z.number().positive().optional(),
distanceUnit: z.string().optional(),
calories: z.number().int().positive().optional(),
notes: z.string().optional(),
})
)
.optional()
.default([]),
});
// GET: List workouts with search/date filters
export async function GET(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
const dateFrom = searchParams.get("dateFrom");
const dateTo = searchParams.get("dateTo");
const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 100);
const offset = parseInt(searchParams.get("offset") || "0");
const where: any = {
userId: user.id,
};
if (query) {
where.name = {
contains: query,
};
}
if (dateFrom || dateTo) {
where.date = {};
if (dateFrom) {
where.date.gte = new Date(dateFrom);
}
if (dateTo) {
const toDate = new Date(dateTo);
toDate.setHours(23, 59, 59, 999);
where.date.lte = toDate;
}
}
const [workouts, total] = await Promise.all([
prisma.workout.findMany({
where,
include: {
setLogs: {
include: {
exercise: true,
},
orderBy: {
setNumber: "asc",
},
},
},
orderBy: {
date: "desc",
},
take: limit,
skip: offset,
}),
prisma.workout.count({ where }),
]);
// Supplement with caloriesBurned from raw SQL
const ids = workouts.map((w) => w.id);
const caloriesMap = await getCaloriesBurnedBulk(ids);
const enriched = workouts.map((w) => ({
...w,
caloriesBurned: caloriesMap[w.id] ?? null,
}));
return NextResponse.json({
data: enriched,
meta: {
total,
limit,
offset,
hasMore: offset + limit < total,
},
});
} catch (error) {
console.error("Failed to fetch workouts:", error);
return NextResponse.json(
{ error: "Failed to fetch workouts" },
{ status: 500 }
);
}
}
// POST: Create workout (can be empty or with sets)
export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const validated = createWorkoutSchema.parse(body);
const workoutDate = validated.date ? new Date(validated.date) : new Date();
// Extract caloriesBurned — handled via raw SQL after creation
const caloriesValue = validated.caloriesBurned;
const createData: any = {
userId: user.id,
name: validated.name || null,
notes: validated.notes,
durationMinutes: validated.durationMinutes,
difficulty: validated.difficulty,
// caloriesBurned handled separately via raw SQL
date: workoutDate,
setLogs:
validated.sets.length > 0
? {
create: validated.sets.map((set) => ({
exerciseId: set.exerciseId,
setNumber: set.setNumber,
reps: set.reps,
weight: set.weight,
weightUnit: set.weightUnit,
rpe: set.rpe,
durationSeconds: set.durationSeconds,
distance: set.distance,
distanceUnit: set.distanceUnit,
calories: set.calories,
notes: set.notes,
} as any)),
}
: undefined,
};
const includeOpts = {
setLogs: {
include: { exercise: true },
orderBy: { setNumber: "asc" as const },
},
};
const workout = await prisma.workout.create({ data: createData, include: includeOpts });
// Set caloriesBurned via raw SQL
if (caloriesValue !== undefined) {
await setCaloriesBurned(workout.id, caloriesValue);
}
return NextResponse.json({ ...workout, caloriesBurned: caloriesValue ?? null }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.errors },
{ status: 400 }
);
}
console.error("Failed to create workout:", error);
return NextResponse.json(
{ error: "Failed to create workout" },
{ status: 500 }
);
}
}