"use client"; import { useState, useRef } from "react"; import { ChevronLeft, Upload, Trash2, Check, X } from "lucide-react"; import Link from "next/link"; 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 WorkoutState extends ParsedWorkout { status: "pending" | "approved" | "skipped"; } export default function ImportCSVPage() { const [workouts, setWorkouts] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [unmapped, setUnmapped] = useState([]); const fileInputRef = useRef(null); const currentWorkout = workouts[currentIndex]; const approved = workouts.filter((w) => w.status === "approved").length; const skipped = workouts.filter((w) => w.status === "skipped").length; const remaining = workouts.filter((w) => w.status === "pending").length; const handleFileChange = async ( event: React.ChangeEvent ) => { const file = event.target.files?.[0]; if (!file) return; setLoading(true); setError(null); try { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/import/parse", { method: "POST", body: formData, }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Failed to parse CSV"); } const data = await response.json(); const initialWorkouts: WorkoutState[] = data.workouts.map( (w: ParsedWorkout) => ({ ...w, status: "pending" as const, }) ); setWorkouts(initialWorkouts); setUnmapped(data.unmapped || []); setCurrentIndex(0); } catch (err) { setError(err instanceof Error ? err.message : "Failed to parse CSV"); } finally { setLoading(false); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); const files = e.dataTransfer.files; if (files.length > 0) { const file = files[0]; if (fileInputRef.current) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); fileInputRef.current.files = dataTransfer.files; const event = new Event("change", { bubbles: true }); fileInputRef.current.dispatchEvent(event); } } }; const updateSet = ( exerciseIdx: number, setIdx: number, field: keyof ParsedSet, value: any ) => { if (!currentWorkout) return; const updatedWorkouts = [...workouts]; const workout = updatedWorkouts[currentIndex]; const set = workout.exercises[exerciseIdx].sets[setIdx]; if (field === "setNumber") { set[field] = value ? parseInt(value, 10) : 0; } else if (field === "reps") { set[field] = value ? parseInt(value, 10) : undefined; } else if (field === "weight") { set[field] = value ? parseFloat(value) : undefined; } else { (set[field] as any) = value; } setWorkouts(updatedWorkouts); }; const deleteSet = (exerciseIdx: number, setIdx: number) => { if (!currentWorkout) return; const updatedWorkouts = [...workouts]; const workout = updatedWorkouts[currentIndex]; const exercise = workout.exercises[exerciseIdx]; // Remove the set exercise.sets.splice(setIdx, 1); // Renumber remaining sets exercise.sets.forEach((set, idx) => { set.setNumber = idx + 1; }); // If no sets left, remove the exercise if (exercise.sets.length === 0) { workout.exercises.splice(exerciseIdx, 1); } setWorkouts(updatedWorkouts); }; const deleteExercise = (exerciseIdx: number) => { if (!currentWorkout) return; const updatedWorkouts = [...workouts]; const workout = updatedWorkouts[currentIndex]; workout.exercises.splice(exerciseIdx, 1); setWorkouts(updatedWorkouts); }; const approveWorkout = async () => { if (!currentWorkout) return; try { setLoading(true); // Transform workout to API format const setLogs = []; for (const exercise of currentWorkout.exercises) { for (const set of exercise.sets) { setLogs.push({ exerciseId: exercise.exerciseId, setNumber: set.setNumber, weight: set.weight || null, weightUnit: set.weightUnit, reps: set.reps || null, notes: set.notes || null, }); } } const response = await fetch("/api/workouts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: currentWorkout.date, sets: setLogs, }), }); if (!response.ok) { throw new Error("Failed to save workout"); } // Mark as approved and move to next const updatedWorkouts = [...workouts]; updatedWorkouts[currentIndex].status = "approved"; setWorkouts(updatedWorkouts); // Find next pending workout const nextPending = updatedWorkouts.findIndex( (w) => w.status === "pending" ); if (nextPending !== -1) { setCurrentIndex(nextPending); } else { setCurrentIndex(currentIndex + 1); } } catch (err) { setError(err instanceof Error ? err.message : "Failed to save workout"); } finally { setLoading(false); } }; const skipWorkout = () => { if (!currentWorkout) return; const updatedWorkouts = [...workouts]; updatedWorkouts[currentIndex].status = "skipped"; setWorkouts(updatedWorkouts); // Find next pending workout const nextPending = updatedWorkouts.findIndex( (w, idx) => w.status === "pending" && idx > currentIndex ); if (nextPending !== -1) { setCurrentIndex(nextPending); } else { setCurrentIndex(currentIndex + 1); } }; const deleteWorkout = () => { const updatedWorkouts = workouts.filter((_, idx) => idx !== currentIndex); setWorkouts(updatedWorkouts); if (updatedWorkouts.length > 0) { setCurrentIndex(Math.min(currentIndex, updatedWorkouts.length - 1)); } }; // Upload step if (workouts.length === 0) { return (
{/* Header */}

Import Workouts

{/* Upload Area */}
{error && (

{error}

)}
fileInputRef.current?.click()} >

Upload CSV File

Drag and drop your CSV file here or click to select

CSV columns: date, exercise, weight, reps, notes

{loading && (

Parsing CSV...

)}
{/* Example Format */}

CSV Format Example

              {`date,exercise,weight,reps,notes
2025-02-15,Bench,225,5,good form
2025-02-15,Bench,225,5,
2025-02-15,Bench,225,3,
2025-02-16,Squat,315,8,30kg per leg
2025-02-16,Squat,315,6,`}
            
); } // Review step if (!currentWorkout) { return (

Import Complete

All Done!

{approved} workouts approved, {skipped} skipped

View Workouts
); } return (
{/* Header */}

Review Workouts

{/* Progress Bar */}
{approved} approved, {skipped} skipped, {remaining} remaining {currentIndex + 1} of {workouts.length}
{/* Content */}
{error && (

{error}

)} {/* Unmapped Exercises Warning */} {unmapped.length > 0 && (

Unmapped exercises (not in your database):

{unmapped.map((name) => ( {name} ))}
)} {/* Workout Card */}
{/* Date Header */}

{new Date(currentWorkout.date).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", })}

{/* Exercises */}
{currentWorkout.exercises.map((exercise, exIdx) => (

{exercise.exerciseName}

{/* Sets Table */}
{exercise.sets.map((set, setIdx) => ( ))}
Set Weight Unit Reps Notes Action
updateSet(exIdx, setIdx, "setNumber", e.target.value) } className="w-12 bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-white text-center" /> updateSet(exIdx, setIdx, "weight", e.target.value) } className="w-20 bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-white" /> updateSet(exIdx, setIdx, "reps", e.target.value) } className="w-16 bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-white" /> updateSet(exIdx, setIdx, "notes", e.target.value) } className="flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-white text-sm" />
))}
{/* Action Buttons */}
); }