Initial commit: JuanitoSport - Muay Thai & Yog4Fight PWA

This commit is contained in:
Wycz
2026-09-08 08:25:33 +02:00
commit a69ca86330
70 changed files with 4301 additions and 0 deletions
@@ -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>
);
}
}
+17
View File
@@ -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} />;
}