3b64e1cdb5
Build and deploy / deploy (push) Successful in 35s
Nouveau mini-jeu à /corne : des avatars de Citoyens tombent dans un bac (physique matter-js), deux avatars de même taille qui se touchent fusionnent en un plus gros, jusqu'au débordement soutenu (5s de grâce, pièces clignotantes pour prévenir) qui met fin à la partie. Même mécanique de gloires que Le Vol d'Icare (record personnel, classement, gel + attribution automatique au Tribunal via pg_cron). Un palier de taille par Citoyen (pas les Archontes) plutôt qu'un nombre arbitraire : citizens[tier] donne l'association palier → Citoyen, fixe pour tout le monde (les Citoyens sont triés par pseudo côté serveur, un ordre déterministe). Rendu en DOM pour réutiliser le composant Avatar existant (étendu pour accepter une taille en pixels arbitraire) plutôt que de redessiner des images sur canvas — position et rotation de chaque pièce appliquées en impératif depuis la boucle physique, jamais via setState. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
import { CorneView } from "./corne-view";
|
|
|
|
export type MelonScoreEntry = {
|
|
id: string;
|
|
pseudo: string;
|
|
avatar_url: string | null;
|
|
best_score: number;
|
|
};
|
|
|
|
export type MemberRow = {
|
|
id: string;
|
|
pseudo: string;
|
|
avatar_url: string | null;
|
|
};
|
|
|
|
export default async function CornePage() {
|
|
const supabase = await createClient();
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
|
|
if (!user) {
|
|
redirect("/login");
|
|
}
|
|
|
|
const [{ data: scores }, { data: members }, { data: settings }] = await Promise.all([
|
|
supabase
|
|
.from("melon_scores")
|
|
.select("user_id, best_score")
|
|
.order("best_score", { ascending: false }),
|
|
supabase.from("profiles").select("id, pseudo, avatar_url, role").order("pseudo", { ascending: true }),
|
|
supabase.from("settings").select("tribunal_date").eq("id", true).single(),
|
|
]);
|
|
|
|
const memberById = new Map((members ?? []).map((member) => [member.id, member]));
|
|
|
|
const leaderboard: MelonScoreEntry[] = (scores ?? []).map((row) => ({
|
|
id: row.user_id,
|
|
pseudo: memberById.get(row.user_id)?.pseudo ?? "Un Citoyen",
|
|
avatar_url: memberById.get(row.user_id)?.avatar_url ?? null,
|
|
best_score: row.best_score,
|
|
}));
|
|
|
|
// Un palier de taille par Citoyen (pas les Archontes) : chacun est
|
|
// associé à une taille fixe pour toute la partie, voir corne-view.tsx.
|
|
const citizens: MemberRow[] = (members ?? [])
|
|
.filter((member) => member.role === "public")
|
|
.map(({ id, pseudo, avatar_url }) => ({ id, pseudo, avatar_url }));
|
|
|
|
const isFrozen = Boolean(settings?.tribunal_date && new Date() >= new Date(settings.tribunal_date));
|
|
const ownBestScore = leaderboard.find((entry) => entry.id === user.id)?.best_score ?? null;
|
|
|
|
return (
|
|
<CorneView
|
|
citizens={citizens}
|
|
initialLeaderboard={leaderboard}
|
|
initialOwnBestScore={ownBestScore}
|
|
isFrozen={isFrozen}
|
|
/>
|
|
);
|
|
}
|