Add slots based per class, admin courses mgmt, admin roles, horizontal nav/landing, glove favicon
This commit is contained in:
+125
-30
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user