Add slots based per class, admin courses mgmt, admin roles, horizontal nav/landing, glove favicon

This commit is contained in:
Wycz
2026-09-08 09:10:37 +02:00
parent 3f4fdeba99
commit 7556b2cd97
15 changed files with 439 additions and 74 deletions
+8 -7
View File
@@ -5,13 +5,14 @@ import { usePathname } from "next/navigation";
import Header from "@/components/Header";
const adminLinks = [
{ href: "/admin", label: "Vue d'ensemble", icon: "🏠" },
{ href: "/admin", label: "Accueil", icon: "🏠" },
{ href: "/admin/users", label: "Membres", icon: "👥" },
{ href: "/admin/messages", label: "Messages", icon: "💬" },
{ href: "/admin/media", label: "Photos & vidéos", icon: "🎬" },
{ href: "/admin/media", label: "Médias", icon: "🎬" },
{ href: "/admin/classes", label: "Cours", icon: "📅" },
{ href: "/admin/homework", label: "Exercices", icon: "🏋️" },
{ href: "/admin/certificates", label: "Certificats", icon: "📄" },
{ href: "/admin/yoga", label: "Gestion Yoga", icon: "🧘" },
{ href: "/admin/yoga", label: "Yoga", icon: "🧘" },
];
export default function AdminLayout({
@@ -32,7 +33,7 @@ export default function AdminLayout({
Gère ton club JuanitoSport
</p>
<nav className="mb-6 flex gap-2 overflow-x-auto pb-2 -mx-4 px-4">
<nav className="mb-6 grid grid-cols-4 gap-1.5 sm:gap-2">
{adminLinks.map((link) => {
const active =
pathname === link.href ||
@@ -41,14 +42,14 @@ export default function AdminLayout({
<Link
key={link.href}
href={link.href}
className={`flex shrink-0 items-center gap-1.5 rounded-xl px-4 py-2 text-sm font-semibold transition-colors ${
className={`flex flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-2.5 text-center text-[11px] font-semibold transition-colors sm:flex-row sm:gap-1.5 sm:px-3 sm:py-2 sm:text-xs ${
active
? "bg-primary-600 text-white"
: "bg-white/5 text-white/70 hover:bg-white/10"
}`}
>
<span>{link.icon}</span>
<span>{link.label}</span>
<span className="text-base leading-none sm:text-lg">{link.icon}</span>
<span className="leading-tight">{link.label}</span>
</Link>
);
})}
+117
View File
@@ -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&apos;affiche «&nbsp;COMPLET&nbsp;» 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>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { prisma } from "@/lib/prisma";
import AdminClasses from "./classes-admin";
export default async function ClassesPage() {
const slots = await prisma.classSlot.findMany({
orderBy: { label: "asc" },
});
return <AdminClasses slots={slots} />;
}
+125 -30
View File
@@ -28,7 +28,7 @@ export default function AdminUsers({ users }: { users: any[] }) {
</button>
</div>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{(
[
["ALL", "Tous"],
@@ -70,6 +70,7 @@ function CreateUserForm({ onDone }: { onDone: () => void }) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("MEMBER");
const [isMuayThai, setIsMuayThai] = useState(true);
const [isYoga, setIsYoga] = useState(false);
const [error, setError] = useState("");
@@ -82,7 +83,14 @@ function CreateUserForm({ onDone }: { onDone: () => void }) {
const res = await fetch("/api/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password, isMuayThai, isYoga }),
body: JSON.stringify({
name,
email,
password,
role,
isMuayThai: role === "MEMBER" ? isMuayThai : false,
isYoga: role === "MEMBER" ? isYoga : false,
}),
});
const data = await res.json();
setSaving(false);
@@ -95,7 +103,7 @@ function CreateUserForm({ onDone }: { onDone: () => void }) {
return (
<form onSubmit={handleSubmit} className="card space-y-4 border-primary-500/30">
<h3 className="font-semibold">✨ Créer un compte membre</h3>
<h3 className="font-semibold">✨ Créer un compte</h3>
<div>
<label className="label">Nom complet</label>
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Jean Dupont" required />
@@ -108,10 +116,39 @@ function CreateUserForm({ onDone }: { onDone: () => void }) {
<label className="label">Mot de passe initial</label>
<input className="input" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Mot de passe temporaire" required />
</div>
<div className="flex gap-3">
<Checkbox label="🥊 Muay Thai" checked={isMuayThai} onChange={setIsMuayThai} />
<Checkbox label="🧘 Yoga" checked={isYoga} onChange={setIsYoga} />
<div>
<label className="label">Rôle</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setRole("MEMBER")}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
role === "MEMBER"
? "bg-primary-600 text-white"
: "bg-white/5 text-white/60"
}`}
>
👤 Membre
</button>
<button
type="button"
onClick={() => setRole("ADMIN")}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
role === "ADMIN"
? "bg-amber-600 text-white"
: "bg-white/5 text-white/60"
}`}
>
⭐ Admin
</button>
</div>
</div>
{role === "MEMBER" && (
<div className="flex gap-3">
<Checkbox label="🥊 Muay Thai" checked={isMuayThai} onChange={setIsMuayThai} />
<Checkbox label="🧘 Yoga" checked={isYoga} onChange={setIsYoga} />
</div>
)}
{error && <p className="text-sm text-red-400">{error}</p>}
<button type="submit" className="btn-primary w-full" disabled={saving}>
{saving ? "Création..." : "✅ Créer le compte"}
@@ -133,19 +170,27 @@ function MemberCard({ user, onUpdate }: { user: any; onUpdate: () => void }) {
const [expanded, setExpanded] = useState(false);
const [editName, setEditName] = useState(user.name);
const [editPassword, setEditPassword] = useState("");
const router = useRouter();
const [editRole, setEditRole] = useState(user.role);
const [error, setError] = useState("");
const [saving, setSaving] = useState(false);
const certStatus = user.certificates?.find((c: any) => c.status !== "REJECTED")?.status;
async function updateUser(data: any) {
setSaving(true);
setError("");
const res = await fetch(`/api/admin/users/${user.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
setSaving(false);
if (res.ok) {
setEditPassword("");
onUpdate();
setExpanded(false);
} else {
const d = await res.json().catch(() => ({}));
setError(d.error || "Erreur lors de la mise à jour");
}
}
@@ -159,17 +204,31 @@ function MemberCard({ user, onUpdate }: { user: any; onUpdate: () => void }) {
if (res.ok) onUpdate();
}
const isAdmin = user.role === "ADMIN";
const canDelete = !isAdmin;
return (
<div className="card">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary-600/20 text-lg font-bold text-primary-300">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold ${
isAdmin
? "bg-amber-500/20 text-amber-300"
: "bg-primary-600/20 text-primary-300"
}`}
>
{user.name.charAt(0).toUpperCase()}
</div>
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-xs text-white/50">{user.email}</p>
<div className="mt-1 flex gap-1.5">
<div className="mt-1 flex flex-wrap gap-1.5">
{isAdmin && (
<span className="rounded-full bg-amber-500/20 px-2 py-0.5 text-xs text-amber-300">
⭐ Admin
</span>
)}
{user.isMuayThai && (
<span className="rounded-full bg-primary-600/20 px-2 py-0.5 text-xs text-primary-300">
🥊 Muay Thai
@@ -180,11 +239,6 @@ function MemberCard({ user, onUpdate }: { user: any; onUpdate: () => void }) {
🧘 Yoga
</span>
)}
{user.role === "ADMIN" && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
⭐ Admin
</span>
)}
</div>
</div>
</div>
@@ -212,33 +266,74 @@ function MemberCard({ user, onUpdate }: { user: any; onUpdate: () => void }) {
</div>
)}
{expanded && user.role !== "ADMIN" && (
{expanded && (
<div className="mt-4 space-y-3 border-t border-white/10 pt-4">
<h4 className="text-sm font-semibold">Modifier</h4>
<div className="grid grid-cols-1 gap-3">
<div>
<label className="label">Nom</label>
<input className="input" value={editName} onChange={(e) => setEditName(e.target.value)} />
</div>
<div>
<label className="label">Nouveau mot de passe (optionnel)</label>
<input className="input" value={editPassword} onChange={(e) => setEditPassword(e.target.value)} placeholder="Laisser vide pour ne pas changer" />
<h4 className="text-sm font-semibold">
⚙️ Configuration — {isAdmin ? "compte admin" : "compte membre"}
</h4>
<div>
<label className="label">Nom</label>
<input className="input" value={editName} onChange={(e) => setEditName(e.target.value)} />
</div>
<div>
<label className="label">Rôle</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setEditRole("MEMBER")}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
editRole === "MEMBER"
? "bg-primary-600 text-white"
: "bg-white/5 text-white/60"
}`}
>
👤 Membre
</button>
<button
type="button"
onClick={() => setEditRole("ADMIN")}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold ${
editRole === "ADMIN"
? "bg-amber-600 text-white"
: "bg-white/5 text-white/60"
}`}
>
⭐ Admin
</button>
</div>
</div>
<div>
<label className="label">Réinitialiser le mot de passe</label>
<input
className="input"
value={editPassword}
onChange={(e) => setEditPassword(e.target.value)}
placeholder="Nouveau mot de passe (laisser vide pour ne pas changer)"
/>
</div>
{error && <p className="text-sm text-red-400">{error}</p>}
<div className="flex gap-2">
<button
onClick={() => {
const data: any = { name: editName };
const data: any = { name: editName, role: editRole };
if (editPassword) data.password = editPassword;
updateUser(data);
}}
className="btn-primary flex-1 !py-2 text-sm"
disabled={saving}
>
💾 Enregistrer
</button>
<button onClick={deleteUser} className="btn-danger">
🗑️
{saving ? "Enregistrement..." : "💾 Enregistrer"}
</button>
{canDelete && (
<button onClick={deleteUser} className="btn-danger" title="Supprimer">
🗑️
</button>
)}
</div>
</div>
)}