Files
MineGeo/app/leaderboard/page.tsx

218 lines
7.7 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.
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
import { Header, Footer } from "@/components/Shell";
import {
LeaderboardEmpty,
LeaderboardTable,
WinnersPodium,
} from "@/components/Leaderboard";
import { PixelTrophy } from "@/components/PixelArt";
import { leaderboard } from "@/lib/leaderboard";
import { user } from "@/lib/supabase";
import {
formatScore,
leaderboardDifficulties,
leaderboardFilters,
leaderboardHref,
} from "@/shared/leaderboard";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Leaderboard",
description:
"Meet MineGeo’s top explorers. Compare the best five-round scores across easy, normal, and hard Minecraft expeditions.",
};
export default async function LeaderboardPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { difficulty, search, page } = leaderboardFilters(await searchParams);
const [data, account] = await Promise.all([
leaderboard({ difficulty, search, page }),
user(),
]);
return (
<>
<Header />
<main className="section page-content leaderboard-page" id="main-content">
<Link href="/" className="text-link leaderboard-back">
<ArrowLeft size={14} /> Back to home
</Link>
<header className="leaderboard-intro">
<div>
<span className="eyebrow">THE EXPLORERS’ BOARD</span>
<h1>Every block counts.</h1>
<p className="lead">
The sharpest eyes in the Overworld.
<br />
Five rounds. 25,000 possible points. Where do you stand?
</p>
</div>
<PixelTrophy className="intro-trophy" />
</header>
<div className="leaderboard-toolbar">
<nav
className="leaderboard-filters"
aria-label="Filter leaderboard by difficulty"
>
{leaderboardDifficulties.map((value) => (
<Link
key={value}
href={leaderboardHref(value, search)}
aria-current={difficulty === value ? "page" : undefined}
>
{value === "all"
? "All difficulties"
: value[0].toUpperCase() + value.slice(1)}
</Link>
))}
</nav>
<span className="leaderboard-period">All time</span>
</div>
{data ? (
<>
{data.leaders.length > 0 && !search && data.page === 1 && (
<WinnersPodium leaders={data.leaders} />
)}
<section
className="rankings-board"
aria-labelledby="rankings-heading"
>
<div className="rankings-heading">
<div>
<h2 id="rankings-heading">The rankings</h2>
<p>
{formatScore(data.total_players)}{" "}
{data.total_players === 1 ? "explorer" : "explorers"} ·{" "}
{formatScore(data.total_games)} completed{" "}
{data.total_games === 1 ? "expedition" : "expeditions"}
</p>
</div>
<form
action="/leaderboard"
className="leaderboard-search"
role="search"
>
{difficulty !== "all" && (
<input type="hidden" name="difficulty" value={difficulty} />
)}
<label className="sr-only" htmlFor="player-search">
Search players
</label>
<input
key={`${difficulty}-${search}`}
id="player-search"
type="search"
name="q"
defaultValue={search}
placeholder="Find an explorer…"
maxLength={64}
/>
<button type="submit" aria-label="Search players">
<Search size={17} />
</button>
</form>
</div>
{search && (
<div className="search-summary">
<span>
{formatScore(data.matching_players)}{" "}
{data.matching_players === 1 ? "result" : "results"} for “
{search}”
</span>
<Link href={leaderboardHref(difficulty)}>Clear search ×</Link>
</div>
)}
{data.entries.length ? (
<LeaderboardTable
entries={data.entries}
playerId={account?.id}
/>
) : (
<LeaderboardEmpty search={!!search} />
)}
{data.matching_players > 0 && (
<div className="leaderboard-pagination">
<span>
Showing {formatScore((data.page - 1) * 25 + 1)}–
{formatScore(
Math.min(data.page * 25, data.matching_players),
)}{" "}
of {formatScore(data.matching_players)}
</span>
<nav aria-label="Leaderboard pages">
{data.page > 1 ? (
<Link
aria-label="Previous page"
href={leaderboardHref(
difficulty,
search,
data.page - 1,
)}
>
<ArrowLeft size={15} /> Previous
</Link>
) : (
<span aria-disabled="true">
<ArrowLeft size={15} /> Previous
</span>
)}
<span className="page-number">
{data.page} / {data.pages}
</span>
{data.page < data.pages ? (
<Link
aria-label="Next page"
href={leaderboardHref(
difficulty,
search,
data.page + 1,
)}
>
Next <ArrowRight size={15} />
</Link>
) : (
<span aria-disabled="true">
Next <ArrowRight size={15} />
</span>
)}
</nav>
</div>
)}
</section>
</>
) : (
<LeaderboardEmpty unavailable />
)}
<aside className="leaderboard-rules">
<div>
<span className="eyebrow">HOW THE BOARD WORKS</span>
<h2>A place earned, one guess at a time.</h2>
</div>
<div>
<p>
Your best completed expedition sets your rank. Ties are broken by
average score, then completed games. Difficulty filters rank only
games in that mode.
</p>
<p>
Log in before you play. All five rounds must be finished to count;
guest games stay off the board.
</p>
<Link href={account ? "/servers" : "/log-in"} className="text-link">
{account
? "Start your next expedition"
: "Log in to join the board"}{" "}
<ArrowRight size={15} />
</Link>
</div>
</aside>
</main>
<Footer />
</>
);
}