399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import Header from "@/components/Header";
|
|
import { useRouter } from "next/navigation";
|
|
import { signOut } from "next-auth/react";
|
|
|
|
type DashboardProps = {
|
|
user: any;
|
|
messages: any[];
|
|
unreadCount: number;
|
|
};
|
|
|
|
function formatDuration(seconds: number | null) {
|
|
if (!seconds) return "—";
|
|
const m = Math.floor(seconds / 60);
|
|
const s = seconds % 60;
|
|
if (m === 0) return `${s}s`;
|
|
return `${m}m${s.toString().padStart(2, "0")}`;
|
|
}
|
|
|
|
export default function MuayThaiDashboard({
|
|
user,
|
|
messages,
|
|
unreadCount,
|
|
}: DashboardProps) {
|
|
const router = useRouter();
|
|
const [media, setMedia] = useState<any[]>([]);
|
|
const [showMedia, setShowMedia] = useState(false);
|
|
const [showHomework, setShowHomework] = useState(false);
|
|
const [showMyPerf, setShowMyPerf] = useState(false);
|
|
|
|
const profile = user?.muayThai;
|
|
|
|
useEffect(() => {
|
|
fetch("/api/media")
|
|
.then((r) => r.json())
|
|
.then((data) =>
|
|
setMedia(
|
|
(data || []).filter(
|
|
(m: any) =>
|
|
m.visibility === "PUBLIC" ||
|
|
m.visibility === "MEMBERS_ONLY" ||
|
|
m.visibility === "MUAY_THAI_ONLY"
|
|
)
|
|
)
|
|
)
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
async function markAsRead(messageId: string) {
|
|
try {
|
|
await fetch("/api/messages", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ messageId }),
|
|
});
|
|
router.refresh();
|
|
} catch {}
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen pb-16">
|
|
<Header />
|
|
<div className="mx-auto max-w-3xl px-4 py-6">
|
|
<div className="mb-6 flex items-start justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-extrabold">
|
|
👋 Salut, {user.name.split(" ")[0]} !
|
|
</h1>
|
|
<p className="mt-1 text-sm text-white/60">
|
|
Voici ton espace Muay Thai
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => signOut({ callbackUrl: "/" })}
|
|
className="rounded-xl border border-white/10 bg-white/5 p-2 text-sm text-white/60"
|
|
title="Déconnexion"
|
|
>
|
|
🚪
|
|
</button>
|
|
</div>
|
|
|
|
{/* Messages de Juanito */}
|
|
<section className="mb-6">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h2 className="text-lg font-bold">
|
|
💬 Messages de Juanito{" "}
|
|
{unreadCount > 0 && (
|
|
<span className="ml-1 rounded-full bg-primary-600 px-2 py-0.5 text-xs font-bold text-white">
|
|
{unreadCount} nouveau(x)
|
|
</span>
|
|
)}
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{messages.length === 0 && (
|
|
<p className="card text-sm text-white/50">
|
|
Aucun message pour le moment. Juanito ne va pas tarder !
|
|
</p>
|
|
)}
|
|
{messages.map((msg) => (
|
|
<div
|
|
key={msg.id}
|
|
className={`card ${!msg.isRead ? "border-primary-500/40 bg-primary-500/5" : ""}`}
|
|
onClick={() => !msg.isRead && markAsRead(msg.id)}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<h3 className="font-semibold">{msg.title}</h3>
|
|
<p className="mt-1 whitespace-pre-line text-sm text-white/75">
|
|
{msg.content}
|
|
</p>
|
|
<p className="mt-2 text-xs text-white/40">
|
|
{new Date(msg.createdAt).toLocaleDateString("fr-FR", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
})}
|
|
</p>
|
|
</div>
|
|
{!msg.isRead && (
|
|
<span className="mt-1 h-3 w-3 shrink-0 rounded-full bg-primary-500" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section Performance */}
|
|
<section className="mb-6">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h2 className="text-lg font-bold">🏅 Ta performance</h2>
|
|
<button
|
|
onClick={() => setShowMyPerf(!showMyPerf)}
|
|
className="text-sm text-primary-300 hover:text-primary-200"
|
|
>
|
|
{showMyPerf ? "Voir la suite" : "+ Ajouter un chrono"}
|
|
</button>
|
|
</div>
|
|
|
|
{showMyPerf && <AddPerfForm profileId={profile?.id} onDone={() => { setShowMyPerf(false); router.refresh(); }} />}
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="card text-center">
|
|
<p className="text-xs text-white/50">🪢 Meilleur chrono corde</p>
|
|
<p className="mt-1 text-2xl font-bold text-primary-300">
|
|
{formatDuration(profile?.jumpRopeBestTime)}
|
|
</p>
|
|
</div>
|
|
<div className="card text-center">
|
|
<p className="text-xs text-white/50">💪 100 burpees</p>
|
|
<p className="mt-1 text-2xl font-bold text-primary-300">
|
|
{formatDuration(profile?.burpee100BestTime)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 card">
|
|
<h3 className="mb-2 text-sm font-semibold text-white/70">
|
|
Historique des entraînements
|
|
</h3>
|
|
{profile?.sessions?.length === 0 && (
|
|
<p className="text-sm text-white/50">
|
|
Aucun entraînement enregistré pour l'instant.
|
|
</p>
|
|
)}
|
|
<div className="max-h-56 space-y-2 overflow-y-auto">
|
|
{profile?.sessions?.map((s: any) => (
|
|
<div
|
|
key={s.id}
|
|
className="flex items-center justify-between rounded-xl bg-white/5 px-3 py-2 text-sm"
|
|
>
|
|
<span className="text-white/70">
|
|
{new Date(s.date).toLocaleDateString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
})}
|
|
</span>
|
|
<span className="flex gap-3 text-xs tabular-nums text-white/50">
|
|
{s.jumpRopeTime && (
|
|
<span>🪢 {formatDuration(s.jumpRopeTime)}</span>
|
|
)}
|
|
{s.burpee100Time && (
|
|
<span>💪 {formatDuration(s.burpee100Time)}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Actions rapides */}
|
|
<section className="mb-6 grid grid-cols-2 gap-3">
|
|
<Link href="/member/muay-thai/certificate" className="card flex flex-col items-center justify-center py-5 text-center transition-colors hover:bg-white/10">
|
|
<span className="mb-2 text-3xl">📄</span>
|
|
<span className="text-sm font-semibold">Certificat médical</span>
|
|
<span className="mt-1 text-xs text-white/50">
|
|
{user.certificates?.some((c: any) => c.status === "VALIDATED")
|
|
? "✓ Validé"
|
|
: user.certificates?.some((c: any) => c.status === "PENDING")
|
|
? "⏳ En cours"
|
|
: "À fournir"}
|
|
</span>
|
|
</Link>
|
|
<Link href="/member/muay-thai/absence" className="card flex flex-col items-center justify-center py-5 text-center transition-colors hover:bg-white/10">
|
|
<span className="mb-2 text-3xl">📅</span>
|
|
<span className="text-sm font-semibold">Signaler une absence</span>
|
|
</Link>
|
|
<button onClick={() => setShowMedia(!showMedia)} className="card flex flex-col items-center justify-center py-5 text-center transition-colors hover:bg-white/10">
|
|
<span className="mb-2 text-3xl">🎬</span>
|
|
<span className="text-sm font-semibold">Photos & vidéos</span>
|
|
</button>
|
|
<button onClick={() => setShowHomework(!showHomework)} className="card flex flex-col items-center justify-center py-5 text-center transition-colors hover:bg-white/10">
|
|
<span className="mb-2 text-3xl">🏋️</span>
|
|
<span className="text-sm font-semibold">Trucs à travailler</span>
|
|
</button>
|
|
</section>
|
|
|
|
{/* Médias */}
|
|
{showMedia && (
|
|
<section className="card mb-6">
|
|
<h3 className="mb-3 font-bold">🎬 Photos & vidéos des adhérents</h3>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{media.length === 0 && (
|
|
<p className="col-span-2 text-sm text-white/50">
|
|
Pas encore de médias publiés.
|
|
</p>
|
|
)}
|
|
{media.map((m) =>
|
|
m.type === "PHOTO" ? (
|
|
<a
|
|
key={m.id}
|
|
href={m.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="overflow-hidden rounded-xl bg-white/5"
|
|
>
|
|
<img
|
|
src={m.url}
|
|
alt={m.title}
|
|
className="aspect-square w-full object-cover"
|
|
loading="lazy"
|
|
/>
|
|
<p className="truncate p-2 text-xs text-white/70">
|
|
{m.title}
|
|
</p>
|
|
</a>
|
|
) : (
|
|
<div key={m.id} className="overflow-hidden rounded-xl bg-white/5">
|
|
<video controls playsInline preload="metadata" className="aspect-video w-full" src={m.url} />
|
|
<p className="truncate p-2 text-xs text-white/70">{m.title}</p>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Homework */}
|
|
{showHomework && (
|
|
<section className="card mb-6">
|
|
<h3 className="mb-3 font-bold">🏋️ Exercices à travailler</h3>
|
|
{user.homework?.length === 0 && (
|
|
<p className="text-sm text-white/50">
|
|
Aucun exercice pour l'instant. Juanito va t'en envoyer !
|
|
</p>
|
|
)}
|
|
<div className="space-y-3">
|
|
{user.homework?.map((h: any) => (
|
|
<div key={h.id} className="rounded-xl bg-white/5 p-4">
|
|
<h4 className="font-semibold text-primary-300">{h.title}</h4>
|
|
<p className="mt-1 whitespace-pre-line text-sm text-white/75">
|
|
{h.description}
|
|
</p>
|
|
{h.videoUrl && (
|
|
<video
|
|
controls
|
|
playsInline
|
|
preload="metadata"
|
|
className="mt-3 aspect-video w-full rounded-xl"
|
|
src={h.videoUrl}
|
|
/>
|
|
)}
|
|
<p className="mt-2 text-xs text-white/40">
|
|
{new Date(h.createdAt).toLocaleDateString("fr-FR")}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function AddPerfForm({
|
|
profileId,
|
|
onDone,
|
|
}: {
|
|
profileId?: string;
|
|
onDone: () => void;
|
|
}) {
|
|
const [jumpRopeTime, setJumpRopeTime] = useState("");
|
|
const [burpee100Time, setBurpee100Time] = useState("");
|
|
const [notes, setNotes] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
function parseTimeToSeconds(input: string): number | null {
|
|
// Accepte "3:45", "3m45", "45" (secondes)
|
|
const clean = input.trim();
|
|
if (!clean) return null;
|
|
const colonMatch = clean.match(/^(\d+):(\d{1,2})$/);
|
|
if (colonMatch) {
|
|
return parseInt(colonMatch[1]) * 60 + parseInt(colonMatch[2]);
|
|
}
|
|
const mMatch = clean.match(/^(\d+)m(\d{1,2})$/i);
|
|
if (mMatch) {
|
|
return parseInt(mMatch[1]) * 60 + parseInt(mMatch[2]);
|
|
}
|
|
const sMatch = clean.match(/^(\d+)s?$/i);
|
|
if (sMatch) return parseInt(sMatch[1]);
|
|
return null;
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!jumpRopeTime && !burpee100Time) {
|
|
setError("Renseigne au moins un chrono");
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
setError("");
|
|
|
|
const payload: any = {};
|
|
const jr = parseTimeToSeconds(jumpRopeTime);
|
|
const bp = parseTimeToSeconds(burpee100Time);
|
|
if (jr !== null) payload.jumpRopeTime = jr;
|
|
if (bp !== null) payload.burpee100Time = bp;
|
|
if (notes) payload.notes = notes;
|
|
|
|
const res = await fetch("/api/training", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (res.ok) {
|
|
onDone();
|
|
} else {
|
|
setError("Erreur lors de l'enregistrement");
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="card mb-3 border-primary-500/30">
|
|
<h3 className="mb-3 font-semibold">⏱️ Enregistrer un chrono</h3>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="label">🪢 Corde à sauter</label>
|
|
<input
|
|
className="input"
|
|
placeholder="ex: 3:45 ou 225"
|
|
value={jumpRopeTime}
|
|
onChange={(e) => setJumpRopeTime(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">💪 100 burpees</label>
|
|
<input
|
|
className="input"
|
|
placeholder="ex: 5:30"
|
|
value={burpee100Time}
|
|
onChange={(e) => setBurpee100Time(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3">
|
|
<label className="label">Notes (optionnel)</label>
|
|
<input
|
|
className="input"
|
|
placeholder="Ta séance en quelques mots..."
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
/>
|
|
</div>
|
|
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
|
|
<button type="submit" className="btn-primary mt-3 w-full" disabled={saving}>
|
|
{saving ? "Enregistrement..." : "✅ Enregistrer"}
|
|
</button>
|
|
</form>
|
|
);
|
|
} |