Improve MineGeo interface and functionality
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
export default function LoadingLeaderboard() {
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
export default async function LoadingLeaderboard() {
|
||||
const { t } = await getI18n();
|
||||
return (
|
||||
<main
|
||||
className="section page-content leaderboard-page"
|
||||
aria-busy="true"
|
||||
aria-label="Loading leaderboard"
|
||||
aria-label={t("Loading leaderboard")}
|
||||
>
|
||||
<span className="eyebrow">THE EXPLORERS’ BOARD</span>
|
||||
<h1>Every block counts.</h1>
|
||||
<span className="eyebrow">{t("THE EXPLORERS’ BOARD")}</span>
|
||||
<h1>{t("Every block counts.")}</h1>
|
||||
<p className="lead" role="status">
|
||||
Fetching the explorers’ scores…
|
||||
{t("Fetching the explorers’ scores…")}
|
||||
</p>
|
||||
<div className="leaderboard-skeleton" aria-hidden="true">
|
||||
{Array.from({ length: 6 }, (_, index) => (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
|
||||
@@ -11,24 +12,28 @@ 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 async function generateMetadata(): Promise<Metadata> {
|
||||
const { t } = await getI18n();
|
||||
return {
|
||||
title: t("Leaderboard"),
|
||||
description: t(
|
||||
"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 { t, formatNumber } = await getI18n();
|
||||
const { difficulty, search, page } = leaderboardFilters(await searchParams);
|
||||
const [data, account] = await Promise.all([
|
||||
leaderboard({ difficulty, search, page }),
|
||||
@@ -39,16 +44,16 @@ export default async function LeaderboardPage({
|
||||
<Header />
|
||||
<main className="section page-content leaderboard-page" id="main-content">
|
||||
<Link href="/" className="text-link leaderboard-back">
|
||||
<ArrowLeft size={14} /> Back to home
|
||||
<ArrowLeft size={14} /> {t("Back to home")}
|
||||
</Link>
|
||||
<header className="leaderboard-intro">
|
||||
<div>
|
||||
<span className="eyebrow">THE EXPLORERS’ BOARD</span>
|
||||
<h1>Every block counts.</h1>
|
||||
<span className="eyebrow">{t("THE EXPLORERS’ BOARD")}</span>
|
||||
<h1>{t("Every block counts.")}</h1>
|
||||
<p className="lead">
|
||||
The sharpest eyes in the Overworld.
|
||||
{t("The sharpest eyes in the Overworld.")}
|
||||
<br />
|
||||
Five rounds. 25,000 possible points. Where do you stand?
|
||||
{t("Five rounds. 25,000 possible points. Where do you stand?")}
|
||||
</p>
|
||||
</div>
|
||||
<PixelTrophy className="intro-trophy" />
|
||||
@@ -56,7 +61,7 @@ export default async function LeaderboardPage({
|
||||
<div className="leaderboard-toolbar">
|
||||
<nav
|
||||
className="leaderboard-filters"
|
||||
aria-label="Filter leaderboard by difficulty"
|
||||
aria-label={t("Filter leaderboard by difficulty")}
|
||||
>
|
||||
{leaderboardDifficulties.map((value) => (
|
||||
<Link
|
||||
@@ -65,12 +70,12 @@ export default async function LeaderboardPage({
|
||||
aria-current={difficulty === value ? "page" : undefined}
|
||||
>
|
||||
{value === "all"
|
||||
? "All difficulties"
|
||||
: value[0].toUpperCase() + value.slice(1)}
|
||||
? t("All difficulties")
|
||||
: t(value[0].toUpperCase() + value.slice(1))}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<span className="leaderboard-period">All time</span>
|
||||
<span className="leaderboard-period">{t("All time")}</span>
|
||||
</div>
|
||||
{data ? (
|
||||
<>
|
||||
@@ -83,12 +88,12 @@ export default async function LeaderboardPage({
|
||||
>
|
||||
<div className="rankings-heading">
|
||||
<div>
|
||||
<h2 id="rankings-heading">The rankings</h2>
|
||||
<h2 id="rankings-heading">{t("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"}
|
||||
{t("Players: {players} · Completed expeditions: {games}", {
|
||||
players: formatNumber(data.total_players),
|
||||
games: formatNumber(data.total_games),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
@@ -100,7 +105,7 @@ export default async function LeaderboardPage({
|
||||
<input type="hidden" name="difficulty" value={difficulty} />
|
||||
)}
|
||||
<label className="sr-only" htmlFor="player-search">
|
||||
Search players
|
||||
{t("Search players")}
|
||||
</label>
|
||||
<input
|
||||
key={`${difficulty}-${search}`}
|
||||
@@ -108,10 +113,10 @@ export default async function LeaderboardPage({
|
||||
type="search"
|
||||
name="q"
|
||||
defaultValue={search}
|
||||
placeholder="Find an explorer…"
|
||||
placeholder={t("Find an explorer…")}
|
||||
maxLength={64}
|
||||
/>
|
||||
<button type="submit" aria-label="Search players">
|
||||
<button type="submit" aria-label={t("Search players")}>
|
||||
<Search size={17} />
|
||||
</button>
|
||||
</form>
|
||||
@@ -119,11 +124,14 @@ export default async function LeaderboardPage({
|
||||
{search && (
|
||||
<div className="search-summary">
|
||||
<span>
|
||||
{formatScore(data.matching_players)}{" "}
|
||||
{data.matching_players === 1 ? "result" : "results"} for “
|
||||
{search}”
|
||||
{t("Results for “{search}”: {count}", {
|
||||
search,
|
||||
count: formatNumber(data.matching_players),
|
||||
})}
|
||||
</span>
|
||||
<Link href={leaderboardHref(difficulty)}>Clear search ×</Link>
|
||||
<Link href={leaderboardHref(difficulty)}>
|
||||
{t("Clear search ×")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{data.entries.length ? (
|
||||
@@ -137,27 +145,27 @@ export default async function LeaderboardPage({
|
||||
{data.matching_players > 0 && (
|
||||
<div className="leaderboard-pagination">
|
||||
<span>
|
||||
Showing {formatScore((data.page - 1) * 25 + 1)}–
|
||||
{formatScore(
|
||||
{t("Showing")} {formatNumber((data.page - 1) * 25 + 1)}–
|
||||
{formatNumber(
|
||||
Math.min(data.page * 25, data.matching_players),
|
||||
)}{" "}
|
||||
of {formatScore(data.matching_players)}
|
||||
{t("of")} {formatNumber(data.matching_players)}
|
||||
</span>
|
||||
<nav aria-label="Leaderboard pages">
|
||||
<nav aria-label={t("Leaderboard pages")}>
|
||||
{data.page > 1 ? (
|
||||
<Link
|
||||
aria-label="Previous page"
|
||||
aria-label={t("Previous page")}
|
||||
href={leaderboardHref(
|
||||
difficulty,
|
||||
search,
|
||||
data.page - 1,
|
||||
)}
|
||||
>
|
||||
<ArrowLeft size={15} /> Previous
|
||||
<ArrowLeft size={15} /> {t("Previous")}
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-disabled="true">
|
||||
<ArrowLeft size={15} /> Previous
|
||||
<ArrowLeft size={15} /> {t("Previous")}
|
||||
</span>
|
||||
)}
|
||||
<span className="page-number">
|
||||
@@ -165,18 +173,18 @@ export default async function LeaderboardPage({
|
||||
</span>
|
||||
{data.page < data.pages ? (
|
||||
<Link
|
||||
aria-label="Next page"
|
||||
aria-label={t("Next page")}
|
||||
href={leaderboardHref(
|
||||
difficulty,
|
||||
search,
|
||||
data.page + 1,
|
||||
)}
|
||||
>
|
||||
Next <ArrowRight size={15} />
|
||||
{t("Next")} <ArrowRight size={15} />
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-disabled="true">
|
||||
Next <ArrowRight size={15} />
|
||||
{t("Next")} <ArrowRight size={15} />
|
||||
</span>
|
||||
)}
|
||||
</nav>
|
||||
@@ -189,23 +197,24 @@ export default async function LeaderboardPage({
|
||||
)}
|
||||
<aside className="leaderboard-rules">
|
||||
<div>
|
||||
<span className="eyebrow">HOW THE BOARD WORKS</span>
|
||||
<h2>A place earned, one guess at a time.</h2>
|
||||
<span className="eyebrow">{t("HOW THE BOARD WORKS")}</span>
|
||||
<h2>{t("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.
|
||||
{t(
|
||||
"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.
|
||||
{t(
|
||||
"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"}{" "}
|
||||
? t("Start your next expedition")
|
||||
: t("Log in to join the board")}{" "}
|
||||
<ArrowRight size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user