Files
proof-of-work/workout-planner/components/import/WorkoutImportClient.tsx
T
2026-02-28 09:27:26 -06:00

1012 lines
38 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import {
Upload,
X,
AlertTriangle,
Check,
Plus,
Trash2,
Loader,
Image,
ChevronDown,
ChevronUp,
} from 'lucide-react';
import { Exercise } from '@prisma/client';
import {
ParsedWorkout,
ParsedExercise,
ParsedSet,
ImportParseResponse,
ReviewedWorkout,
} from '@/types';
interface WorkoutImportClientProps {
hasApiKey: boolean;
exercises: Exercise[];
}
type Phase = 'upload' | 'review' | 'save';
interface ImageFile {
id: string;
file: File;
preview: string;
}
interface EditableSet extends ParsedSet {
id: string;
}
interface EditableExercise extends ParsedExercise {
id: string;
sets: EditableSet[];
}
interface EditableWorkout extends ParsedWorkout {
id: string;
exercises: EditableExercise[];
overallConfidence?: string;
warnings?: string[];
}
const WorkoutImportClient: React.FC<WorkoutImportClientProps> = ({
hasApiKey,
exercises: _exercises,
}) => {
const [phase, setPhase] = useState<Phase>('upload');
const [images, setImages] = useState<ImageFile[]>([]);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [editableWorkouts, setEditableWorkouts] = useState<EditableWorkout[]>([]);
const [expandedWorkouts, setExpandedWorkouts] = useState<Set<string>>(new Set());
const [isSaving, setIsSaving] = useState(false);
const [_saveProgress, setSaveProgress] = useState<{
current: number;
total: number;
} | null>(null);
const [saveSuccess, setSuccess] = useState<{ ids: string[] } | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// File upload handlers
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files).filter((f) =>
f.type.startsWith('image/')
);
addImages(files);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.currentTarget.files || []);
addImages(files);
};
const addImages = (files: File[]) => {
const newImages: ImageFile[] = files.map((file) => ({
id: Math.random().toString(36).slice(2),
file,
preview: URL.createObjectURL(file),
}));
setImages((prev) => [...prev, ...newImages]);
};
const removeImage = (id: string) => {
setImages((prev) => {
const img = prev.find((i) => i.id === id);
if (img) URL.revokeObjectURL(img.preview);
return prev.filter((i) => i.id !== id);
});
};
const handleAnalyze = async () => {
setIsAnalyzing(true);
setErrorMessage(null);
try {
const base64Images = await Promise.all(images.map((img) => fileToBase64(img.file)));
const response = await fetch('/api/workouts/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ images: base64Images }),
});
if (!response.ok) {
throw new Error(`Analysis failed: ${response.statusText}`);
}
const data: ImportParseResponse = await response.json();
// Convert to editable format with unique IDs
const editable: EditableWorkout[] = data.workouts.map((w) => ({
...w,
id: Math.random().toString(36).slice(2),
date: w.date || new Date().toISOString().split('T')[0],
exercises: w.exercises.map((e) => ({
...e,
id: Math.random().toString(36).slice(2),
sets: e.sets.map((s) => ({
...s,
id: Math.random().toString(36).slice(2),
})),
})),
}));
setEditableWorkouts(editable);
setExpandedWorkouts(new Set(editable.map((w) => w.id)));
setPhase('review');
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Unknown error');
} finally {
setIsAnalyzing(false);
}
};
const toggleWorkoutExpanded = (id: string) => {
setExpandedWorkouts((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const updateWorkoutField = (
workoutId: string,
field: keyof EditableWorkout,
value: unknown
) => {
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId ? { ...w, [field]: value } : w
)
);
};
const updateExerciseField = (
workoutId: string,
exerciseId: string,
field: keyof EditableExercise,
value: unknown
) => {
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? {
...w,
exercises: w.exercises.map((e) =>
e.id === exerciseId ? { ...e, [field]: value } : e
),
}
: w
)
);
};
const updateSetField = (
workoutId: string,
exerciseId: string,
setId: string,
field: keyof EditableSet,
value: unknown
) => {
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? {
...w,
exercises: w.exercises.map((e) =>
e.id === exerciseId
? {
...e,
sets: e.sets.map((s) =>
s.id === setId ? { ...s, [field]: value } : s
),
}
: e
),
}
: w
)
);
};
const addSet = (workoutId: string, exerciseId: string) => {
const newSet: EditableSet = {
id: Math.random().toString(36).slice(2),
reps: null,
weight: null,
weightUnit: 'lb',
durationSeconds: null,
distance: null,
distanceUnit: 'mi',
calories: null,
rpe: null,
notes: null,
};
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? {
...w,
exercises: w.exercises.map((e) =>
e.id === exerciseId
? { ...e, sets: [...e.sets, newSet] }
: e
),
}
: w
)
);
};
const removeSet = (workoutId: string, exerciseId: string, setId: string) => {
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? {
...w,
exercises: w.exercises.map((e) =>
e.id === exerciseId
? { ...e, sets: e.sets.filter((s) => s.id !== setId) }
: e
),
}
: w
)
);
};
const addExercise = (workoutId: string) => {
const newExercise: EditableExercise = {
id: Math.random().toString(36).slice(2),
name: 'New Exercise',
uncertain: false,
uncertainReason: undefined,
sets: [
{
id: Math.random().toString(36).slice(2),
reps: null,
weight: null,
weightUnit: 'lb',
durationSeconds: null,
distance: null,
distanceUnit: 'mi',
calories: null,
rpe: null,
notes: null,
},
],
};
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? { ...w, exercises: [...w.exercises, newExercise] }
: w
)
);
};
const removeExercise = (workoutId: string, exerciseId: string) => {
setEditableWorkouts((prev) =>
prev.map((w) =>
w.id === workoutId
? { ...w, exercises: w.exercises.filter((e) => e.id !== exerciseId) }
: w
)
);
};
const removeWorkout = (workoutId: string) => {
setEditableWorkouts((prev) => prev.filter((w) => w.id !== workoutId));
};
const handleSaveAll = async () => {
setIsSaving(true);
setSaveProgress({ current: 0, total: editableWorkouts.length });
setErrorMessage(null);
const createdIds: string[] = [];
try {
for (let i = 0; i < editableWorkouts.length; i++) {
const workout = editableWorkouts[i];
setSaveProgress({ current: i + 1, total: editableWorkouts.length });
// Prepare reviewed data (remove internal IDs)
const reviewedWorkout: ReviewedWorkout = {
date: workout.date,
name: workout.name,
notes: workout.notes,
exercises: workout.exercises.map((e) => ({
name: e.name,
sets: e.sets.map((s) => ({
reps: s.reps,
weight: s.weight,
weightUnit: s.weightUnit,
durationSeconds: s.durationSeconds,
distance: s.distance,
distanceUnit: s.distanceUnit,
calories: s.calories,
rpe: s.rpe,
notes: s.notes,
})),
})),
};
const response = await fetch('/api/workouts/import/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reviewedWorkout),
});
if (!response.ok) {
throw new Error(`Failed to save workout: ${response.statusText}`);
}
const result = await response.json();
if (result.id) {
createdIds.push(result.id);
}
}
setSuccess({ ids: createdIds });
setEditableWorkouts([]);
setPhase('save');
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Unknown error');
} finally {
setIsSaving(false);
setSaveProgress(null);
}
};
const resetToUpload = () => {
setPhase('upload');
setImages([]);
setEditableWorkouts([]);
setExpandedWorkouts(new Set());
setSuccess(null);
setErrorMessage(null);
};
// Phase 1: Upload
if (phase === 'upload') {
if (!hasApiKey) {
return (
<div className="min-h-screen bg-[#0A0A0A] p-4 md:p-8">
<div className="mx-auto max-w-2xl">
<div className="rounded-lg border border-zinc-800 bg-zinc-900 p-8">
<div className="flex items-start gap-4">
<AlertTriangle className="h-6 w-6 flex-shrink-0 text-amber-500 mt-1" />
<div>
<h3 className="text-lg font-semibold text-white mb-2">
Claude API Key Required
</h3>
<p className="text-zinc-400 mb-4">
Configure your Claude API key in Settings to use import.
</p>
<Link
href="/main/settings"
className="inline-block px-4 py-2 bg-white text-black font-medium rounded-lg hover:bg-zinc-100 transition-colors"
>
Go to Settings
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#0A0A0A] p-4 md:p-8">
<div className="mx-auto max-w-2xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Import Workouts</h1>
<p className="text-zinc-400">
Upload images of your workout to have Claude automatically parse them.
</p>
</div>
<div className="space-y-6">
{/* Drag and Drop Zone */}
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
className="rounded-lg border-2 border-dashed border-zinc-700 bg-zinc-900/50 p-8 text-center transition-colors hover:border-zinc-500"
>
<Image className="mx-auto h-12 w-12 text-zinc-500 mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Drop your workout images here
</h3>
<p className="text-zinc-400 mb-6">
or use the button below to select files
</p>
<label>
<input
type="file"
multiple
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
<span className="inline-block px-6 py-2 bg-white text-black font-medium rounded-lg hover:bg-zinc-100 transition-colors cursor-pointer">
Select Images
</span>
</label>
</div>
{/* Image Previews */}
{images.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-white mb-4">
Selected Images ({images.length})
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{images.map((img) => (
<div
key={img.id}
className="relative group rounded-lg overflow-hidden bg-zinc-800"
>
<img
src={img.preview}
alt="Preview"
className="w-full h-32 object-cover"
/>
<button
onClick={() => removeImage(img.id)}
className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
>
<X className="h-6 w-6 text-white" />
</button>
</div>
))}
</div>
</div>
)}
{/* Error Message */}
{errorMessage && (
<div className="rounded-lg border border-red-800 bg-red-900/20 p-4">
<p className="text-red-400">{errorMessage}</p>
</div>
)}
{/* Analyze Button */}
<button
onClick={handleAnalyze}
disabled={images.length === 0 || isAnalyzing}
className="w-full py-3 bg-white text-black font-semibold rounded-lg hover:bg-zinc-100 transition-colors disabled:bg-zinc-700 disabled:text-zinc-400 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isAnalyzing ? (
<>
<Loader className="h-5 w-5 animate-spin" />
Analyzing Images...
</>
) : (
<>
<Upload className="h-5 w-5" />
Analyze ({images.length})
</>
)}
</button>
</div>
</div>
</div>
);
}
// Phase 2: Review & Edit
if (phase === 'review') {
return (
<div className="min-h-screen bg-[#0A0A0A] p-4 md:p-8">
<div className="mx-auto max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Review Workouts</h1>
<p className="text-zinc-400">
Verify and edit the parsed workout data before saving.
</p>
</div>
{/* Error Message */}
{errorMessage && (
<div className="mb-6 rounded-lg border border-red-800 bg-red-900/20 p-4">
<p className="text-red-400">{errorMessage}</p>
</div>
)}
{/* Workouts */}
<div className="space-y-4 mb-8">
{editableWorkouts.map((workout) => {
const isExpanded = expandedWorkouts.has(workout.id);
const confidenceColor =
workout.overallConfidence === 'high'
? 'bg-green-900/20 border-green-800'
: workout.overallConfidence === 'medium'
? 'bg-amber-900/20 border-amber-800'
: 'bg-red-900/20 border-red-800';
return (
<div
key={workout.id}
className="rounded-lg border border-zinc-800 bg-zinc-900 overflow-hidden"
>
{/* Workout Header */}
<div className="border-b border-zinc-800 p-4">
<button
onClick={() => toggleWorkoutExpanded(workout.id)}
className="w-full flex items-center justify-between text-left"
>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white">
{workout.name || 'Unnamed Workout'}
</h3>
{workout.overallConfidence !== 'high' && (
<span
className={`text-xs px-2 py-1 rounded border ${confidenceColor}`}
>
{workout.overallConfidence?.toUpperCase()}
</span>
)}
</div>
<p className="text-sm text-zinc-400">{workout.date}</p>
</div>
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-zinc-400" />
) : (
<ChevronDown className="h-5 w-5 text-zinc-400" />
)}
</button>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="p-4 space-y-6 border-t border-zinc-800">
{/* Workout Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-white mb-1">
Date
</label>
<input
type="date"
value={workout.date}
onChange={(e) =>
updateWorkoutField(workout.id, 'date', e.target.value)
}
className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600"
/>
</div>
<div>
<label className="block text-sm font-medium text-white mb-1">
Workout Name
</label>
<input
type="text"
value={workout.name || ''}
onChange={(e) =>
updateWorkoutField(workout.id, 'name', e.target.value)
}
placeholder="e.g., Upper Body Day"
className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600"
/>
</div>
</div>
{/* Warnings */}
{workout.warnings && workout.warnings.length > 0 && (
<div className="rounded-lg border border-amber-800 bg-amber-900/20 p-4">
<p className="text-sm font-medium text-amber-400 mb-2">
Warnings:
</p>
<ul className="space-y-1">
{workout.warnings.map((warning, idx) => (
<li key={idx} className="text-sm text-amber-300">
{warning}
</li>
))}
</ul>
</div>
)}
{/* Exercises */}
<div className="space-y-4">
{workout.exercises.map((exercise) => (
<div
key={exercise.id}
className="rounded-lg border border-zinc-700 bg-zinc-800/50 p-4"
>
{/* Exercise Header */}
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex-1">
<label className="block text-sm font-medium text-white mb-1">
Exercise Name
</label>
<input
type="text"
value={exercise.name || ''}
onChange={(e) =>
updateExerciseField(
workout.id,
exercise.id,
'name',
e.target.value
)
}
className="w-full px-3 py-2 bg-zinc-700 border border-zinc-600 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-500"
/>
</div>
<button
onClick={() =>
removeExercise(workout.id, exercise.id)
}
className="mt-6 p-2 text-red-400 hover:bg-red-900/20 rounded transition-colors"
>
<Trash2 className="h-5 w-5" />
</button>
</div>
{/* Uncertain Warning */}
{exercise.uncertain && (
<div className="mb-4 rounded-lg border border-amber-800 bg-amber-900/20 p-3 flex items-start gap-2">
<AlertTriangle className="h-5 w-5 text-amber-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-300">
{exercise.uncertainReason ||
'Please verify this exercise name'}
</p>
</div>
)}
{/* Sets */}
<div className="space-y-3 mb-4">
{exercise.sets.map((set, setIdx) => (
<div
key={set.id}
className="bg-zinc-700/50 p-3 rounded border border-zinc-600"
>
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-zinc-400">
Set {setIdx + 1}
</span>
<button
onClick={() =>
removeSet(workout.id, exercise.id, set.id)
}
className="p-1 text-red-400 hover:bg-red-900/20 rounded"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
{/* Reps */}
<div>
<label className="block text-xs text-zinc-400 mb-1">
Reps
</label>
<input
type="number"
value={set.reps ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'reps',
e.target.value ? parseInt(e.target.value) : null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
{/* Weight */}
<div>
<label className="block text-xs text-zinc-400 mb-1">
Weight ({set.weightUnit})
</label>
<input
type="number"
step="0.5"
value={set.weight ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'weight',
e.target.value ? parseFloat(e.target.value) : null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
{/* Duration */}
<div>
<label className="block text-xs text-zinc-400 mb-1">
Time (mm:ss)
</label>
<DurationInput
value={set.durationSeconds ?? null}
onChange={(seconds) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'durationSeconds',
seconds
)
}
/>
</div>
{/* Distance */}
<div>
<label className="block text-xs text-zinc-400 mb-1">
Distance
</label>
<input
type="number"
step="0.1"
value={set.distance ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'distance',
e.target.value ? parseFloat(e.target.value) : null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
{/* RPE */}
<div>
<label className="block text-xs text-zinc-400 mb-1">
RPE
</label>
<input
type="number"
min="1"
max="10"
value={set.rpe ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'rpe',
e.target.value ? parseInt(e.target.value) : null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
{/* Calories */}
<div className="col-span-2 md:col-span-1 lg:col-span-1">
<label className="block text-xs text-zinc-400 mb-1">
Calories
</label>
<input
type="number"
value={set.calories ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'calories',
e.target.value ? parseInt(e.target.value) : null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
{/* Notes */}
<div className="col-span-2 md:col-span-3 lg:col-span-2">
<label className="block text-xs text-zinc-400 mb-1">
Notes
</label>
<input
type="text"
value={set.notes ?? ''}
onChange={(e) =>
updateSetField(
workout.id,
exercise.id,
set.id,
'notes',
e.target.value || null
)
}
className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
</div>
</div>
))}
</div>
{/* Add Set Button */}
<button
onClick={() => addSet(workout.id, exercise.id)}
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
>
<Plus className="h-4 w-4" />
Add Set
</button>
</div>
))}
</div>
{/* Add Exercise Button */}
<button
onClick={() => addExercise(workout.id)}
className="flex items-center gap-2 text-white font-medium px-4 py-2 rounded-lg border border-zinc-700 hover:bg-zinc-800 transition-colors"
>
<Plus className="h-5 w-5" />
Add Exercise
</button>
{/* Remove Workout Button */}
<button
onClick={() => removeWorkout(workout.id)}
className="w-full flex items-center justify-center gap-2 text-red-400 hover:bg-red-900/20 px-4 py-2 rounded-lg border border-red-800 transition-colors"
>
<Trash2 className="h-5 w-5" />
Remove Workout
</button>
</div>
)}
</div>
);
})}
</div>
{/* Action Buttons */}
<div className="flex gap-4">
<button
onClick={resetToUpload}
className="flex-1 py-3 px-4 border border-zinc-700 text-white font-semibold rounded-lg hover:bg-zinc-800 transition-colors"
>
Back to Upload
</button>
<button
onClick={handleSaveAll}
disabled={editableWorkouts.length === 0 || isSaving}
className="flex-1 py-3 px-4 bg-white text-black font-semibold rounded-lg hover:bg-zinc-100 transition-colors disabled:bg-zinc-700 disabled:text-zinc-400 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSaving ? (
<>
<Loader className="h-5 w-5 animate-spin" />
Saving...
</>
) : (
<>
<Check className="h-5 w-5" />
Save All Workouts
</>
)}
</button>
</div>
</div>
</div>
);
}
// Phase 3: Save Success
return (
<div className="min-h-screen bg-[#0A0A0A] p-4 md:p-8">
<div className="mx-auto max-w-2xl">
<div className="rounded-lg border border-green-800 bg-green-900/20 p-8 text-center">
<Check className="mx-auto h-16 w-16 text-green-500 mb-4" />
<h1 className="text-3xl font-bold text-white mb-2">Workouts Saved!</h1>
<p className="text-zinc-400 mb-6">
Successfully imported {saveSuccess?.ids.length || 0} workout(s).
</p>
{saveSuccess?.ids && saveSuccess.ids.length > 0 && (
<div className="mb-8 space-y-2">
{saveSuccess.ids.map((id) => (
<Link
key={id}
href={`/main/workouts/${id}`}
className="block py-2 px-4 bg-zinc-800 hover:bg-zinc-700 rounded transition-colors text-white"
>
View Workout
</Link>
))}
</div>
)}
<button
onClick={resetToUpload}
className="inline-block px-6 py-2 bg-white text-black font-medium rounded-lg hover:bg-zinc-100 transition-colors"
>
Import More Workouts
</button>
</div>
</div>
</div>
);
};
// Helper component for duration input
const DurationInput: React.FC<{
value: number | null;
onChange: (seconds: number | null) => void;
}> = ({ value, onChange }) => {
const [minutes, setMinutes] = React.useState<string>(
value ? Math.floor(value / 60).toString() : ''
);
const [seconds, setSeconds] = React.useState<string>(
value ? (value % 60).toString() : ''
);
const handleChange = (min: string, sec: string) => {
setMinutes(min);
setSeconds(sec);
const minNum = min ? parseInt(min) : 0;
const secNum = sec ? parseInt(sec) : 0;
const totalSeconds = minNum * 60 + secNum;
onChange(totalSeconds || null);
};
return (
<div className="flex gap-1">
<input
type="number"
min="0"
max="59"
value={minutes}
onChange={(e) => handleChange(e.target.value, seconds)}
placeholder="0"
className="w-2/3 px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
<span className="flex items-center text-white text-sm">:</span>
<input
type="number"
min="0"
max="59"
value={seconds}
onChange={(e) => handleChange(minutes, e.target.value)}
placeholder="0"
className="w-2/3 px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400"
/>
</div>
);
};
export default WorkoutImportClient;