Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3aa2ea451c | |||
| 6922d33e39 |
@@ -179,7 +179,7 @@ Deuxième volet de l'audit (revue complète de tout le code applicatif, pas seul
|
|||||||
| `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` |
|
| `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` |
|
||||||
| `/login` | Email + mot de passe |
|
| `/login` | Email + mot de passe |
|
||||||
| `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email |
|
| `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email |
|
||||||
| `/leaderboard` | « L'Agora » — podium top 3 + liste, Realtime, contrôles de points pour les juges, date/décompte du Tribunal en en-tête (`src/lib/tribunal-date.ts`, partagé avec `/calendrier`). Joue le rôle de page d'accueil (`/` y redirige) : une rangée de raccourcis (`quick-links.tsx`, icônes réutilisées du menu du header) vers les pages d'activité — jeux, Mur, Urne, Crieur — juste au-dessus du classement |
|
| `/leaderboard` | « L'Agora » — podium top 3 + liste, Realtime, contrôles de points pour les juges, date/décompte du Tribunal en en-tête (`src/lib/tribunal-date.ts`, partagé avec `/calendrier`). Joue le rôle de page d'accueil (`/` y redirige) : une rangée de raccourcis (`quick-links.tsx`, icônes réutilisées du menu du header) vers les pages d'activité — jeux, Mur, Urne, Crieur — juste au-dessus du classement, avec une pastille façon iOS sur le raccourci du Mur (nombre de publications du jour, via `wall_notes_for_date()` — pas de lecture directe de `wall_notes`, dont la RLS ne laisserait voir que ses propres notes) |
|
||||||
| `/icare` | « Le Vol d'Icare » — Flappy Bird grec, record personnel all-time, scores figés au Tribunal, points attribués automatiquement |
|
| `/icare` | « Le Vol d'Icare » — Flappy Bird grec, record personnel all-time, scores figés au Tribunal, points attribués automatiquement |
|
||||||
| `/corne` | « La Corne d'Abondance » — jeu de fusion façon Watermelon Game (avatars aléatoires, physique `matter-js`), même mécanique de gloires que Le Vol d'Icare |
|
| `/corne` | « La Corne d'Abondance » — jeu de fusion façon Watermelon Game (avatars aléatoires, physique `matter-js`), même mécanique de gloires que Le Vol d'Icare |
|
||||||
| `/char` | « Le Jeu de l'Agora » — format paysage (projeté) : Le Char (2 colonnes de 3, boutons de duel), Le Gardien du Silence (tirage aléatoire, minuteur discret) et La Roulette (tirage au sort animé) |
|
| `/char` | « Le Jeu de l'Agora » — format paysage (projeté) : Le Char (2 colonnes de 3, boutons de duel), Le Gardien du Silence (tirage aléatoire, minuteur discret) et La Roulette (tirage au sort animé) |
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ export default async function LeaderboardPage() {
|
|||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ data: profiles, error }, { data: settings }] = await Promise.all([
|
const [{ data: profiles, error }, { data: settings }, { data: wallNotesToday }] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
.from("profiles")
|
.from("profiles")
|
||||||
.select("id, pseudo, avatar_url, points, previous_rank, role")
|
.select("id, pseudo, avatar_url, points, previous_rank, role")
|
||||||
.order("points", { ascending: false }),
|
.order("points", { ascending: false }),
|
||||||
supabase.from("settings").select("tribunal_date").eq("id", true).single(),
|
supabase.from("settings").select("tribunal_date").eq("id", true).single(),
|
||||||
|
// wall_notes_for_date() (jamais une lecture directe de wall_notes, dont
|
||||||
|
// la RLS ne laisserait voir que ses propres notes) : seul moyen de
|
||||||
|
// connaître le nombre total de publications du jour sans exposer qui a
|
||||||
|
// publié quoi — sert juste de compteur pour la pastille du raccourci.
|
||||||
|
supabase.rpc("wall_notes_for_date"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isJudge = profiles?.find((p) => p.id === user.id)?.role === "judge";
|
const isJudge = profiles?.find((p) => p.id === user.id)?.role === "judge";
|
||||||
@@ -37,7 +42,7 @@ export default async function LeaderboardPage() {
|
|||||||
{tribunalDate && <p className="text-xs text-marble/50">{formatTribunalDateTime(tribunalDate)}</p>}
|
{tribunalDate && <p className="text-xs text-marble/50">{formatTribunalDateTime(tribunalDate)}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<QuickLinks />
|
<QuickLinks wallNoteCount={wallNotesToday?.length ?? 0} />
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="text-sm text-oxblood">
|
<p role="alert" className="text-sm text-oxblood">
|
||||||
|
|||||||
@@ -15,19 +15,33 @@ const LINKS = [
|
|||||||
{ href: "/journal", label: "Le Crieur", icon: IconScroll },
|
{ href: "/journal", label: "Le Crieur", icon: IconScroll },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function QuickLinks() {
|
export function QuickLinks({ wallNoteCount }: { wallNoteCount: number }) {
|
||||||
return (
|
return (
|
||||||
<nav aria-label="Raccourcis" className="mb-6 grid grid-cols-3 gap-2 sm:grid-cols-6 sm:gap-3">
|
<nav aria-label="Raccourcis" className="mb-6 grid grid-cols-3 gap-2 sm:grid-cols-6 sm:gap-3">
|
||||||
{LINKS.map(({ href, label, icon: Icon }) => (
|
{LINKS.map(({ href, label, icon: Icon }) => {
|
||||||
<Link
|
// Pastille "façon iPhone" (nombre de publications du jour) : pour
|
||||||
key={href}
|
// l'instant seul le Mur en a une, les autres raccourcis n'ont pas
|
||||||
href={href}
|
// d'équivalent "à consulter aujourd'hui" aussi direct.
|
||||||
className="marble-surface flex flex-col items-center gap-1.5 rounded-xl border border-gold/25 px-2 py-3 text-center shadow-sm transition hover:border-gold/50 hover:shadow-md"
|
const badge = href === "/mur" ? wallNoteCount : 0;
|
||||||
>
|
return (
|
||||||
<Icon className="h-6 w-6 text-gold" />
|
<Link
|
||||||
<span className="text-[0.65rem] leading-tight font-medium text-text-marble sm:text-xs">{label}</span>
|
key={href}
|
||||||
</Link>
|
href={href}
|
||||||
))}
|
className="marble-surface relative flex flex-col items-center gap-1.5 rounded-xl border border-gold/25 px-2 py-3 text-center shadow-sm transition hover:border-gold/50 hover:shadow-md"
|
||||||
|
>
|
||||||
|
{badge > 0 && (
|
||||||
|
<span
|
||||||
|
className="absolute -top-1.5 -right-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-oxblood px-1 text-[0.65rem] font-bold text-marble shadow"
|
||||||
|
aria-label={`${badge} publication${badge > 1 ? "s" : ""} aujourd'hui sur le Mur de la Honte`}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Icon className="h-6 w-6 text-gold" />
|
||||||
|
<span className="text-[0.65rem] leading-tight font-medium text-text-marble sm:text-xs">{label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,20 +308,30 @@ export function MurView({
|
|||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
try {
|
try {
|
||||||
const blob = await resizeImageToBlob(imageFile);
|
const blob = await resizeImageToBlob(imageFile);
|
||||||
const path = `${crypto.randomUUID()}.webp`;
|
// L'extension/le Content-Type suivent le type RÉEL du blob rendu par
|
||||||
|
// canvas.toBlob (pas "image/webp" en dur) : certains navigateurs
|
||||||
|
// (Safari notamment, webp encodé via canvas longtemps mal supporté)
|
||||||
|
// retombent silencieusement sur du PNG malgré le type demandé —
|
||||||
|
// uploader ce PNG sous une étiquette "webp" trompeuse pouvait faire
|
||||||
|
// échouer des vérifications en aval qui, elles, regardent le vrai
|
||||||
|
// contenu (l'optimiseur d'image de next/image par exemple).
|
||||||
|
const extension = blob.type === "image/png" ? "png" : blob.type === "image/jpeg" ? "jpg" : "webp";
|
||||||
|
const path = `${crypto.randomUUID()}.${extension}`;
|
||||||
const { error: uploadError } = await supabase.storage
|
const { error: uploadError } = await supabase.storage
|
||||||
.from("wall-images")
|
.from("wall-images")
|
||||||
.upload(path, blob, { contentType: "image/webp" });
|
.upload(path, blob, { contentType: blob.type || "image/webp" });
|
||||||
if (uploadError) {
|
if (uploadError) {
|
||||||
setPosting(false);
|
setPosting(false);
|
||||||
setError("Impossible d'envoyer l'image, réessaie.");
|
console.error("Échec de l'envoi de l'image sur le Mur :", uploadError);
|
||||||
|
setError(`Envoi de l'image impossible (${uploadError.message}). Réessaie.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data: publicUrlData } = supabase.storage.from("wall-images").getPublicUrl(path);
|
const { data: publicUrlData } = supabase.storage.from("wall-images").getPublicUrl(path);
|
||||||
imageUrl = publicUrlData.publicUrl;
|
imageUrl = publicUrlData.publicUrl;
|
||||||
} catch {
|
} catch (err) {
|
||||||
setPosting(false);
|
setPosting(false);
|
||||||
setError("Impossible de traiter l'image, réessaie.");
|
console.error("Échec du traitement de l'image sur le Mur :", err);
|
||||||
|
setError("Traitement de l'image impossible (format non supporté ?) — réessaie sans image ou avec une autre photo.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,12 +343,15 @@ export function MurView({
|
|||||||
setPosting(false);
|
setPosting(false);
|
||||||
|
|
||||||
if (rpcError) {
|
if (rpcError) {
|
||||||
|
console.error("Échec de post_wall_note :", rpcError);
|
||||||
setError(
|
setError(
|
||||||
rpcError.message.includes("already posted")
|
rpcError.message.includes("already posted")
|
||||||
? "Tu as déjà publié une note aujourd'hui."
|
? "Tu as déjà publié une note aujourd'hui."
|
||||||
: rpcError.message.includes("wall full")
|
: rpcError.message.includes("wall full")
|
||||||
? "Le mur est déjà complet pour aujourd'hui."
|
? "Le mur est déjà complet pour aujourd'hui."
|
||||||
: "Une erreur est survenue, réessaie.",
|
: rpcError.message.includes("image invalide")
|
||||||
|
? "L'image n'a pas pu être validée côté serveur — réessaie sans image ou avec une autre photo."
|
||||||
|
: `Publication impossible (${rpcError.message}). Réessaie.`,
|
||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user