Initial commit: JuanitoSport - Muay Thai & Yog4Fight PWA
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
export default function AbsenceForm() {
|
||||
const router = useRouter();
|
||||
const [date, setDate] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!date) return;
|
||||
setSaving(true);
|
||||
const res = await fetch("/api/absences", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ date, reason }),
|
||||
});
|
||||
setSaving(false);
|
||||
if (res.ok) {
|
||||
setDone(true);
|
||||
setTimeout(() => {
|
||||
router.push("/member/muay-thai");
|
||||
router.refresh();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-3xl px-4 py-8">
|
||||
<Link href="/member/muay-thai" className="mb-4 block text-sm text-primary-300">
|
||||
← Retour à mon espace
|
||||
</Link>
|
||||
<h1 className="mb-4 text-2xl font-bold">📅 Signaler une absence</h1>
|
||||
|
||||
{done ? (
|
||||
<div className="card border-emerald-500/30 text-center">
|
||||
<p className="text-2xl">👍</p>
|
||||
<p className="mt-2 font-semibold text-emerald-300">
|
||||
Absence signalée, merci !
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/50">Meilleure forme, à bientôt.</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="card space-y-4">
|
||||
<div>
|
||||
<label className="label">Date du cours manqué</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Raison (optionnelle)</label>
|
||||
<textarea
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
className="input min-h-[100px]"
|
||||
placeholder="Ex: blessure, travail, vacances..."
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn-primary w-full" disabled={saving}>
|
||||
{saving ? "Envoi..." : "✅ Confirmer mon absence"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import AbsenceForm from "./absence-form";
|
||||
|
||||
export default async function AbsencePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) redirect("/login");
|
||||
return <AbsenceForm />;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
export default function CertificateUpload({ certificates }: { certificates: any[] }) {
|
||||
const router = useRouter();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const latest = certificates[0];
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
setError("Choisis un fichier d'abord");
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("subdir", "certificates");
|
||||
|
||||
const up = await fetch("/api/upload", { method: "POST", body: formData });
|
||||
if (!up.ok) throw new Error("Upload échoué");
|
||||
const { url } = await up.json();
|
||||
|
||||
const res = await fetch("/api/certificates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fileUrl: url }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Enregistrement échoué");
|
||||
|
||||
setSuccess("Certificat envoyé ! Juanito va le vérifier.");
|
||||
setFile(null);
|
||||
router.refresh();
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Une erreur est survenue");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-3xl px-4 py-8">
|
||||
<Link href="/member/muay-thai" className="mb-4 block text-sm text-primary-300">
|
||||
← Retour à mon espace
|
||||
</Link>
|
||||
<h1 className="mb-4 text-2xl font-bold">📄 Certificat médical</h1>
|
||||
|
||||
<div className="card mb-6 border-amber-500/30 bg-amber-500/5">
|
||||
<p className="text-sm text-amber-300">
|
||||
⚠️ Le certificat médical est obligatoire pour pratiquer le Muay
|
||||
Thai. Envoie une photo ou un scan de ton certificat, Juanito le
|
||||
validera rapidement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{latest && (
|
||||
<div className="card mb-6">
|
||||
<h2 className="mb-2 font-semibold">Statut actuel</h2>
|
||||
<StatusBadge status={latest.status} />
|
||||
<p className="mt-2 text-xs text-white/50">
|
||||
Envoyé le{" "}
|
||||
{new Date(latest.createdAt).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="card space-y-4">
|
||||
<h2 className="font-semibold">
|
||||
{latest?.status === "VALIDATED"
|
||||
? "Renvoyer un certificat (si le tien expire)"
|
||||
: "Envoyer votre certificat"}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-white/20 bg-white/5 py-8">
|
||||
<span className="mb-2 text-4xl">📎</span>
|
||||
{file ? (
|
||||
<p className="text-sm text-primary-300">{file.name}</p>
|
||||
) : (
|
||||
<p className="text-sm text-white/50">
|
||||
Photo ou PDF du certificat
|
||||
</p>
|
||||
)}
|
||||
<label className="btn-secondary mt-4 cursor-pointer">
|
||||
{file ? "Changer de fichier" : "Choisir un fichier"}
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.webp,.heic"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] || null);
|
||||
setError("");
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
{success && (
|
||||
<p className="rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-300">
|
||||
✅ {success}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary w-full"
|
||||
disabled={uploading || !file}
|
||||
>
|
||||
{uploading ? "Envoi en cours..." : "📤 Envoyer le certificat"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
switch (status) {
|
||||
case "VALIDATED":
|
||||
return (
|
||||
<span className="inline-flex rounded-full bg-emerald-500/20 px-3 py-1 text-sm font-semibold text-emerald-300">
|
||||
✓ Validé
|
||||
</span>
|
||||
);
|
||||
case "REJECTED":
|
||||
return (
|
||||
<span className="inline-flex rounded-full bg-red-500/20 px-3 py-1 text-sm font-semibold text-red-300">
|
||||
✗ Rejeté
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="inline-flex rounded-full bg-amber-500/20 px-3 py-1 text-sm font-semibold text-amber-300">
|
||||
⏳ En attente de validation
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { redirect } from "next/navigation";
|
||||
import CertificateUpload from "./certificate-upload";
|
||||
|
||||
export default async function CertificatePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) redirect("/login");
|
||||
|
||||
const certificates = await prisma.certificate.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return <CertificateUpload certificates={certificates} />;
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { redirect } from "next/navigation";
|
||||
import MuayThaiDashboard from "./dashboard";
|
||||
|
||||
export default async function MuayThaiPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) redirect("/login");
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: {
|
||||
muayThai: {
|
||||
include: {
|
||||
sessions: { orderBy: { date: "desc" }, take: 20 },
|
||||
},
|
||||
},
|
||||
certificates: { orderBy: { createdAt: "desc" } },
|
||||
absences: { orderBy: { date: "desc" } },
|
||||
homework: { orderBy: { createdAt: "desc" } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) redirect("/login");
|
||||
|
||||
const messages = await prisma.message.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
where: {
|
||||
OR: [{ category: null }, { category: "general" }, { category: "muay-thai" }],
|
||||
},
|
||||
include: {
|
||||
readBy: { where: { userId: user.id } },
|
||||
},
|
||||
});
|
||||
|
||||
const unreadCount = messages.filter((m) => m.readBy.length === 0).length;
|
||||
|
||||
return (
|
||||
<MuayThaiDashboard
|
||||
user={user}
|
||||
messages={messages.map((m) => ({ ...m, isRead: m.readBy.length > 0 }))}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user