Improve MineGeo interface and functionality
This commit is contained in:
@@ -1,45 +1,49 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import Link from "next/link";
|
||||
export default function About() {
|
||||
export default async function About() {
|
||||
const { t } = await getI18n();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content prose">
|
||||
<span className="eyebrow">THE EXPLORER’S FIELD GUIDE</span>
|
||||
<span className="eyebrow">{t("THE EXPLORER’S FIELD GUIDE")}</span>
|
||||
<h1>
|
||||
You know this place.
|
||||
{t("You know this place.")}
|
||||
<br />
|
||||
<span className="green">Probably.</span>
|
||||
<span className="green">{t("Probably.")}</span>
|
||||
</h1>
|
||||
<p className="lead">
|
||||
MineGeo drops you into a panorama from a community Minecraft world.
|
||||
Find out where you are before the clock runs out.
|
||||
{t(
|
||||
"MineGeo drops you into a panorama from a community Minecraft world. Find out where you are before the clock runs out.",
|
||||
)}
|
||||
</p>
|
||||
<h2>Look for the little things</h2>
|
||||
<h2>{t("Look for the little things")}</h2>
|
||||
<p>
|
||||
Drag to look around. Mountains, shorelines, roofs, and landmarks all
|
||||
help. You stay in one place throughout a round.
|
||||
{t(
|
||||
"Drag to look around. Mountains, shorelines, roofs, and landmarks all help. You stay in one place throughout a round.",
|
||||
)}
|
||||
</p>
|
||||
<h2>Make your guess</h2>
|
||||
<h2>{t("Make your guess")}</h2>
|
||||
<p>
|
||||
Expand the map in the bottom-right corner and click to place a pin.
|
||||
North is up, X increases east, and Z increases south. Move your pin
|
||||
until you’re ready, then submit.
|
||||
{t(
|
||||
"Expand the map in the bottom-right corner and click to place a pin. North is up, X increases east, and Z increases south. Move your pin until you’re ready, then submit.",
|
||||
)}
|
||||
</p>
|
||||
<h2>Five chances to find your way</h2>
|
||||
<h2>{t("Five chances to find your way")}</h2>
|
||||
<p>
|
||||
Easy gives you 180 seconds, Normal 90, and Hard 30. Hard disables
|
||||
zoom. A timed-out round scores zero; you can still finish the
|
||||
expedition.
|
||||
{t(
|
||||
"Easy gives you 180 seconds, Normal 90, and Hard 30. Hard disables zoom. A timed-out round scores zero; you can still finish the expedition.",
|
||||
)}
|
||||
</p>
|
||||
<h2>Closer means better</h2>
|
||||
<h2>{t("Closer means better")}</h2>
|
||||
<p>
|
||||
Each round awards up to 5,000 points based on horizontal distance. A
|
||||
guess within one block earns a perfect score. Compare all five guesses
|
||||
at the end.
|
||||
{t(
|
||||
"Each round awards up to 5,000 points based on horizontal distance. A guess within one block earns a perfect score. Compare all five guesses at the end.",
|
||||
)}
|
||||
</p>
|
||||
<Link href="/servers" className="button primary">
|
||||
Let’s find a world →
|
||||
{t("Let’s find a world →")}
|
||||
</Link>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { PasswordForm } from "@/components/PasswordForm";
|
||||
import { authClient, user } from "@/lib/supabase";
|
||||
import { recoveryCookie, verifyRecoveryGrant } from "@/lib/password-recovery";
|
||||
export const metadata = { title: "Reset password" };
|
||||
export async function generateMetadata() {
|
||||
const { t } = await getI18n();
|
||||
return { title: t("Reset password") };
|
||||
}
|
||||
export default async function ResetPassword() {
|
||||
const { t } = await getI18n();
|
||||
const [account, jar, client] = await Promise.all([
|
||||
user(),
|
||||
cookies(),
|
||||
@@ -27,24 +32,27 @@ export default async function ResetPassword() {
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<section className="auth-card reset-card">
|
||||
<span className="eyebrow">ACCOUNT RECOVERY</span>
|
||||
<h1>A fresh start.</h1>
|
||||
<span className="eyebrow">{t("ACCOUNT RECOVERY")}</span>
|
||||
<h1>{t("A fresh start.")}</h1>
|
||||
{valid ? (
|
||||
<>
|
||||
<p className="muted">Choose a new password for your account.</p>
|
||||
<p className="muted">
|
||||
{t("Choose a new password for your account.")}
|
||||
</p>
|
||||
<PasswordForm recovery />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
This reset link has expired or has already been used. Request a
|
||||
fresh link to continue.
|
||||
{t(
|
||||
"This reset link has expired or has already been used. Request a fresh link to continue.",
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
className="button primary"
|
||||
href={account ? "/settings" : "/log-in"}
|
||||
>
|
||||
Request a new reset link →
|
||||
{t("Request a new reset link →")}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { DraftEditor } from "@/components/DraftEditor";
|
||||
import { user } from "@/lib/supabase";
|
||||
@@ -7,6 +8,7 @@ export default async function Draft({
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { t } = await getI18n();
|
||||
const { id } = await params;
|
||||
if (!(await user()))
|
||||
redirect(`/log-in?next=${encodeURIComponent("/dashboard/" + id)}`);
|
||||
@@ -14,8 +16,8 @@ export default async function Draft({
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">YOUR WORLD · YOUR LOCATIONS</span>
|
||||
<h1>Map workshop</h1>
|
||||
<span className="eyebrow">{t("YOUR WORLD · YOUR LOCATIONS")}</span>
|
||||
<h1>{t("Map workshop")}</h1>
|
||||
<DraftEditor id={id} />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import { Dashboard } from "@/components/Dashboard";
|
||||
@@ -6,16 +7,21 @@ import Link from "next/link";
|
||||
import { accountProfile } from "@/lib/profile";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function DashboardPage() {
|
||||
const { t, formatDate } = await getI18n();
|
||||
const account = await user();
|
||||
const profile = account ? await accountProfile(account) : null;
|
||||
const historyCutoff = new Date(
|
||||
Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const history = account
|
||||
? required(
|
||||
await admin()
|
||||
.from("game_sessions")
|
||||
.select("id,difficulty,created_at,current_round")
|
||||
.eq("owner_id", account.id)
|
||||
.gte("created_at", historyCutoff)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20),
|
||||
.limit(5),
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
@@ -24,24 +30,31 @@ export default async function DashboardPage() {
|
||||
<main className="section page-content">
|
||||
{account ? (
|
||||
<>
|
||||
<span className="eyebrow">THE CARTOGRAPHER’S DESK</span>
|
||||
<h1>My expedition.</h1>
|
||||
<p className="lead">Welcome back, {profile?.display_name}.</p>
|
||||
<span className="eyebrow">{t("THE CARTOGRAPHER’S DESK")}</span>
|
||||
<h1>{t("My expedition.")}</h1>
|
||||
<p className="lead">
|
||||
{t("Welcome back,")} {profile?.display_name}.
|
||||
</p>
|
||||
<Dashboard />
|
||||
{history.length > 0 && (
|
||||
<section className="token-box">
|
||||
<h2>Recent journeys</h2>
|
||||
<h2>{t("Recent journeys")}</h2>
|
||||
<p className="muted">
|
||||
{t("Your five latest journeys from the past 30 days.")}
|
||||
</p>
|
||||
{history.map((s) => (
|
||||
<div className="draft-row" key={s.id}>
|
||||
<span>
|
||||
{new Date(s.created_at).toLocaleDateString("en-GB")} ·{" "}
|
||||
{s.difficulty} · {s.current_round}/5 rounds
|
||||
{formatDate(s.created_at)} ·{" "}
|
||||
{t(s.difficulty[0].toUpperCase() + s.difficulty.slice(1))}{" "}
|
||||
· {s.current_round}
|
||||
{t("/5 rounds")}
|
||||
</span>
|
||||
<Link
|
||||
className="button"
|
||||
href={`/${s.current_round >= 5 ? "results" : "play"}/${s.id}`}
|
||||
>
|
||||
Open →
|
||||
{t("Open →")}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Blocks, Server, Download, ExternalLink } from "lucide-react";
|
||||
import { existsSync } from "node:fs";
|
||||
import Link from "next/link";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default function Downloads() {
|
||||
export default async function Downloads() {
|
||||
const { t } = await getI18n();
|
||||
const fabric = existsSync("public/tools/mod/minegeo-fabric-1.21.11.jar"),
|
||||
plugin = existsSync("public/tools/plugin/minegeo-plugin-1.21.11.jar");
|
||||
return (
|
||||
@@ -11,32 +13,40 @@ export default function Downloads() {
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">
|
||||
MADE FOR THE PEOPLE WHO KNOW THEIR WORLDS
|
||||
{t("MADE FOR THE PEOPLE WHO KNOW THEIR WORLDS")}
|
||||
</span>
|
||||
<h1>Become a cartographer.</h1>
|
||||
<h1>{t("Become a cartographer.")}</h1>
|
||||
<p className="lead">
|
||||
Two ways to bring your world to MineGeo. Both create private drafts
|
||||
for you to preview before submitting for review.
|
||||
{t(
|
||||
"Two ways to bring your world to MineGeo. Both create private drafts for you to preview before submitting for review.",
|
||||
)}
|
||||
</p>
|
||||
<div className="tool-grid">
|
||||
<article className="tool-card">
|
||||
<Blocks size={34} />
|
||||
<span className="eyebrow">FOR PLAYERS</span>
|
||||
<h2>Fabric capture mod.</h2>
|
||||
<span className="eyebrow">{t("FOR PLAYERS")}</span>
|
||||
<h2>{t("Fabric capture mod.")}</h2>
|
||||
<p className="muted">
|
||||
Explore normally and capture the places worth remembering. Your
|
||||
server doesn’t need a plugin.
|
||||
{t(
|
||||
"Explore normally and capture the places worth remembering. Your server doesn’t need a plugin.",
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>Minecraft Java 1.21.11 + Fabric Loader + Fabric API</li>
|
||||
<li>Open the capture panel with G</li>
|
||||
<li>Link your account through a browser pairing code</li>
|
||||
<li>Capture 5–24 viewpoints for a new map, then upload</li>
|
||||
<li>
|
||||
Paste a map ID into “Add to map” to contribute 1–12 panoramas
|
||||
{t("Minecraft Java 1.21.11 + Fabric Loader + Fabric API")}
|
||||
</li>
|
||||
<li>Only terrain received by your client enters the map</li>
|
||||
<li>Contributions check the recorded server address</li>
|
||||
<li>{t("Open the capture panel with G")}</li>
|
||||
<li>{t("Link your account through a browser pairing code")}</li>
|
||||
<li>{t("Capture 5–24 viewpoints for a new map, then upload")}</li>
|
||||
<li>
|
||||
{t(
|
||||
"Paste a map ID into “Add to map” to contribute 1–12 panoramas",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t("Only terrain received by your client enters the map")}
|
||||
</li>
|
||||
<li>{t("Contributions check the recorded server address")}</li>
|
||||
</ul>
|
||||
{fabric ? (
|
||||
<a
|
||||
@@ -44,34 +54,40 @@ export default function Downloads() {
|
||||
href="/tools/mod/minegeo-fabric-1.21.11.jar"
|
||||
download
|
||||
>
|
||||
<Download size={16} /> Download Fabric mod
|
||||
<Download size={16} /> {t("Download Fabric mod")}
|
||||
</a>
|
||||
) : (
|
||||
<p className="muted">The Fabric build is not available yet.</p>
|
||||
<p className="muted">
|
||||
{t("The Fabric build is not available yet.")}
|
||||
</p>
|
||||
)}
|
||||
<p className="muted" style={{ fontSize: 11, marginTop: 20 }}>
|
||||
Install the JAR in your Minecraft mods folder alongside Fabric
|
||||
API.
|
||||
{t(
|
||||
"Install the JAR in your Minecraft mods folder alongside Fabric API.",
|
||||
)}
|
||||
</p>
|
||||
</article>
|
||||
<article className="tool-card">
|
||||
<Server size={34} />
|
||||
<span className="eyebrow">FOR SERVER ADMINISTRATORS</span>
|
||||
<h2>Server export plugin.</h2>
|
||||
<span className="eyebrow">{t("FOR SERVER ADMINISTRATORS")}</span>
|
||||
<h2>{t("Server export plugin.")}</h2>
|
||||
<p className="muted">
|
||||
Export generated Overworld terrain. MineGeo renders panoramas
|
||||
without a connected player or BlueMap.
|
||||
{t(
|
||||
"Export generated Overworld terrain. MineGeo renders panoramas without a connected player or BlueMap.",
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>Bukkit/Paper 1.21.11 · Java 21</li>
|
||||
<li>Select a bounded area and export existing chunks</li>
|
||||
<li>Choose up to 24 random, safe surface viewpoints</li>
|
||||
<li>{t("Bukkit/Paper 1.21.11 · Java 21")}</li>
|
||||
<li>{t("Select a bounded area and export existing chunks")}</li>
|
||||
<li>{t("Choose up to 24 random, safe surface viewpoints")}</li>
|
||||
<li>
|
||||
Use /minegeo contribute <map ID> to join an existing map
|
||||
</li>
|
||||
<li>Link a contributor token and upload resumably</li>
|
||||
<li>Set the public Minecraft server address before exporting</li>
|
||||
<li>Vanilla blocks/fluids, simplified lighting</li>
|
||||
<li>{t("Link a contributor token and upload resumably")}</li>
|
||||
<li>
|
||||
{t("Set the public Minecraft server address before exporting")}
|
||||
</li>
|
||||
<li>{t("Vanilla blocks/fluids, simplified lighting")}</li>
|
||||
</ul>
|
||||
{plugin ? (
|
||||
<a
|
||||
@@ -79,10 +95,12 @@ export default function Downloads() {
|
||||
href="/tools/plugin/minegeo-plugin-1.21.11.jar"
|
||||
download
|
||||
>
|
||||
<Download size={16} /> Download server plugin
|
||||
<Download size={16} /> {t("Download server plugin")}
|
||||
</a>
|
||||
) : (
|
||||
<p className="muted">The plugin build is not available yet.</p>
|
||||
<p className="muted">
|
||||
{t("The plugin build is not available yet.")}
|
||||
</p>
|
||||
)}
|
||||
<pre>
|
||||
{
|
||||
@@ -90,10 +108,13 @@ export default function Downloads() {
|
||||
}
|
||||
</pre>
|
||||
<p className="muted">
|
||||
Start with a small, explored area around your builds. Exports are
|
||||
saved in <code>plugins/MineGeo/exports/export-…/</code>. The
|
||||
manifest appears when export finishes. Upload sends the latest
|
||||
completed export; it does not capture the world.
|
||||
{t(
|
||||
"Start with a small, explored area around your builds. Exports are saved in",
|
||||
)}{" "}
|
||||
<code>plugins/MineGeo/exports/export-…/</code>
|
||||
{t(
|
||||
". The manifest appears when export finishes. Upload sends the latest completed export; it does not capture the world.",
|
||||
)}
|
||||
</p>
|
||||
<p className="muted" style={{ fontSize: 11 }}>
|
||||
Put the JAR in plugins/ and restart. Set website, server-name and
|
||||
@@ -105,15 +126,15 @@ export default function Downloads() {
|
||||
</div>
|
||||
<div className="contribute-banner">
|
||||
<div>
|
||||
<h2>Your map, your credit.</h2>
|
||||
<h2>{t("Your map, your credit.")}</h2>
|
||||
<p>
|
||||
Map owners can allow player contributions. Each player may add up
|
||||
to 12 panoramas per map. Owners approve the selection, then
|
||||
moderators review the map update before it becomes playable.
|
||||
{t(
|
||||
"Map owners can allow player contributions. Each player may add up to 12 panoramas per map. Owners approve the selection, then moderators review the map update before it becomes playable.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Link className="button" href="/dashboard">
|
||||
Open my dashboard <ExternalLink size={15} />
|
||||
{t("Open my dashboard")} <ExternalLink size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
import { useI18n } from "@/components/I18nProvider";
|
||||
|
||||
export default function ErrorPage({ retry }: { retry: () => void }) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<main className="section page-content">
|
||||
<h1>Lost connection.</h1>
|
||||
<h1>{t("Lost connection.")}</h1>
|
||||
<p>
|
||||
We couldn’t load this part of the world. Check your connection and try
|
||||
again.
|
||||
{t(
|
||||
"We couldn’t load this part of the world. Check your connection and try again.",
|
||||
)}
|
||||
</p>
|
||||
<button className="button primary" onClick={retry}>
|
||||
Try again
|
||||
{t("Try again")}
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -982,7 +982,9 @@ pre {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
}
|
||||
.panorama:active { cursor: grabbing; }
|
||||
.panorama canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -9,24 +9,45 @@ import "./home-map.css";
|
||||
import "./rounded-theme.css";
|
||||
import "./cursor.css";
|
||||
import { SiteCursor } from "@/components/SiteCursor";
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "MineGeo — Get lost in a familiar world",
|
||||
template: "%s · MineGeo",
|
||||
},
|
||||
description:
|
||||
"Guess where you are in Minecraft. Explore community servers through 360° panoramas and place your pin on the map.",
|
||||
};
|
||||
export default function RootLayout({
|
||||
import { I18nProvider } from "@/components/I18nProvider";
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import "./polish.css";
|
||||
import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
|
||||
import { initialSiteSettings } from "@/lib/site-settings";
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const { t } = await getI18n();
|
||||
return {
|
||||
title: {
|
||||
default: t("MineGeo — Get lost in a familiar world"),
|
||||
template: "%s · MineGeo",
|
||||
},
|
||||
description: t(
|
||||
"Guess where you are in Minecraft. Explore community servers through 360° panoramas and place your pin on the map.",
|
||||
),
|
||||
};
|
||||
}
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [{ locale, preference, detectedLocale }, settings] = await Promise.all([
|
||||
getI18n(),
|
||||
initialSiteSettings(),
|
||||
]);
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
{children}
|
||||
<SiteCursor />
|
||||
<I18nProvider
|
||||
locale={locale}
|
||||
preference={preference}
|
||||
detectedLocale={detectedLocale}
|
||||
>
|
||||
<SiteSettingsProvider initialSettings={settings}>
|
||||
{children}
|
||||
<SiteCursor />
|
||||
</SiteSettingsProvider>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import { user } from "@/lib/supabase";
|
||||
import { authDestination } from "@/shared/account";
|
||||
export const metadata = { title: "Log in" };
|
||||
export async function generateMetadata() {
|
||||
const { t } = await getI18n();
|
||||
return { title: t("Log in") };
|
||||
}
|
||||
export default async function LogIn({
|
||||
searchParams,
|
||||
}: {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Moderation } from "@/components/Moderation";
|
||||
export default function Review() {
|
||||
export default async function Review() {
|
||||
const { t } = await getI18n();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">THE COMMUNITY’S COMPASS</span>
|
||||
<h1>Map moderation.</h1>
|
||||
<span className="eyebrow">{t("THE COMMUNITY’S COMPASS")}</span>
|
||||
<h1>{t("Map moderation.")}</h1>
|
||||
<p className="lead">
|
||||
Review map quality and coverage before a world enters the public
|
||||
catalog.
|
||||
{t(
|
||||
"Review map quality and coverage before a world enters the public catalog.",
|
||||
)}
|
||||
</p>
|
||||
<Moderation />
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import Link from "next/link";
|
||||
export default function NotFound() {
|
||||
export default async function NotFound() {
|
||||
const { t } = await getI18n();
|
||||
return (
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">UNKNOWN COORDINATES</span>
|
||||
<h1>Uncharted territory.</h1>
|
||||
<p>This map or page isn’t available.</p>
|
||||
<span className="eyebrow">{t("UNKNOWN COORDINATES")}</span>
|
||||
<h1>{t("Uncharted territory.")}</h1>
|
||||
<p>{t("This map or page isn’t available.")}</p>
|
||||
<Link className="button primary" href="/servers">
|
||||
Explore worlds →
|
||||
{t("Explore worlds →")}
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
|
||||
133
app/page.tsx
133
app/page.tsx
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import { leaderboard } from "@/lib/leaderboard";
|
||||
import { LeaderboardPreview } from "@/components/Leaderboard";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function Home() {
|
||||
const { t, formatNumber } = await getI18n();
|
||||
const [maps, rankings] = await Promise.all([
|
||||
catalog(),
|
||||
leaderboard({ limit: 5 }),
|
||||
@@ -27,24 +29,24 @@ export default async function Home() {
|
||||
<main className="home-page" id="main-content">
|
||||
<section className="hero">
|
||||
<div className="hero-copy">
|
||||
<div className="status-label">A MINECRAFT GUESSING GAME</div>
|
||||
<div className="status-label">{t("A MINECRAFT GUESSING GAME")}</div>
|
||||
<h1>
|
||||
Guess where you are
|
||||
{t("Guess where you are")}
|
||||
<br />
|
||||
in a <span>blocky world.</span>
|
||||
{t("in a")} <span>{t("blocky world.")}</span>
|
||||
</h1>
|
||||
<p>
|
||||
Drop into a random corner of a Minecraft world.
|
||||
<br className="desktop" /> Look around, spot the landmarks, and
|
||||
place your pin
|
||||
<br className="desktop" /> before the clock runs out.
|
||||
{t("Drop into a random corner of a Minecraft world.")}
|
||||
<br className="desktop" />{" "}
|
||||
{t("Look around, spot the landmarks, and place your pin")}
|
||||
<br className="desktop" /> {t("before the clock runs out.")}
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Link href="/servers" className="button primary">
|
||||
Start exploring <ArrowRight size={18} />
|
||||
{t("Start exploring")} <ArrowRight size={18} />
|
||||
</Link>
|
||||
<Link href="/leaderboard" className="text-link">
|
||||
Meet the top explorers <ArrowRight size={14} />
|
||||
{t("Meet the top explorers")} <ArrowRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hero-notes">
|
||||
@@ -55,14 +57,16 @@ export default async function Home() {
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span>Five rounds. No roads. Just your instincts.</span>
|
||||
<span>{t("Five rounds. No roads. Just your instincts.")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-art" aria-label="Minecraft surface map">
|
||||
<div className="hero-art" aria-label={t("Minecraft surface map")}>
|
||||
<div className="map-art-frame minecraft-paper">
|
||||
<Image
|
||||
src={maps[0]?.mapImage || "/images/example-map.webp"}
|
||||
alt="A Minecraft surface map on the original Minecraft parchment texture"
|
||||
alt={t(
|
||||
"A Minecraft surface map on the original Minecraft parchment texture",
|
||||
)}
|
||||
width={1024}
|
||||
height={1024}
|
||||
loading="eager"
|
||||
@@ -79,51 +83,52 @@ export default async function Home() {
|
||||
<div className="floating-tag">
|
||||
<MapPin size={14} />
|
||||
<strong>X: ??? Z: ???</strong>
|
||||
<span>Overworld</span>
|
||||
<span>{t("Overworld")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="grass-divider" aria-hidden="true">
|
||||
<span />
|
||||
</div>
|
||||
<section className="world-stats" aria-label="Community statistics">
|
||||
<section className="world-stats" aria-label={t("Community statistics")}>
|
||||
<div>
|
||||
<i className="stat-cube green-cube" />
|
||||
<div>
|
||||
<strong>{servers.toLocaleString()}</strong>
|
||||
<span>COMMUNITY SERVERS</span>
|
||||
<strong>{formatNumber(servers)}</strong>
|
||||
<span>{t("COMMUNITY SERVERS")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<i className="stat-cube gold-cube" />
|
||||
<div>
|
||||
<strong>{maps.length.toLocaleString()}</strong>
|
||||
<span>PUBLISHED MAPS</span>
|
||||
<strong>{formatNumber(maps.length)}</strong>
|
||||
<span>{t("PUBLISHED MAPS")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<i className="stat-cube cyan-cube" />
|
||||
<div>
|
||||
<strong>{locations.toLocaleString()}</strong>
|
||||
<span>PLACES TO DISCOVER</span>
|
||||
<strong>{formatNumber(locations)}</strong>
|
||||
<span>{t("PLACES TO DISCOVER")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<i className="stat-cube red-cube" />
|
||||
<div>
|
||||
<strong>5 rounds</strong>
|
||||
<span>EVERY EXPEDITION</span>
|
||||
<strong>{t("5 rounds")}</strong>
|
||||
<span>{t("EVERY EXPEDITION")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="section catalog-section">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="eyebrow">COMMUNITY MAPS</span>
|
||||
<h2>Where will you go today?</h2>
|
||||
<span className="eyebrow">{t("COMMUNITY MAPS")}</span>
|
||||
<h2>{t("Where will you go today?")}</h2>
|
||||
</div>
|
||||
<Link className="text-link" href="/servers">
|
||||
All {maps.length} maps <ArrowRight size={15} />
|
||||
{t("All maps ({count})", { count: maps.length })}{" "}
|
||||
<ArrowRight size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
{maps.length ? (
|
||||
@@ -142,20 +147,20 @@ export default async function Home() {
|
||||
height={180}
|
||||
unoptimized
|
||||
/>
|
||||
<span>YOUR WORLD HERE</span>
|
||||
<span>{t("YOUR WORLD HERE")}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="eyebrow">
|
||||
THE FIRST EXPEDITION STARTS WITH YOU
|
||||
{t("THE FIRST EXPEDITION STARTS WITH YOU")}
|
||||
</span>
|
||||
<h3>Put your server on the map.</h3>
|
||||
<h3>{t("Put your server on the map.")}</h3>
|
||||
<p>
|
||||
No community maps have been published yet. Export your world
|
||||
with the server plugin, or capture five locations with the
|
||||
Fabric mod. Preview your map and submit it for review.
|
||||
{t(
|
||||
"No community maps have been published yet. Export your world with the server plugin, or capture five locations with the Fabric mod. Preview your map and submit it for review.",
|
||||
)}
|
||||
</p>
|
||||
<Link className="button primary" href="/downloads">
|
||||
<Upload size={16} /> Get the capture tools
|
||||
<Upload size={16} /> {t("Get the capture tools")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,58 +169,64 @@ export default async function Home() {
|
||||
<LeaderboardPreview data={rankings} />
|
||||
<section className="section home-panels">
|
||||
<article className="home-panel">
|
||||
<span className="eyebrow">HOW TO PLAY</span>
|
||||
<h2>Know your blocks?</h2>
|
||||
<span className="eyebrow">{t("HOW TO PLAY")}</span>
|
||||
<h2>{t("Know your blocks?")}</h2>
|
||||
<ol className="play-steps">
|
||||
<li>
|
||||
<span>01</span>
|
||||
<div>
|
||||
<h3>Pick your world</h3>
|
||||
<p>Choose a community server, map, and difficulty.</p>
|
||||
<h3>{t("Pick your world")}</h3>
|
||||
<p>{t("Choose a community server, map, and difficulty.")}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>02</span>
|
||||
<div>
|
||||
<h3>Look for the clues</h3>
|
||||
<h3>{t("Look for the clues")}</h3>
|
||||
<p>
|
||||
Rotate the panorama. Find builds, biomes, and landmarks.
|
||||
{t(
|
||||
"Rotate the panorama. Find builds, biomes, and landmarks.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>03</span>
|
||||
<div>
|
||||
<h3>Make your guess</h3>
|
||||
<p>Drop a pin on the map. Closer guesses earn more points.</p>
|
||||
<h3>{t("Make your guess")}</h3>
|
||||
<p>
|
||||
{t(
|
||||
"Drop a pin on the map. Closer guesses earn more points.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<Link className="text-link" href="/about">
|
||||
Read the field guide <ArrowRight size={14} />
|
||||
{t("Read the field guide")} <ArrowRight size={14} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="home-panel">
|
||||
<span className="eyebrow">CHOOSE YOUR CHALLENGE</span>
|
||||
<h2>One world. Three ways.</h2>
|
||||
<span className="eyebrow">{t("CHOOSE YOUR CHALLENGE")}</span>
|
||||
<h2>{t("One world. Three ways.")}</h2>
|
||||
<div className="mode-list">
|
||||
{[
|
||||
{
|
||||
name: "Easy",
|
||||
time: "180 seconds",
|
||||
detail: "Take your time. Rotate and zoom.",
|
||||
name: t("Easy"),
|
||||
time: t("180 seconds"),
|
||||
detail: t("Take your time. Rotate and zoom."),
|
||||
color: "green",
|
||||
},
|
||||
{
|
||||
name: "Normal",
|
||||
time: "90 seconds",
|
||||
detail: "Trust your eye. Rotate and zoom.",
|
||||
name: t("Normal"),
|
||||
time: t("90 seconds"),
|
||||
detail: t("Trust your eye. Rotate and zoom."),
|
||||
color: "gold",
|
||||
},
|
||||
{
|
||||
name: "Hard",
|
||||
time: "30 seconds",
|
||||
detail: "Think fast. No zoom allowed.",
|
||||
name: t("Hard"),
|
||||
time: t("30 seconds"),
|
||||
detail: t("Think fast. No zoom allowed."),
|
||||
color: "red",
|
||||
},
|
||||
].map((mode) => (
|
||||
@@ -223,32 +234,34 @@ export default async function Home() {
|
||||
<i className={`stat-cube ${mode.color}-cube`} />
|
||||
<div>
|
||||
<h3>
|
||||
{mode.name}
|
||||
{t(mode.name)}
|
||||
<span>
|
||||
<Timer size={12} />
|
||||
{mode.time}
|
||||
{t(mode.time)}
|
||||
</span>
|
||||
</h3>
|
||||
<p>{mode.detail}</p>
|
||||
<p>{t(mode.detail)}</p>
|
||||
</div>
|
||||
<ArrowRight size={15} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<p className="mode-footnote">
|
||||
Up to 5,000 points per round. How close can you get?
|
||||
{t("Up to 5,000 points per round. How close can you get?")}
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
<section className="section contribute-banner">
|
||||
<Blocks size={38} />
|
||||
<div>
|
||||
<span className="eyebrow">BUILT BY THE COMMUNITY</span>
|
||||
<h2>Your world deserves to be explored.</h2>
|
||||
<p>Share your builds and your favorite corners of Minecraft.</p>
|
||||
<span className="eyebrow">{t("BUILT BY THE COMMUNITY")}</span>
|
||||
<h2>{t("Your world deserves to be explored.")}</h2>
|
||||
<p>
|
||||
{t("Share your builds and your favorite corners of Minecraft.")}
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/downloads" className="button">
|
||||
Create a map <ArrowRight size={16} />
|
||||
{t("Create a map")} <ArrowRight size={16} />
|
||||
</Link>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Pairing } from "@/components/Pairing";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
@@ -7,6 +8,7 @@ export default async function Pair({
|
||||
}: {
|
||||
searchParams: Promise<{ code?: string }>;
|
||||
}) {
|
||||
const { t } = await getI18n();
|
||||
const { code } = await searchParams;
|
||||
const account = await user();
|
||||
return (
|
||||
@@ -16,7 +18,7 @@ export default async function Pair({
|
||||
{!account ? (
|
||||
<>
|
||||
<p className="muted">
|
||||
Sign in first, then reopen the pairing link from Minecraft.
|
||||
{t("Sign in first, then reopen the pairing link from Minecraft.")}
|
||||
</p>
|
||||
<SignIn
|
||||
next={
|
||||
@@ -29,7 +31,7 @@ export default async function Pair({
|
||||
) : code && /^[A-F0-9]{8}$/.test(code) ? (
|
||||
<Pairing code={code} />
|
||||
) : (
|
||||
<p>Open the pairing link shown by your Fabric mod.</p>
|
||||
<p>{t("Open the pairing link shown by your Fabric mod.")}</p>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
220
app/polish.css
Normal file
220
app/polish.css
Normal file
@@ -0,0 +1,220 @@
|
||||
/* Responsive corrections shared by both languages. */
|
||||
.language-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.language-switcher select {
|
||||
color: var(--text);
|
||||
background: #151e28;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 8px 28px 8px 10px;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.language-switcher select:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid var(--green);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MineGeoPixelCyrillic;
|
||||
src: url("/fonts/Monocraft.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
html[lang="ru"] h1,
|
||||
html[lang="ru"] h2,
|
||||
html[lang="ru"] h3,
|
||||
html[lang="ru"] .button,
|
||||
html[lang="ru"] .header-account,
|
||||
html[lang="ru"] .site-header nav,
|
||||
html[lang="ru"] .world-stats strong {
|
||||
font-family: MineGeoPixelCyrillic, monospace;
|
||||
line-height: 1.45;
|
||||
}
|
||||
html[lang="ru"] h1 {
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
html[lang="ru"] h2 {
|
||||
font-size: clamp(22px, 2.1vw, 30px);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
html[lang="ru"] h3 {
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
html[lang="ru"] .button,
|
||||
html[lang="ru"] .header-account {
|
||||
font-size: 14px;
|
||||
}
|
||||
html[lang="ru"] .site-header nav {
|
||||
font-size: 12px;
|
||||
gap: 24px;
|
||||
}
|
||||
html[lang="ru"] .world-stats strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
html[lang="ru"] .leaderboard-preview-copy h2 {
|
||||
font-size: clamp(27px, 3.2vw, 42px);
|
||||
}
|
||||
html[lang="ru"] .mode-row h3 {
|
||||
font-size: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
html[lang="ru"] .mode-row h3 span {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.header-inner > *,
|
||||
.hero > *,
|
||||
.section-heading > *,
|
||||
.home-panels > *,
|
||||
.contribute-banner > *,
|
||||
.site-footer > * {
|
||||
min-width: 0;
|
||||
}
|
||||
.site-footer > div:last-of-type {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.site-footer .disclaimer {
|
||||
color: var(--muted);
|
||||
}
|
||||
.card-body h3,
|
||||
.podium-player h2 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.board-heading {
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mode-row h3 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tool-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tool-card pre,
|
||||
.token-box code {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tool-card .button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.leaderboard-filters {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hero-notes > span:last-child {
|
||||
min-width: 0;
|
||||
}
|
||||
.round-blocks {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.button,
|
||||
.header-account {
|
||||
text-align: center;
|
||||
}
|
||||
.auth-tabs button {
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.site-header {
|
||||
height: auto;
|
||||
}
|
||||
.header-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
padding: 14px 20px;
|
||||
gap: 14px;
|
||||
}
|
||||
.header-inner nav {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 22px;
|
||||
padding: 12px 0 2px;
|
||||
border-top: 1px solid var(--line);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
html[lang="ru"] .header-inner nav {
|
||||
gap: 12px 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.header-account {
|
||||
justify-self: end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-header nav a {
|
||||
padding-block: 6px;
|
||||
}
|
||||
.site-header nav a[aria-current="page"]::after {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.section-heading {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.section-heading .text-link {
|
||||
white-space: normal;
|
||||
}
|
||||
.world-stats {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.world-stats > div {
|
||||
padding-left: 0;
|
||||
}
|
||||
.home-page .map-art-frame {
|
||||
width: min(256px, 76vw);
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.site-footer {
|
||||
gap: 24px;
|
||||
}
|
||||
.site-footer > div:last-of-type {
|
||||
width: 100%;
|
||||
gap: 16px 24px;
|
||||
}
|
||||
.language-switcher {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
html[lang="ru"] .hero h1 {
|
||||
font-size: clamp(26px, 7vw, 34px);
|
||||
}
|
||||
html[lang="ru"] .world-stats strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
html[lang="ru"] .card-body h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
html[lang="ru"] .card-type {
|
||||
font-size: 8px;
|
||||
}
|
||||
.hero-actions .text-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
.home-panel + .home-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.moderator-settings { margin-bottom: 28px; }
|
||||
.moderator-setting-row { display: flex; gap: 24px; justify-content: space-between; align-items: center; margin-top: 24px; }
|
||||
.moderator-setting-row h3 { margin: 0 0 8px; }
|
||||
.moderator-setting-row p { margin: 6px 0; }
|
||||
.moderator-setting-row .button { flex-shrink: 0; }
|
||||
@media (max-width: 600px) {
|
||||
.moderator-setting-row { align-items: stretch; flex-direction: column; }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { notFound } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
@@ -10,6 +11,7 @@ export default async function ServerPage({
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { t } = await getI18n();
|
||||
const { id } = await params;
|
||||
const map = await getMap(id);
|
||||
if (!map) notFound();
|
||||
@@ -18,15 +20,19 @@ export default async function ServerPage({
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">
|
||||
{map.source === "plugin" ? "SERVER EXPORT" : "PLAYER CONTRIBUTION"} ·
|
||||
MINECRAFT 1.21.11
|
||||
{map.source === "plugin"
|
||||
? t("SERVER EXPORT")
|
||||
: t("PLAYER CONTRIBUTION")}{" "}
|
||||
{t("· MINECRAFT 1.21.11")}
|
||||
</span>
|
||||
<h1>{map.name}</h1>
|
||||
<p className="lead">{map.description}</p>
|
||||
<p className="map-credit">Created by {map.credit}</p>
|
||||
<p className="map-credit">
|
||||
{t("Created by")} {map.credit}
|
||||
</p>
|
||||
<ReportContent datasetId={id} />
|
||||
<p className="muted">
|
||||
Map ID: <code>{map.id}</code>
|
||||
{t("Map ID:")} <code>{map.id}</code>
|
||||
</p>
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
@@ -41,11 +47,13 @@ export default async function ServerPage({
|
||||
/>
|
||||
</div>
|
||||
<div className="card-meta">
|
||||
<span>{map.panorama_count} panoramas</span>
|
||||
<span>Overworld surface</span>
|
||||
<span>
|
||||
{map.panorama_count} {t("panoramas")}
|
||||
</span>
|
||||
<span>{t("Overworld surface")}</span>
|
||||
<span>
|
||||
{map.bounds.maxX - map.bounds.minX} ×{" "}
|
||||
{map.bounds.maxZ - map.bounds.minZ} blocks
|
||||
{map.bounds.maxZ - map.bounds.minZ} {t("blocks")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer, EmptyState } from "@/components/Shell";
|
||||
import { catalog } from "@/lib/catalog";
|
||||
import { CatalogBrowser } from "@/components/CatalogBrowser";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function Servers() {
|
||||
const { t } = await getI18n();
|
||||
const maps = await catalog();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">THE COMMUNITY AT YOUR COORDINATES</span>
|
||||
<h1>Explore the worlds.</h1>
|
||||
<span className="eyebrow">
|
||||
{t("THE COMMUNITY AT YOUR COORDINATES")}
|
||||
</span>
|
||||
<h1>{t("Explore the worlds.")}</h1>
|
||||
<p className="lead">
|
||||
From familiar spawn towns to places nobody’s quite sure about. Pick
|
||||
your next expedition.
|
||||
{t(
|
||||
"From familiar spawn towns to places nobody’s quite sure about. Pick your next expedition.",
|
||||
)}
|
||||
</p>
|
||||
{maps.length ? (
|
||||
<CatalogBrowser maps={maps} />
|
||||
) : (
|
||||
<EmptyState title="Fresh chunks, fresh start">
|
||||
No maps have been approved yet. Your server could be the first.
|
||||
<EmptyState title={t("Fresh chunks, fresh start")}>
|
||||
{t(
|
||||
"No maps have been approved yet. Your server could be the first.",
|
||||
)}
|
||||
</EmptyState>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { AccountSettings } from "@/components/AccountSettings";
|
||||
import { user } from "@/lib/supabase";
|
||||
import { accountProfile } from "@/lib/profile";
|
||||
export const metadata = { title: "Account settings" };
|
||||
export async function generateMetadata() {
|
||||
const { t } = await getI18n();
|
||||
return { title: t("Account settings") };
|
||||
}
|
||||
export default async function Settings({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ password?: string }>;
|
||||
}) {
|
||||
const { t } = await getI18n();
|
||||
const account = await user();
|
||||
if (!account) redirect("/log-in?next=%2Fsettings");
|
||||
const [profile, query] = await Promise.all([
|
||||
@@ -21,12 +26,14 @@ export default async function Settings({
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<Link className="text-link" href="/dashboard">
|
||||
← My account
|
||||
{t("← My account")}
|
||||
</Link>
|
||||
<div className="account-page-heading">
|
||||
<span className="eyebrow">YOUR CARTOGRAPHER ACCOUNT</span>
|
||||
<h1>Account settings.</h1>
|
||||
<p className="lead">Your name, your sign-in, your next expedition.</p>
|
||||
<span className="eyebrow">{t("YOUR CARTOGRAPHER ACCOUNT")}</span>
|
||||
<h1>{t("Account settings.")}</h1>
|
||||
<p className="lead">
|
||||
{t("Your name, your sign-in, your next expedition.")}
|
||||
</p>
|
||||
</div>
|
||||
<AccountSettings
|
||||
email={account.email ?? ""}
|
||||
|
||||
Reference in New Issue
Block a user