Initial commit for Start9 packaging
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Exercise name mapping - CSV shorthand to DB names
|
||||
const NAME_MAP: Record<string, string> = {
|
||||
"Ab Wheel": "Ab Wheel Rollout",
|
||||
"BB Upright Row": "Upright Row",
|
||||
"Ball Situp": "Exercise Ball Situp",
|
||||
"Bench": "Bench Press",
|
||||
"DB Lateral Raise": "Lateral Raise",
|
||||
"Dip": "Dips (Chest)",
|
||||
"Face Pull": "Face Pulls",
|
||||
"SA Lat Pulldown": "Lat Pulldown",
|
||||
"SL Calf Raise": "Calf Raise",
|
||||
"BB Row": "Barbell Row",
|
||||
"DB Row": "Dumbbell Row",
|
||||
"GHD": "Glute ham developer",
|
||||
"Hamstring DL": "Hamstring deadlift",
|
||||
"BB Curl": "Barbell Curl",
|
||||
"BB Hip Bridge": "Hip Thrust",
|
||||
"Cable Trap": "Rear delt",
|
||||
"Chinup (Narrow)": "Chinup",
|
||||
"Chinup Negatives": "Chinup",
|
||||
"Squat (Foot Elevated)": "Squat",
|
||||
"Ball Bicep Curl": "Dumbbell Curl",
|
||||
"KB Hip Flexor": "Hip Flexor",
|
||||
"Hamstring Deadlift": "Hamstring deadlift",
|
||||
"Shoulder Press": "Overhead Press",
|
||||
"CoC": "Captains of Crush",
|
||||
"Hex DL": "Hex Bar Deadlift",
|
||||
"KB Extension": "Kettlebell Leg Extension",
|
||||
"Ski": "SkiErg",
|
||||
};
|
||||
|
||||
interface ParsedSet {
|
||||
setNumber: number;
|
||||
weight?: number;
|
||||
weightUnit: string;
|
||||
reps?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface ParsedExercise {
|
||||
exerciseId: string;
|
||||
exerciseName: string;
|
||||
sets: ParsedSet[];
|
||||
}
|
||||
|
||||
interface ParsedWorkout {
|
||||
date: string;
|
||||
exercises: ParsedExercise[];
|
||||
}
|
||||
|
||||
interface ParseResponse {
|
||||
workouts: ParsedWorkout[];
|
||||
unmapped: string[];
|
||||
}
|
||||
|
||||
function parseCSV(content: string): Array<Record<string, string>> {
|
||||
const lines = content.trim().split("\n");
|
||||
if (lines.length === 0) return [];
|
||||
|
||||
// Parse header
|
||||
const header = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
const rows = [];
|
||||
|
||||
// Parse data rows
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
const values = line.split(",").map((v) => v.trim());
|
||||
const row: Record<string, string> = {};
|
||||
|
||||
header.forEach((col, idx) => {
|
||||
if (values[idx]) {
|
||||
row[col] = values[idx];
|
||||
}
|
||||
});
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function getVariationNote(originalName: string): string | null {
|
||||
if (originalName.includes("Narrow")) return "narrow";
|
||||
if (originalName.includes("Negatives")) return "negatives";
|
||||
if (originalName.includes("Foot Elevated")) return "foot elevated";
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveExerciseName(csvName: string): string {
|
||||
// Check if it's in the name map
|
||||
if (NAME_MAP[csvName]) {
|
||||
return NAME_MAP[csvName];
|
||||
}
|
||||
// Return as-is for direct lookup
|
||||
return csvName;
|
||||
}
|
||||
|
||||
// Parse dates like "1/27/2026" or "2026-01-27" into ISO date string
|
||||
function parseDate(dateStr: string): string {
|
||||
// Try M/D/YYYY format
|
||||
const mdyMatch = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||
if (mdyMatch) {
|
||||
const month = mdyMatch[1].padStart(2, "0");
|
||||
const day = mdyMatch[2].padStart(2, "0");
|
||||
const year = mdyMatch[3];
|
||||
return `${year}-${month}-${day}T12:00:00.000Z`;
|
||||
}
|
||||
// Try ISO format
|
||||
if (dateStr.includes("-")) {
|
||||
return new Date(dateStr + "T12:00:00.000Z").toISOString();
|
||||
}
|
||||
// Fallback
|
||||
return new Date(dateStr).toISOString();
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!file.name.endsWith(".csv")) {
|
||||
return NextResponse.json(
|
||||
{ error: "File must be a CSV file" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const content = await file.text();
|
||||
const rows = parseCSV(content);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "CSV is empty or invalid format" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get all user exercises for matching
|
||||
const exercises = await prisma.exercise.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
// Build case-insensitive lookup map
|
||||
const exerciseMap = new Map<string, string>();
|
||||
for (const ex of exercises) {
|
||||
exerciseMap.set(ex.name.toLowerCase(), ex.id);
|
||||
}
|
||||
|
||||
// Group rows by date
|
||||
const workoutsByDate = new Map<string, Array<Record<string, string>>>();
|
||||
|
||||
const unmappedExercises = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
const date = row.date || row.date_str || row.workout_date || "";
|
||||
const exerciseName = row.exercise || row.exercise_name || "";
|
||||
|
||||
if (!date || !exerciseName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!workoutsByDate.has(date)) {
|
||||
workoutsByDate.set(date, []);
|
||||
}
|
||||
|
||||
workoutsByDate.get(date)!.push(row);
|
||||
|
||||
// Check if exercise can be resolved
|
||||
const resolvedName = resolveExerciseName(exerciseName);
|
||||
const isKnown = exerciseMap.has(resolvedName.toLowerCase());
|
||||
if (!isKnown) {
|
||||
unmappedExercises.add(exerciseName);
|
||||
}
|
||||
}
|
||||
|
||||
// Build parsed workouts
|
||||
const parsedWorkouts: ParsedWorkout[] = [];
|
||||
|
||||
for (const [date, rowsForDate] of workoutsByDate) {
|
||||
const exercisesMap = new Map<
|
||||
string,
|
||||
{
|
||||
exerciseId: string;
|
||||
exerciseName: string;
|
||||
sets: ParsedSet[];
|
||||
}
|
||||
>();
|
||||
|
||||
for (const row of rowsForDate) {
|
||||
const csvExerciseName = row.exercise || row.exercise_name || "";
|
||||
const resolvedName = resolveExerciseName(csvExerciseName);
|
||||
const exerciseId =
|
||||
exerciseMap.get(resolvedName.toLowerCase()) || "";
|
||||
|
||||
if (!exerciseId) {
|
||||
unmappedExercises.add(csvExerciseName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!exercisesMap.has(exerciseId)) {
|
||||
exercisesMap.set(exerciseId, {
|
||||
exerciseId,
|
||||
exerciseName: resolvedName,
|
||||
sets: [],
|
||||
});
|
||||
}
|
||||
|
||||
const exerciseData = exercisesMap.get(exerciseId)!;
|
||||
const weight = row.weight ? parseFloat(row.weight) : undefined;
|
||||
const reps = row.reps ? parseInt(row.reps, 10) : undefined;
|
||||
let notes = row.notes || "";
|
||||
|
||||
// Detect weight unit from notes
|
||||
let weightUnit = "lbs";
|
||||
if (notes.toLowerCase().includes("kg")) {
|
||||
weightUnit = "kg";
|
||||
}
|
||||
|
||||
// Add variation note if applicable
|
||||
const variationNote = getVariationNote(csvExerciseName);
|
||||
if (variationNote) {
|
||||
notes = notes
|
||||
? `${notes} (${variationNote})`
|
||||
: `(${variationNote})`;
|
||||
}
|
||||
|
||||
const setNumber = exerciseData.sets.length + 1;
|
||||
|
||||
exerciseData.sets.push({
|
||||
setNumber,
|
||||
weight,
|
||||
weightUnit,
|
||||
reps,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const workoutExercises = Array.from(exercisesMap.values());
|
||||
if (workoutExercises.length > 0) {
|
||||
parsedWorkouts.push({
|
||||
date: parseDate(date),
|
||||
exercises: workoutExercises,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date ascending (oldest first)
|
||||
parsedWorkouts.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
|
||||
const response: ParseResponse = {
|
||||
workouts: parsedWorkouts,
|
||||
unmapped: Array.from(unmappedExercises),
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("CSV parsing error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to parse CSV file" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user