UI-UX/src/app/ranking/page.tsx

125 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo, useState } from "react";
import { Sparkles } from "lucide-react";
import Top3Podium from "@/components/ranking/Top3Podium";
import RankingRow from "@/components/ranking/RankingRow";
import DebutLineDivider from "@/components/ranking/DebutLineDivider";
import VoteModal from "@/components/VoteModal";
import Countdown from "@/components/ui/Countdown";
import { ARTISTS, getActivityEndTime, sortArtists } from "@/lib/mock-data";
import type { Artist } from "@/types/artist";
export default function RankingPage() {
const [voteTarget, setVoteTarget] = useState<Artist | null>(null);
const sorted = useMemo(() => sortArtists(ARTISTS, "votes"), []);
const endTime = useMemo(() => getActivityEndTime(), []);
const top3 = sorted.slice(0, 3);
const top4to12 = sorted.slice(3, 12);
const candidates = sorted.slice(12);
// 计算 #13 与第 12 名的差距,用于"救援投票"
const debutCutoff = sorted[11]?.votes ?? 0;
const handleVote = async (a: Artist, count: number) => {
await new Promise((r) => setTimeout(r, 400));
console.log(`Vote: ${a.name} × ${count}`);
setVoteTarget(null);
};
return (
<>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
{/* 头部 */}
<div className="text-center mb-10">
<p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-300 mb-2">
Live Ranking · 2026
</p>
<h1 className="font-logo text-4xl sm:text-5xl tracking-[0.3em] uppercase glow-text-purple mb-3 inline-flex items-baseline">
Top
<span className="text-purple-300 mx-2 text-2xl sm:text-3xl"></span>
35
</h1>
<p className="text-white/55 text-sm mb-5">35 · </p>
<div className="flex justify-center">
<Countdown endTime={endTime} compact />
</div>
</div>
{/* Top3 领奖台 */}
<Top3Podium top3={top3} />
{/* Top 4-12 标题 */}
<div className="flex items-center gap-2 mt-10 mb-4">
<Sparkles size={14} className="text-purple-300" />
<h2 className="font-display text-sm tracking-[0.25em] text-white uppercase">
· Top 4 ~ 12
</h2>
</div>
{/* 表头 */}
<div className="hidden sm:grid grid-cols-[64px_64px_1fr_100px_140px_110px] gap-4 px-3 py-2 text-[10px] tracking-widest uppercase text-white/40 font-label">
<span className="text-center"></span>
<span></span>
<span></span>
<span className="text-right"></span>
<span className="text-right"></span>
<span className="text-center"></span>
</div>
{/* Top4-12 行 */}
<div className="space-y-2">
{top4to12.map((a, idx) => {
const prev = idx === 0 ? top3[2] : top4to12[idx - 1];
const gap = prev ? prev.votes - a.votes : undefined;
return (
<RankingRow
key={a.id}
artist={a}
gapAbove={gap}
onVote={setVoteTarget}
/>
);
})}
</div>
{/* 出道线 */}
<DebutLineDivider />
{/* 候补区 */}
<div className="space-y-2">
{candidates.map((a, idx) => {
const prev = idx === 0 ? top4to12[top4to12.length - 1] : candidates[idx - 1];
const gap = prev ? prev.votes - a.votes : undefined;
const isRescue = idx === 0; // 第 13 位
const gapToDebut = isRescue ? debutCutoff - a.votes + 1 : undefined;
return (
<RankingRow
key={a.id}
artist={a}
gapAbove={gap}
gapToDebut={gapToDebut}
isRescue={isRescue}
onVote={setVoteTarget}
/>
);
})}
</div>
{/* 底部提示 */}
<p className="text-xs text-white/35 text-center mt-10">
·
</p>
</div>
<VoteModal
artist={voteTarget}
onClose={() => setVoteTarget(null)}
onConfirm={handleVote}
/>
</>
);
}