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
+164
View File
@@ -0,0 +1,164 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Header from "@/components/Header";
import { signOut } from "next-auth/react";
export default function YogaDashboard({ user, messages, unreadCount }: any) {
const router = useRouter();
const profile = user?.yoga;
const attendances = profile?.attendances || [];
const remaining = profile?.sessionsRemaining ?? 0;
const pricePerSession = 9;
const sessionsDone = attendances.length;
const totalAttendedCost = sessionsDone * pricePerSession;
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">
Ton espace Yog4Fight
</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>
{/* Compteur de séances */}
<section className="mb-6">
<div className="card text-center">
<p className="text-sm text-white/60">Séances restantes</p>
<div className="my-2 flex items-center justify-center gap-1 text-5xl font-extrabold text-emerald-300">
{remaining}
<span className="text-xl">
séance{remaining > 1 ? "s" : ""}
</span>
</div>
<p className="text-sm text-white/50">
{sessionsDone} séance{sessionsDone > 1 ? "s" : ""} suivie
{sessionsDone > 1 ? "s" : ""}
</p>
</div>
</section>
{/* Coût */}
<section className="mb-6">
<div className="grid grid-cols-2 gap-3">
<div className="card text-center">
<p className="text-xs text-white/50">Prix par séance</p>
<p className="mt-1 text-xl font-bold text-emerald-300">
{pricePerSession} €
</p>
</div>
<div className="card text-center">
<p className="text-xs text-white/50">Coût si assidu</p>
<p className="mt-1 text-xl font-bold text-emerald-300">
Tarif dégressif
</p>
</div>
</div>
<p className="mt-2 text-center text-xs text-white/50">
Demande à Juanito le tarif dégressif qui s&apos;applique à
l&apos;assiduité 💪
</p>
</section>
{/* 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-emerald-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.
</p>
)}
{messages.map((msg: any) => (
<div
key={msg.id}
className={`card ${!msg.isRead ? "border-emerald-500/40 bg-emerald-500/5" : ""}`}
onClick={() => {
if (!msg.isRead) {
fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messageId: msg.id }),
}).then(() => router.refresh());
}
}}
>
<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")}
</p>
</div>
{!msg.isRead && (
<span className="mt-1 h-3 w-3 shrink-0 rounded-full bg-emerald-500" />
)}
</div>
</div>
))}
</div>
</section>
{/* Historique séances */}
<section className="card">
<h3 className="mb-3 font-bold">📋 Mes séances suivies</h3>
{attendances.length === 0 && (
<p className="text-sm text-white/50">
Aucune séance enregistrée pour l&apos;instant.
</p>
)}
<div className="max-h-64 space-y-2 overflow-y-auto">
{attendances.map((a: any) => (
<div
key={a.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(a.date).toLocaleDateString("fr-FR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
{a.pricePaid !== null && (
<span className="tabular-nums text-emerald-300 text-xs">
{a.pricePaid} €
</span>
)}
</div>
))}
</div>
</section>
</div>
</main>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import YogaDashboard from "./dashboard";
export default async function YogaPage() {
const session = await getServerSession(authOptions);
if (!session?.user) redirect("/login");
const user = await prisma.user.findUnique({
where: { id: session.user.id },
include: {
yoga: {
include: {
attendances: { orderBy: { date: "desc" } },
},
},
},
});
if (!user) redirect("/login");
const messages = await prisma.message.findMany({
orderBy: { createdAt: "desc" },
where: {
OR: [{ category: null }, { category: "general" }, { category: "yoga" }],
},
include: {
readBy: { where: { userId: user.id } },
},
});
const unreadCount = messages.filter((m) => m.readBy.length === 0).length;
return (
<YogaDashboard
user={user}
messages={messages.map((m) => ({ ...m, isRead: m.readBy.length > 0 }))}
unreadCount={unreadCount}
/>
);
}