Add slots based per class, admin courses mgmt, admin roles, horizontal nav/landing, glove favicon
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const SPORT_LABEL: Record<string, { icon: string; name: string }> = {
|
||||
"muay-thai": { icon: "🥊", name: "Muay Thai" },
|
||||
yoga: { icon: "🧘", name: "Yoga" },
|
||||
};
|
||||
|
||||
export default function AdminClasses({ slots }: { slots: any[] }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-bold">📅 Cours & places disponibles</h2>
|
||||
<p className="text-sm text-white/60">
|
||||
Renseigne le nombre de places pour chaque cours. Quand les places sont
|
||||
épuisées, le cours s'affiche « COMPLET » sur le site.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{slots.map((slot) => (
|
||||
<SlotCard key={slot.id} slot={slot} onUpdate={() => router.refresh()} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SlotCard({ slot, onUpdate }: { slot: any; onUpdate: () => void }) {
|
||||
const sport = SPORT_LABEL[slot.sport] || { icon: "🏅", name: slot.sport };
|
||||
const [capacity, setCapacity] = useState(String(slot.capacity));
|
||||
const [booked, setBooked] = useState(String(slot.booked));
|
||||
const [isFull, setIsFull] = useState(slot.isFull);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const capacityNum = Math.max(0, parseInt(capacity) || 0);
|
||||
const bookedNum = Math.max(0, parseInt(booked) || 0);
|
||||
const remaining = Math.max(0, capacityNum - bookedNum);
|
||||
const full = isFull || remaining === 0;
|
||||
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
const res = await fetch("/api/admin/classes", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: slot.id, capacity, booked, isFull }),
|
||||
});
|
||||
setSaving(false);
|
||||
if (res.ok) onUpdate();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{sport.icon}</span>
|
||||
<div>
|
||||
<p className="font-bold">
|
||||
{sport.name} — {slot.label}
|
||||
</p>
|
||||
<p className="text-xs text-white/50">{slot.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-bold ${
|
||||
full
|
||||
? "bg-red-500/20 text-red-300"
|
||||
: remaining <= 3
|
||||
? "bg-amber-500/20 text-amber-300"
|
||||
: "bg-emerald-500/20 text-emerald-300"
|
||||
}`}
|
||||
>
|
||||
{full ? "❌ COMPLET" : `${remaining} places libres`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="label">Capacité totale</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className="input"
|
||||
value={capacity}
|
||||
onChange={(e) => setCapacity(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Places occupées</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className="input"
|
||||
value={booked}
|
||||
onChange={(e) => setBooked(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="mt-3 flex cursor-pointer items-center gap-2 rounded-xl bg-white/5 px-4 py-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isFull}
|
||||
onChange={(e) => setIsFull(e.target.checked)}
|
||||
className="h-4 w-4 accent-red-600"
|
||||
/>
|
||||
<span className="font-semibold text-red-300">Marquer ce cours comme COMPLET</span>
|
||||
</label>
|
||||
|
||||
<button onClick={save} className="btn-primary mt-3 w-full" disabled={saving}>
|
||||
{saving ? "Enregistrement..." : "💾 Enregistrer"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user