Initial commit for Start9 packaging

This commit is contained in:
MacPro
2026-02-28 09:27:26 -06:00
commit 1b64c45c52
124 changed files with 15671 additions and 0 deletions
@@ -0,0 +1,256 @@
"use client";
import { useState } from "react";
import { Exercise } from "@prisma/client";
import { Loader2 } from "lucide-react";
const EXERCISE_TYPES = [
"barbell",
"dumbbell",
"machine",
"cable",
"bodyweight",
"cardio",
"kettlebell",
"other",
];
const MUSCLE_GROUPS = [
"chest",
"back",
"shoulders",
"biceps",
"triceps",
"forearms",
"quads",
"hamstrings",
"glutes",
"calves",
"core",
"cardio",
];
const INPUT_FIELD_OPTIONS = [
{ value: "sets", label: "Sets" },
{ value: "reps", label: "Reps" },
{ value: "weight", label: "Weight" },
{ value: "duration", label: "Duration" },
{ value: "distance", label: "Distance" },
{ value: "calories", label: "Calories" },
];
interface AddExerciseFormProps {
onExerciseAdded: (exercise: Exercise) => void;
}
export default function AddExerciseForm({
onExerciseAdded,
}: AddExerciseFormProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState({
name: "",
type: "barbell",
muscleGroups: [] as string[],
inputFields: ["sets", "reps", "weight"] as string[],
description: "",
});
const handleMuscleGroupToggle = (group: string) => {
setFormData((prev) => {
const groups = prev.muscleGroups.includes(group)
? prev.muscleGroups.filter((g) => g !== group)
: [...prev.muscleGroups, group];
return { ...prev, muscleGroups: groups };
});
};
const handleInputFieldToggle = (field: string) => {
setFormData((prev) => {
const fields = prev.inputFields.includes(field)
? prev.inputFields.filter((f) => f !== field)
: [...prev.inputFields, field];
return { ...prev, inputFields: fields };
});
};
// Auto-set sensible input fields when type changes
const handleTypeChange = (type: string) => {
let defaultFields = ["sets", "reps", "weight"];
if (type === "cardio") {
defaultFields = ["sets", "duration", "calories"];
} else if (type === "bodyweight") {
defaultFields = ["sets", "reps"];
}
setFormData((prev) => ({
...prev,
type,
inputFields: defaultFields,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const response = await fetch("/api/exercises", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: formData.name,
type: formData.type,
muscleGroups: formData.muscleGroups,
inputFields: formData.inputFields,
description: formData.description || undefined,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Failed to add exercise");
}
const exercise = await response.json();
onExerciseAdded(exercise);
setFormData({
name: "",
type: "barbell",
muscleGroups: [],
inputFields: ["sets", "reps", "weight"],
description: "",
});
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="bg-red-900/50 border border-red-800 rounded-lg p-3 text-red-400 text-sm">
{error}
</div>
)}
{/* Name */}
<div>
<label className="block text-xs font-medium text-zinc-400 uppercase tracking-wider mb-2">
Exercise Name
</label>
<input
type="text"
required
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-white/20"
placeholder="e.g., Barbell Bench Press"
/>
</div>
{/* Equipment */}
<div>
<label className="block text-xs font-medium text-zinc-400 uppercase tracking-wider mb-2">
Equipment
</label>
<div className="flex flex-wrap gap-2">
{EXERCISE_TYPES.map((type) => (
<button
key={type}
type="button"
onClick={() => handleTypeChange(type)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
formData.type === type
? "bg-white text-black"
: "bg-zinc-800 text-zinc-400 hover:text-white"
}`}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
</div>
{/* Input Fields */}
<div>
<label className="block text-xs font-medium text-zinc-400 uppercase tracking-wider mb-1">
Tracked Fields
</label>
<p className="text-xs text-zinc-600 mb-2">
What data do you log for this exercise?
</p>
<div className="flex flex-wrap gap-2">
{INPUT_FIELD_OPTIONS.map((field) => (
<button
key={field.value}
type="button"
onClick={() => handleInputFieldToggle(field.value)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
formData.inputFields.includes(field.value)
? "bg-white text-black"
: "bg-zinc-800 text-zinc-400 hover:text-white"
}`}
>
{field.label}
</button>
))}
</div>
</div>
{/* Muscle Groups */}
<div>
<label className="block text-xs font-medium text-zinc-400 uppercase tracking-wider mb-2">
Muscle Groups
</label>
<div className="flex flex-wrap gap-2">
{MUSCLE_GROUPS.map((group) => (
<button
key={group}
type="button"
onClick={() => handleMuscleGroupToggle(group)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
formData.muscleGroups.includes(group)
? "bg-white text-black"
: "bg-zinc-800 text-zinc-400 hover:text-white"
}`}
>
{group.charAt(0).toUpperCase() + group.slice(1)}
</button>
))}
</div>
</div>
{/* Description */}
<div>
<label className="block text-xs font-medium text-zinc-400 uppercase tracking-wider mb-2">
Description (optional)
</label>
<textarea
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({ ...prev, description: e.target.value }))
}
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-white/20 resize-none"
placeholder="Notes about form, tips, or variations..."
rows={2}
/>
</div>
{/* Submit */}
<button
type="submit"
disabled={loading || !formData.name}
className="w-full bg-white hover:bg-zinc-200 disabled:bg-zinc-700 disabled:text-zinc-500 text-black font-bold py-3 px-4 rounded-lg transition flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
{loading ? "Adding..." : "Add Exercise"}
</button>
</form>
);
}