2026-04-11 15:17:56 +02:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
|
|
|
|
type PublicProfile = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
stats: {
|
|
|
|
|
total: number;
|
|
|
|
|
wins: number;
|
|
|
|
|
winRate: number;
|
|
|
|
|
avgClicks: number | null;
|
|
|
|
|
bestClicks: number | null;
|
|
|
|
|
bestTime: number | null;
|
|
|
|
|
avgTime: number | null;
|
|
|
|
|
solo: { games: number; wins: number };
|
|
|
|
|
multi: { games: number; wins: number };
|
|
|
|
|
daily: { games: number; wins: number };
|
|
|
|
|
blitz: { games: number; wins: number };
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function fmt(s: number): string {
|
|
|
|
|
const m = Math.floor(s / 60);
|
|
|
|
|
const sec = Math.floor(s % 60);
|
|
|
|
|
return `${m}:${String(sec).padStart(2, "0")}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MODE_LABELS: Record<string, string> = {
|
|
|
|
|
solo: "Solo",
|
|
|
|
|
multi: "Multijoueur",
|
|
|
|
|
daily: "Défi du jour",
|
|
|
|
|
blitz: "Blitz",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function PublicProfileScreen({
|
|
|
|
|
userId,
|
|
|
|
|
onBack,
|
|
|
|
|
}: {
|
|
|
|
|
userId: string;
|
|
|
|
|
onBack: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [profile, setProfile] = useState<PublicProfile | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [notFound, setNotFound] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetch(`/api/users/${userId}`)
|
|
|
|
|
.then((r) => {
|
2026-04-11 22:25:34 +02:00
|
|
|
if (r.status === 404) {
|
|
|
|
|
setNotFound(true);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-04-11 15:17:56 +02:00
|
|
|
return r.json();
|
|
|
|
|
})
|
2026-04-11 22:25:34 +02:00
|
|
|
.then((d) => {
|
|
|
|
|
if (d) setProfile(d);
|
|
|
|
|
})
|
2026-04-11 15:17:56 +02:00
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
}, [userId]);
|
|
|
|
|
|
2026-04-11 22:25:34 +02:00
|
|
|
const statCard =
|
|
|
|
|
"bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-3 sm:p-3.5 text-center flex flex-col gap-1";
|
|
|
|
|
const btnGhost =
|
|
|
|
|
"min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
2026-04-11 15:17:56 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col max-w-2xl mx-auto px-4 pb-10">
|
|
|
|
|
<div className="flex items-center gap-3 py-4">
|
2026-04-11 22:25:34 +02:00
|
|
|
<button className={btnGhost} onClick={onBack}>
|
|
|
|
|
← Retour
|
|
|
|
|
</button>
|
2026-04-11 15:17:56 +02:00
|
|
|
{profile && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-[#7c3aed] text-white flex items-center justify-center text-base sm:text-lg font-bold shrink-0">
|
|
|
|
|
{profile.name[0].toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-base sm:text-xl font-bold">{profile.name}</h2>
|
|
|
|
|
<p className="text-[10px] text-[#555]">
|
2026-04-11 22:25:34 +02:00
|
|
|
Membre depuis{" "}
|
|
|
|
|
{new Date(profile.createdAt).toLocaleDateString("fr-FR", {
|
|
|
|
|
month: "long",
|
|
|
|
|
year: "numeric",
|
|
|
|
|
})}
|
2026-04-11 15:17:56 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-11 22:25:34 +02:00
|
|
|
{loading && (
|
|
|
|
|
<div className="text-[#888] text-sm py-10 text-center animate-pulse">
|
|
|
|
|
Chargement...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{notFound && (
|
|
|
|
|
<p className="text-[#888] text-sm py-10 text-center">
|
|
|
|
|
Joueur introuvable.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-04-11 15:17:56 +02:00
|
|
|
|
|
|
|
|
{profile && (
|
|
|
|
|
<div className="flex flex-col gap-5">
|
|
|
|
|
{/* Stats principales */}
|
|
|
|
|
<div className="grid grid-cols-3 gap-2 sm:gap-2.5">
|
|
|
|
|
<div className={statCard}>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-lg sm:text-[22px] font-black">
|
|
|
|
|
{profile.stats.total}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Parties
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
<div className={statCard}>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-lg sm:text-[22px] font-black">
|
|
|
|
|
{profile.stats.wins}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Victoires
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
<div className={statCard}>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-lg sm:text-[22px] font-black">
|
|
|
|
|
{profile.stats.winRate}%
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Win rate
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
<div className={statCard}>
|
|
|
|
|
<span className="text-lg sm:text-[22px] font-black">
|
|
|
|
|
{profile.stats.avgClicks ?? "-"}
|
|
|
|
|
</span>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Clics moy.
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
<div className={`${statCard} border-[#7c3aed]`}>
|
|
|
|
|
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">
|
|
|
|
|
{profile.stats.bestClicks ?? "-"}
|
|
|
|
|
</span>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Meilleur clics
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
<div className={`${statCard} border-[#7c3aed]`}>
|
|
|
|
|
<span className="text-lg sm:text-[22px] font-black text-[#7c3aed]">
|
|
|
|
|
{profile.stats.bestTime ? fmt(profile.stats.bestTime) : "-"}
|
|
|
|
|
</span>
|
2026-04-11 22:25:34 +02:00
|
|
|
<span className="text-[9px] sm:text-[11px] uppercase tracking-wider text-[#888]">
|
|
|
|
|
Meilleur temps
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Stats par mode */}
|
|
|
|
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-2">
|
2026-04-11 22:25:34 +02:00
|
|
|
<h3 className="text-[10px] font-bold text-[#555] uppercase tracking-widest mb-1">
|
|
|
|
|
Par mode
|
|
|
|
|
</h3>
|
2026-04-11 15:17:56 +02:00
|
|
|
{(["solo", "multi", "daily", "blitz"] as const).map((m) => {
|
|
|
|
|
const s = profile.stats[m];
|
|
|
|
|
if (s.games === 0) return null;
|
|
|
|
|
return (
|
2026-04-11 22:25:34 +02:00
|
|
|
<div
|
|
|
|
|
key={m}
|
|
|
|
|
className="flex items-center justify-between text-sm py-1.5 border-b border-[#2e2e2e] last:border-0"
|
|
|
|
|
>
|
|
|
|
|
<span className="font-semibold text-[#f0f0f0]">
|
|
|
|
|
{MODE_LABELS[m]}
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
<div className="flex items-center gap-3 text-[#888] text-xs">
|
2026-04-11 22:25:34 +02:00
|
|
|
<span>
|
|
|
|
|
<span className="text-[#f0f0f0] font-bold">{s.wins}</span>{" "}
|
|
|
|
|
victoires
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
<span className="text-[#f0f0f0] font-bold">
|
|
|
|
|
{s.games}
|
|
|
|
|
</span>{" "}
|
|
|
|
|
parties
|
|
|
|
|
</span>
|
2026-04-11 15:17:56 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|