feat: initial project scaffold with Watch companion app
This commit is contained in:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user