|
1 | 1 | const FRONTEND_ORIGIN = "https://tools.mathspp.com"; |
2 | 2 | const WORKOUTS_KEY = "user:me:workouts"; |
| 3 | +const WORKOUT_LOGS_PREFIX = "user:me:workoutLogs:"; |
3 | 4 |
|
4 | 5 | function corsHeaders() { |
5 | 6 | return { |
@@ -51,6 +52,17 @@ async function saveWorkouts(env, workouts) { |
51 | 52 | await env.WORKOUTS.put(WORKOUTS_KEY, JSON.stringify(workouts)); |
52 | 53 | } |
53 | 54 |
|
| 55 | +async function getWorkoutLogs(env, workoutId) { |
| 56 | + const logs = await env.WORKOUTS.get(`${WORKOUT_LOGS_PREFIX}${workoutId}`, "json"); |
| 57 | + const parsed = Array.isArray(logs) ? logs : []; |
| 58 | + return parsed.sort((a, b) => new Date(b.finishedAt) - new Date(a.finishedAt)); |
| 59 | +} |
| 60 | + |
| 61 | +async function saveWorkoutLogs(env, workoutId, logs) { |
| 62 | + const sorted = [...logs].sort((a, b) => new Date(b.finishedAt) - new Date(a.finishedAt)); |
| 63 | + await env.WORKOUTS.put(`${WORKOUT_LOGS_PREFIX}${workoutId}`, JSON.stringify(sorted)); |
| 64 | +} |
| 65 | + |
54 | 66 | function normalizeExerciseBlock(block) { |
55 | 67 | if (!block || typeof block !== "object") { |
56 | 68 | return null; |
@@ -92,9 +104,70 @@ function findWorkout(workouts, id) { |
92 | 104 | return workouts.find((workout) => workout.id === id); |
93 | 105 | } |
94 | 106 |
|
| 107 | +function normalizeSet(set, setIndex) { |
| 108 | + const weight = Number.isFinite(Number(set?.weight)) ? Number(set.weight) : null; |
| 109 | + const reps = Number.isFinite(Number(set?.reps)) ? Number(set.reps) : null; |
| 110 | + const rir = Number.isFinite(Number(set?.rir)) ? Number(set.rir) : null; |
| 111 | + const notes = typeof set?.notes === "string" ? set.notes.trim() : ""; |
| 112 | + |
| 113 | + return { |
| 114 | + setIndex, |
| 115 | + weight, |
| 116 | + reps, |
| 117 | + rir, |
| 118 | + notes, |
| 119 | + }; |
| 120 | +} |
| 121 | + |
| 122 | +function normalizeExerciseLog(block, payloadExercise) { |
| 123 | + const sets = []; |
| 124 | + const providedSets = Array.isArray(payloadExercise?.sets) ? payloadExercise.sets : []; |
| 125 | + |
| 126 | + for (let i = 0; i < block.sets; i += 1) { |
| 127 | + sets.push(normalizeSet(providedSets[i] || {}, i)); |
| 128 | + } |
| 129 | + |
| 130 | + const progressionNotes = |
| 131 | + typeof payloadExercise?.progressionNotes === "string" ? payloadExercise.progressionNotes.trim() : ""; |
| 132 | + |
| 133 | + return { |
| 134 | + blockId: block.id, |
| 135 | + blockName: block.name, |
| 136 | + sets, |
| 137 | + progressionNotes, |
| 138 | + }; |
| 139 | +} |
| 140 | + |
| 141 | +function buildLogEntry(template, payload) { |
| 142 | + const startedAt = typeof payload?.startedAt === "string" ? payload.startedAt : null; |
| 143 | + const finishedAt = typeof payload?.finishedAt === "string" ? payload.finishedAt : null; |
| 144 | + const overallNotes = typeof payload?.overallNotes === "string" ? payload.overallNotes.trim() : ""; |
| 145 | + |
| 146 | + const payloadExercises = Array.isArray(payload?.exercises) ? payload.exercises : []; |
| 147 | + const exerciseMap = new Map(payloadExercises.map((ex) => [ex?.blockId, ex])); |
| 148 | + |
| 149 | + const exercises = template.exerciseBlocks.map((block) => { |
| 150 | + const payloadExercise = exerciseMap.get(block.id) || {}; |
| 151 | + return normalizeExerciseLog(block, payloadExercise); |
| 152 | + }); |
| 153 | + |
| 154 | + const completedAt = finishedAt || new Date().toISOString(); |
| 155 | + |
| 156 | + return { |
| 157 | + id: crypto.randomUUID(), |
| 158 | + workoutTemplateId: template.id, |
| 159 | + templateName: template.name, |
| 160 | + startedAt: startedAt || completedAt, |
| 161 | + finishedAt: completedAt, |
| 162 | + overallNotes, |
| 163 | + exercises, |
| 164 | + }; |
| 165 | +} |
| 166 | + |
95 | 167 | export default { |
96 | 168 | async fetch(request, env) { |
97 | 169 | const url = new URL(request.url); |
| 170 | + const pathParts = url.pathname.replace(/^\/+/, "").split("/"); |
98 | 171 |
|
99 | 172 | if (request.method === "OPTIONS") { |
100 | 173 | return handleOptions(); |
@@ -145,20 +218,41 @@ export default { |
145 | 218 | return jsonResponse(workout, 201); |
146 | 219 | } |
147 | 220 |
|
148 | | - if (url.pathname.startsWith("/api/workouts/")) { |
149 | | - const id = decodeURIComponent(url.pathname.replace("/api/workouts/", "")); |
150 | | - |
151 | | - if (!id) { |
152 | | - return jsonResponse({ error: "Workout id is required" }, 400); |
153 | | - } |
154 | | - |
| 221 | + if (pathParts[0] === "api" && pathParts[1] === "workouts" && pathParts[2]) { |
| 222 | + const id = decodeURIComponent(pathParts[2]); |
155 | 223 | const workouts = await getWorkouts(env); |
156 | 224 | const existing = findWorkout(workouts, id); |
157 | 225 |
|
158 | 226 | if (!existing) { |
159 | 227 | return jsonResponse({ error: "Workout not found" }, 404); |
160 | 228 | } |
161 | 229 |
|
| 230 | + if (pathParts[3] === "logs") { |
| 231 | + if (request.method === "GET" && pathParts[4] === "latest") { |
| 232 | + const logs = await getWorkoutLogs(env, id); |
| 233 | + const latest = logs[0] || null; |
| 234 | + return jsonResponse({ log: latest }); |
| 235 | + } |
| 236 | + |
| 237 | + if (request.method === "GET") { |
| 238 | + const logs = await getWorkoutLogs(env, id); |
| 239 | + return jsonResponse({ logs }); |
| 240 | + } |
| 241 | + |
| 242 | + if (request.method === "POST") { |
| 243 | + const body = await readJson(request); |
| 244 | + if (body.error) { |
| 245 | + return jsonResponse({ error: body.error }, 400); |
| 246 | + } |
| 247 | + |
| 248 | + const logEntry = buildLogEntry(existing, body); |
| 249 | + const logs = await getWorkoutLogs(env, id); |
| 250 | + logs.push(logEntry); |
| 251 | + await saveWorkoutLogs(env, id, logs); |
| 252 | + return jsonResponse(logEntry, 201); |
| 253 | + } |
| 254 | + } |
| 255 | + |
162 | 256 | if (request.method === "GET") { |
163 | 257 | return jsonResponse(existing); |
164 | 258 | } |
|
0 commit comments