"use client"; 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, type IcarusRunTelemetry } from "./icarus-game"; import type { IcarusScoreEntry } from "./page"; export function IcareView({ initialLeaderboard, initialOwnBestScore, isFrozen, }: { initialLeaderboard: IcarusScoreEntry[]; initialOwnBestScore: number | null; isFrozen: boolean; }) { const [leaderboard, setLeaderboard] = useState(initialLeaderboard); const [ownBestScore, setOwnBestScore] = useState(initialOwnBestScore); const [showRanking, setShowRanking] = useState(false); const refetch = useCallback(async () => { const supabase = createClient(); const { data: scores } = await supabase .from("icarus_scores") .select("user_id, best_score") .order("best_score", { ascending: false }); const userIds = (scores ?? []).map((row) => row.user_id); const { data: profiles } = userIds.length > 0 ? await supabase.from("profiles").select("id, pseudo, avatar_url").in("id", userIds) : { data: [] as { id: string; pseudo: string; avatar_url: string | null }[] }; const profileById = new Map((profiles ?? []).map((profile) => [profile.id, profile])); setLeaderboard( (scores ?? []).map((row) => ({ id: row.user_id, pseudo: profileById.get(row.user_id)?.pseudo ?? "Un Citoyen", avatar_url: profileById.get(row.user_id)?.avatar_url ?? null, best_score: row.best_score, })), ); }, []); useEffect(() => { const supabase = createClient(); let channel: ReturnType | null = null; let cancelled = false; waitForRealtimeAuth(supabase).then(() => { if (cancelled) return; channel = supabase .channel("icarus-scores") .on("postgres_changes", { event: "*", schema: "public", table: "icarus_scores" }, () => { refetch(); }) .subscribe(); }); return () => { cancelled = true; if (channel) supabase.removeChannel(channel); }; }, [refetch]); async function handleFinish(score: number, run: IcarusRunTelemetry) { const supabase = createClient(); const { data, error } = await supabase.rpc("submit_icarus_score", { p_score: score, p_run: run }); if (!error && data) { setOwnBestScore(data.best_score); refetch(); } } // 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 (

Le Vol d'Icare

{showRanking && (

Classement

{isFrozen && (

Les scores sont figés depuis le début du Tribunal.

)} {podiumProfiles.length > 0 && ( member.best_score} /> )} {rest.length > 0 && (
    {rest.map((entry) => (
  1. {entry.rank} {entry.pseudo} {entry.best_score}
  2. ))}
)} {ownBestScore !== null && (

Ton record : {ownBestScore}

)}

{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."}

)}
); }