131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { type Lang, dict } from "@/lib/i18n";
|
|
import {
|
|
fetchLeaderboard,
|
|
type LeaderEntry,
|
|
type LeaderTab,
|
|
} from "@/lib/leaderboard";
|
|
|
|
// 누적 랭킹 — 참가자 / AI 모델 2탭, 폴딩(기본 접힘)
|
|
// 매치 페이지(골드 카드 직후)에 삽입. defaultOpen=true 면 전체 페이지용 펼친 상태.
|
|
export default function Leaderboard({
|
|
lang = "ko",
|
|
defaultOpen = false,
|
|
showAllLink = true,
|
|
}: {
|
|
lang?: Lang;
|
|
defaultOpen?: boolean;
|
|
showAllLink?: boolean;
|
|
}) {
|
|
const t = dict(lang);
|
|
const [open, setOpen] = useState(defaultOpen);
|
|
const [tab, setTab] = useState<LeaderTab>("voters");
|
|
const [rows, setRows] = useState<LeaderEntry[]>([]);
|
|
|
|
// 펼쳐졌을 때 + 탭 변경 시에만 조회 (접힌 동안은 네트워크 안 씀)
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
let alive = true;
|
|
fetchLeaderboard(tab).then((r) => alive && setRows(r));
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, [open, tab]);
|
|
|
|
return (
|
|
<section className="mt-5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<h3 className="text-[16px] font-extrabold">{t.lbTitle}</h3>
|
|
{showAllLink && (
|
|
<Link
|
|
href={lang === "en" ? "/leaderboard?lang=en" : "/leaderboard"}
|
|
className="shrink-0 text-[12px] font-bold text-[var(--share)]"
|
|
>
|
|
{t.lbAll} →
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{open && (
|
|
<>
|
|
{/* 탭 */}
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
{(
|
|
[
|
|
["voters", t.lbTabVoters],
|
|
["ai", t.lbTabAI],
|
|
] as [LeaderTab, string][]
|
|
).map(([val, label]) => (
|
|
<button
|
|
key={val}
|
|
onClick={() => setTab(val)}
|
|
className={`rounded-xl py-2.5 text-[14px] font-extrabold transition ${
|
|
tab === val
|
|
? "bg-[var(--share)] text-white"
|
|
: "border border-[var(--line-d)] text-[var(--ink-muted)]"
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 헤더 라벨 */}
|
|
<div className="mt-3 flex items-center justify-between px-3 text-[11px] font-bold text-[var(--ink-muted)]">
|
|
<span>{tab === "ai" ? t.lbTabAI : t.lbTabVoters}</span>
|
|
<span>{t.lbPoints}</span>
|
|
</div>
|
|
|
|
{/* TOP 10 목록 */}
|
|
<ol className="mt-1.5 flex flex-col gap-1.5">
|
|
{rows.map((r, i) => {
|
|
const top3 = i < 3;
|
|
return (
|
|
<li
|
|
key={`${r.label}-${i}`}
|
|
className={`flex items-center gap-3 rounded-xl border px-3 py-2.5 ${
|
|
top3
|
|
? "border-[var(--share)]/50 bg-[rgba(166,94,255,0.08)]"
|
|
: "border-[var(--line-d)]"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`w-6 shrink-0 text-center font-mono text-[15px] font-extrabold tabular-nums ${
|
|
top3 ? "text-[var(--share)]" : "text-[var(--ink-muted)]"
|
|
}`}
|
|
>
|
|
{i + 1}
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate text-[15px] font-bold text-white">
|
|
{r.label}
|
|
</span>
|
|
<span className="shrink-0 font-mono text-[15px] font-extrabold tabular-nums text-white">
|
|
{r.points.toLocaleString()}
|
|
<span className="ml-0.5 text-[12px] text-[var(--ink-muted)]">
|
|
{t.lbUnit}
|
|
</span>
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</>
|
|
)}
|
|
|
|
{/* 폴딩 토글 — defaultOpen(전체 페이지)에서는 숨김 */}
|
|
{!defaultOpen && (
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
className="mt-3 flex w-full items-center justify-center gap-1 rounded-xl border border-[var(--line-d)] py-2.5 text-[13px] font-bold text-[var(--ink-muted)] transition active:scale-[0.99]"
|
|
>
|
|
{open ? t.lbClose : t.lbOpen}
|
|
<span aria-hidden>{open ? "▲" : "▼"}</span>
|
|
</button>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|