Improve MineGeo interface and functionality

This commit is contained in:
2026-10-06 19:24:41 +05:00
parent 7404f1dc1c
commit 233e4d57f0
67 changed files with 3053 additions and 643 deletions

View File

@@ -1,8 +1,9 @@
"use client";
import { useI18n } from "@/components/I18nProvider";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { PixelTrophy, PlayerAvatar } from "./PixelArt";
import {
formatScore,
type LeaderboardData,
type LeaderboardEntry,
} from "@/shared/leaderboard";
@@ -14,6 +15,7 @@ export function LeaderboardEmpty({
unavailable?: boolean;
search?: boolean;
}) {
const { t } = useI18n();
return (
<div
className="leaderboard-empty"
@@ -22,24 +24,24 @@ export function LeaderboardEmpty({
<PixelTrophy />
<h2>
{unavailable
? "The scoreboard is taking a break."
? t("The scoreboard is taking a break.")
: search
? "No explorers found."
: "The first place is still open."}
? t("No explorers found.")
: t("The first place is still open.")}
</h2>
<p>
{unavailable
? "We couldn’t load the scores. Try again in a moment."
? t("We couldn’t load the scores. Try again in a moment.")
: search
? "Try another player name or clear your search."
: "Log in and finish five rounds to put your name on the board."}
? t("Try another player name or clear your search.")
: t("Log in and finish five rounds to put your name on the board.")}
</p>
{!search && (
<Link
href={unavailable ? "/leaderboard" : "/servers"}
className="button primary"
>
{unavailable ? "Reload scores" : "Play an expedition"}{" "}
{unavailable ? t("Reload scores") : t("Play an expedition")}{" "}
<ArrowRight size={16} />
</Link>
)}
@@ -56,31 +58,33 @@ export function LeaderboardTable({
compact?: boolean;
playerId?: string;
}) {
const { t, formatNumber } = useI18n();
return (
<div className={`leaderboard-table-wrap${compact ? " compact" : ""}`}>
<table className="leaderboard-table">
<caption className="sr-only">
Players ranked by best five-round score. Ties are broken by average
score, then games played.
{t(
"Players ranked by best five-round score. Ties are broken by average score, then games played.",
)}
</caption>
<thead>
<tr>
<th scope="col" className="rank-column">
Rank
{t("Rank")}
</th>
<th scope="col">Explorer</th>
<th scope="col">{t("Explorer")}</th>
{!compact && (
<th scope="col" className="games-column">
Games
{t("Games")}
</th>
)}
{!compact && (
<th scope="col" className="average-column">
Average
{t("Average")}
</th>
)}
<th scope="col" className="score-column">
Best score
{t("Best score")}
</th>
</tr>
</thead>
@@ -103,24 +107,24 @@ export function LeaderboardTable({
<span className="player-name">
{entry.display_name}
{entry.player_id === playerId && (
<span className="you-label">You</span>
<span className="you-label">{t("You")}</span>
)}
</span>
</div>
</th>
{!compact && (
<td className="games-column">
{formatScore(entry.games_played)}
{formatNumber(entry.games_played)}
</td>
)}
{!compact && (
<td className="average-column">
{formatScore(entry.average_score)}
{formatNumber(entry.average_score)}
</td>
)}
<td className="score-column">
<strong>{formatScore(entry.best_score)}</strong>
<span className="score-unit"> pts</span>
<strong>{formatNumber(entry.best_score)}</strong>
<span className="score-unit"> {t("pts")}</span>
</td>
</tr>
))}
@@ -131,8 +135,9 @@ export function LeaderboardTable({
}
export function WinnersPodium({ leaders }: { leaders: LeaderboardEntry[] }) {
const { t, formatNumber } = useI18n();
return (
<section className="winners-podium" aria-label="Top three explorers">
<section className="winners-podium" aria-label={t("Top three explorers")}>
{leaders.map((entry, index) => (
<article
className={`podium-place podium-${index + 1}`}
@@ -140,12 +145,16 @@ export function WinnersPodium({ leaders }: { leaders: LeaderboardEntry[] }) {
>
<div className="podium-player">
<span className="podium-label">
{index === 0 ? "TOP EXPLORER" : `PLACE 0${index + 1}`}
{index === 0
? t("TOP EXPLORER")
: t("Place {place}", {
place: String(index + 1).padStart(2, "0"),
})}
</span>
<PlayerAvatar id={entry.player_id} />
<h2>{entry.display_name}</h2>
<p>
{formatScore(entry.best_score)} <span>/ 25,000</span>
{formatNumber(entry.best_score)} <span>/ 25,000</span>
</p>
</div>
<div className="podium-block" aria-hidden="true">
@@ -159,6 +168,7 @@ export function WinnersPodium({ leaders }: { leaders: LeaderboardEntry[] }) {
}
export function LeaderboardPreview({ data }: { data: LeaderboardData | null }) {
const { t, formatNumber } = useI18n();
return (
<section
className="section home-leaderboard"
@@ -167,24 +177,24 @@ export function LeaderboardPreview({ data }: { data: LeaderboardData | null }) {
<div className="leaderboard-preview-copy">
<div className="leaderboard-kicker">
<PixelTrophy />
<span className="eyebrow">THE EXPLORERS’ BOARD</span>
<span className="eyebrow">{t("THE EXPLORERS’ BOARD")}</span>
</div>
<h2 id="home-leaderboard-heading">
Good eye.
<br /> <span>Great guess.</span>
{t("Good eye.")}
<br /> <span>{t("Great guess.")}</span>
</h2>
<p>
Five rounds to find your way.
<br /> One score to make your mark.
{t("Five rounds to find your way.")}
<br /> {t("One score to make your mark.")}
</p>
<Link className="text-link" href="/leaderboard">
See the full leaderboard <ArrowRight size={16} />
{t("See the full leaderboard")} <ArrowRight size={16} />
</Link>
</div>
<div className="leaderboard-preview-board">
<div className="board-heading">
<span>Top explorers</span>
<span>All difficulties · All time</span>
<span>{t("Top explorers")}</span>
<span>{t("All difficulties · All time")}</span>
</div>
{data?.entries.length ? (
<LeaderboardTable entries={data.entries} compact />
@@ -192,10 +202,12 @@ export function LeaderboardPreview({ data }: { data: LeaderboardData | null }) {
<LeaderboardEmpty unavailable={!data} />
)}
<div className="board-footnote">
Best expedition score ·{" "}
{t("Best expedition score ·")}{" "}
{data
? `${formatScore(data.total_players)} ranked ${data.total_players === 1 ? "explorer" : "explorers"}`
: "Scores temporarily unavailable"}
? t("Ranked explorers: {count}", {
count: formatNumber(data.total_players),
})
: t("Scores temporarily unavailable")}
</div>
</div>
</section>