feat: initial project scaffold with Watch companion app

This commit is contained in:
2026-08-04 17:28:06 +02:00
parent adbb4f01c5
commit 43b6467efb
65 changed files with 6361 additions and 166 deletions
+177
View File
@@ -0,0 +1,177 @@
import { db } from "./client";
import { workouts, sets, laps, templates } from "./schema";
import { eq, desc, sql } from "drizzle-orm";
import type { WorkoutSet, LapData } from "@types/workout";
// ─── Templates ───────────────────────────────────────────────
export async function saveTemplate(name: string, poolLength: number, setsData: WorkoutSet[]) {
const id = crypto.randomUUID();
await db.insert(templates).values({
id,
name,
poolLength,
jsonData: JSON.stringify(setsData),
createdAt: new Date().toISOString(),
});
return id;
}
export async function getTemplates() {
return db.select().from(templates).orderBy(desc(templates.createdAt));
}
export async function deleteTemplate(id: string) {
await db.delete(templates).where(eq(templates.id, id));
}
export async function toggleTemplateFavorite(id: string, isFavorite: boolean) {
await db.update(templates).set({ isFavorite }).where(eq(templates.id, id));
}
// ─── Workouts ────────────────────────────────────────────────
export async function saveWorkout(
name: string | null,
poolLength: number,
setsData: WorkoutSet[],
templateId?: string
) {
const workoutId = crypto.randomUUID();
const totalDistance = setsData.reduce((acc, s) => acc + s.distancePerRep * s.repCount, 0);
await db.insert(workouts).values({
id: workoutId,
name,
templateId: templateId ?? null,
poolLength,
startedAt: new Date().toISOString(),
totalDistance,
source: "manual",
});
for (let i = 0; i < setsData.length; i++) {
const s = setsData[i];
await db.insert(sets).values({
id: crypto.randomUUID(),
workoutId,
position: i + 1,
stroke: s.stroke,
distancePerRep: s.distancePerRep,
repCount: s.repCount,
restType: s.restType,
restDuration: s.restDuration,
targetTime: s.targetTime ?? null,
});
}
return workoutId;
}
export async function finishWorkout(
workoutId: string,
duration: number,
calories: number,
avgHeartRate: number
) {
await db
.update(workouts)
.set({
finishedAt: new Date().toISOString(),
totalDuration: duration,
calories,
avgHeartRate,
})
.where(eq(workouts.id, workoutId));
}
export async function getWorkoutWithSets(workoutId: string) {
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
if (!workout) return null;
const workoutSets = await db.select().from(sets).where(eq(sets.workoutId, workoutId));
const workoutLaps = await db.select().from(laps).where(eq(laps.workoutId, workoutId));
return { ...workout, sets: workoutSets, laps: workoutLaps };
}
export async function getAllWorkouts() {
return db.select().from(workouts).orderBy(desc(workouts.startedAt));
}
export async function deleteWorkout(id: string) {
await db.delete(laps).where(eq(laps.workoutId, id));
await db.delete(sets).where(eq(sets.workoutId, id));
await db.delete(workouts).where(eq(workouts.id, id));
}
// ─── Laps ────────────────────────────────────────────────────
export async function saveLap(workoutId: string, lap: LapData) {
const swolf =
lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null;
await db.insert(laps).values({
id: crypto.randomUUID(),
workoutId,
setPosition: lap.setPosition,
repNumber: lap.repNumber,
distance: lap.distance,
duration: lap.duration,
strokeCount: lap.strokeCount ?? null,
heartRate: lap.heartRate ?? null,
swolf,
});
}
export async function saveLaps(workoutId: string, lapsData: LapData[]) {
for (const lap of lapsData) {
await saveLap(workoutId, lap);
}
}
export async function getWorkoutLaps(workoutId: string) {
return db.select().from(laps).where(eq(laps.workoutId, workoutId));
}
// ─── Stats ───────────────────────────────────────────────────
export async function getWeeklyStats() {
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return db
.select({
count: sql<number>`count(*)`,
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
avgCalories: sql<number>`coalesce(avg(${workouts.calories}), 0)`,
})
.from(workouts)
.where(sql`${workouts.startedAt} >= ${weekAgo.toISOString()}`)
.get();
}
export async function getPersonalBests() {
return db
.select({
distance: laps.distance,
bestTime: sql<number>`min(${laps.duration})`,
bestSwolf: sql<number>`min(${laps.swolf})`,
})
.from(laps)
.groupBy(laps.distance)
.having(sql`${laps.distance} > 0`);
}
export async function getTotalStats() {
return db
.select({
totalWorkouts: sql<number>`count(*)`,
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
})
.from(workouts)
.where(sql`${workouts.finishedAt} IS NOT NULL`)
.get();
}