Passe Le Vol d'Icare en plein écran avec podium animé pour le top 3
Build and deploy / deploy (push) Successful in 35s
Build and deploy / deploy (push) Successful in 35s
Le classement (dont un nouveau podium top 3, réutilisant celui de L'Agora) vit maintenant dans une modale entièrement détachée de la mise en page du jeu : une mise à jour Realtime du classement pendant une partie ne peut plus redimensionner le canvas et casser la partie en cours. Le jeu occupe tout l'écran disponible (contain avec letterboxing) au lieu d'une carte fixe en haut de page. Corrige aussi le score du classement, en bleu au lieu de doré (contraste insuffisant sur fond marbre clair). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Avatar } from "@/components/avatar";
|
||||
import { Podium } from "@/components/podium";
|
||||
import { IconClose, IconTrophy } from "@/components/icons";
|
||||
import { computeRanksBy } from "@/lib/ranking";
|
||||
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||
import { IcarusGame } from "./icarus-game";
|
||||
@@ -18,6 +20,7 @@ export function IcareView({
|
||||
}) {
|
||||
const [leaderboard, setLeaderboard] = useState(initialLeaderboard);
|
||||
const [ownBestScore, setOwnBestScore] = useState(initialOwnBestScore);
|
||||
const [showRanking, setShowRanking] = useState(false);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
const supabase = createClient();
|
||||
@@ -73,45 +76,89 @@ export function IcareView({
|
||||
}
|
||||
}
|
||||
|
||||
// Le classement vit dans une modale, complètement hors du flux de mise en
|
||||
// page du jeu : sans ça, une mise à jour Realtime pendant une partie (le
|
||||
// score d'un autre joueur qui change) redimensionnait le canvas et cassait
|
||||
// la partie en cours.
|
||||
const ranked = useMemo(() => computeRanksBy(leaderboard, (a, b) => b.best_score - a.best_score), [leaderboard]);
|
||||
const podiumProfiles = ranked.filter((p) => p.rank <= 3);
|
||||
const rest = ranked.filter((p) => p.rank > 3);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{isFrozen && (
|
||||
<p className="marble-surface rounded-lg border border-gold/40 px-4 py-2 text-center text-sm text-text-marble">
|
||||
Les scores sont figés depuis le début du Tribunal.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{ranked.length > 0 && (
|
||||
<ol className="flex flex-col gap-1.5">
|
||||
{ranked.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="marble-surface flex items-center gap-3 rounded-lg border border-gold/25 px-3 py-2 shadow-sm"
|
||||
>
|
||||
<span className="w-5 shrink-0 text-center font-heading text-sm text-text-mut">{entry.rank}</span>
|
||||
<Avatar pseudo={entry.pseudo} avatarUrl={entry.avatar_url} size="xs" />
|
||||
<span className="flex-1 truncate text-sm font-medium text-text-marble">{entry.pseudo}</span>
|
||||
<span className="font-heading text-sm font-semibold text-gold-bright">{entry.best_score}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
|
||||
{ownBestScore !== null && (
|
||||
<p className="text-center font-heading text-sm tracking-wide text-marble">
|
||||
Ton record : <span className="text-gold-bright">{ownBestScore}</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="relative flex flex-1 flex-col bg-ink">
|
||||
<div className="flex items-center justify-between px-4 py-2">
|
||||
<h1 className="font-heading text-sm tracking-[0.15em] text-gold-bright uppercase">Le Vol d'Icare</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRanking(true)}
|
||||
className="flex items-center gap-1.5 rounded-md border border-gold/30 px-3 py-1.5 text-xs font-medium text-marble/80 uppercase tracking-wide transition hover:bg-gold/10"
|
||||
>
|
||||
<IconTrophy className="h-4 w-4" />
|
||||
Classement
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IcarusGame onFinish={handleFinish} />
|
||||
|
||||
<p className="text-center text-xs text-marble/60">
|
||||
{isFrozen
|
||||
? "Les gloires du podium ont été attribuées automatiquement au début du Tribunal."
|
||||
: "Au début du Tribunal, les scores seront figés et le podium recevra des gloires automatiquement — aucun Archonte n'a besoin d'intervenir."}
|
||||
</p>
|
||||
{showRanking && (
|
||||
<div className="fixed inset-0 z-30 flex items-center justify-center bg-ink/80 p-4 backdrop-blur-sm">
|
||||
<div className="marble-surface flex max-h-[85vh] w-full max-w-md flex-col overflow-hidden rounded-2xl border border-gold/40 shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-gold/20 px-4 py-3">
|
||||
<h2 className="font-heading text-sm tracking-[0.1em] text-text-marble uppercase">Classement</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRanking(false)}
|
||||
className="rounded-md p-1 text-text-mut transition hover:bg-gold/10 hover:text-text-marble"
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<IconClose className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-4 py-4">
|
||||
{isFrozen && (
|
||||
<p className="mb-4 rounded-lg border border-gold/40 px-4 py-2 text-center text-sm text-text-marble">
|
||||
Les scores sont figés depuis le début du Tribunal.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{podiumProfiles.length > 0 && (
|
||||
<Podium profiles={podiumProfiles} renderValue={(member) => member.best_score} />
|
||||
)}
|
||||
|
||||
{rest.length > 0 && (
|
||||
<ol className="flex flex-col gap-1.5">
|
||||
{rest.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center gap-3 rounded-lg border border-gold/25 px-3 py-2"
|
||||
>
|
||||
<span className="w-5 shrink-0 text-center font-heading text-sm text-text-mut">
|
||||
{entry.rank}
|
||||
</span>
|
||||
<Avatar pseudo={entry.pseudo} avatarUrl={entry.avatar_url} size="xs" />
|
||||
<span className="flex-1 truncate text-sm font-medium text-text-marble">{entry.pseudo}</span>
|
||||
<span className="font-heading text-sm font-semibold text-sea">{entry.best_score}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
|
||||
{ownBestScore !== null && (
|
||||
<p className="mt-4 text-center font-heading text-sm tracking-wide text-text-marble">
|
||||
Ton record : <span className="text-sea">{ownBestScore}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-center text-xs text-text-mut">
|
||||
{isFrozen
|
||||
? "Les gloires du podium ont été attribuées automatiquement au début du Tribunal."
|
||||
: "Au début du Tribunal, les scores seront figés et le podium recevra des gloires automatiquement — aucun Archonte n'a besoin d'intervenir."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user