The first commit, the core and all files is adding to repo
This commit is contained in:
48
app/about/page.tsx
Normal file
48
app/about/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import Link from "next/link";
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content prose">
|
||||
<span className="eyebrow">THE EXPLORER’S FIELD GUIDE</span>
|
||||
<h1>
|
||||
You know this place.
|
||||
<br />
|
||||
<span className="green">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.
|
||||
</p>
|
||||
<h2>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.
|
||||
</p>
|
||||
<h2>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.
|
||||
</p>
|
||||
<h2>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.
|
||||
</p>
|
||||
<h2>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.
|
||||
</p>
|
||||
<Link href="/servers" className="button primary">
|
||||
Let’s find a world →
|
||||
</Link>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
app/api/v1/[...path]/route.ts
Normal file
11
app/api/v1/[...path]/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { handle } from "@/lib/api";
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
async function route(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> },
|
||||
) {
|
||||
return handle(request, (await params).path);
|
||||
}
|
||||
export { route as GET, route as POST, route as PATCH, route as DELETE };
|
||||
12
app/auth/callback/route.ts
Normal file
12
app/auth/callback/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authClient } from "@/lib/supabase";
|
||||
export async function GET(request: NextRequest) {
|
||||
const code = request.nextUrl.searchParams.get("code");
|
||||
if (code) {
|
||||
const client = await authClient();
|
||||
const { error } = await client.auth.exchangeCodeForSession(code);
|
||||
if (!error)
|
||||
return NextResponse.redirect(new URL("/dashboard", request.url));
|
||||
}
|
||||
return NextResponse.redirect(new URL("/dashboard?auth=failed", request.url));
|
||||
}
|
||||
19
app/dashboard/[id]/page.tsx
Normal file
19
app/dashboard/[id]/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { DraftEditor } from "@/components/DraftEditor";
|
||||
export default async function Draft({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">YOUR WORLD, BEFORE THE WORLD SEES IT</span>
|
||||
<h1>At the drawing board.</h1>
|
||||
<DraftEditor id={(await params).id} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
55
app/dashboard/page.tsx
Normal file
55
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import { Dashboard } from "@/components/Dashboard";
|
||||
import { admin, user, required } from "@/lib/supabase";
|
||||
import Link from "next/link";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function DashboardPage() {
|
||||
const account = await user();
|
||||
const history = account
|
||||
? required(
|
||||
await admin()
|
||||
.from("game_sessions")
|
||||
.select("id,difficulty,created_at,current_round")
|
||||
.eq("owner_id", account.id)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(20),
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
{account ? (
|
||||
<>
|
||||
<span className="eyebrow">THE CARTOGRAPHER’S DESK</span>
|
||||
<h1>My expedition.</h1>
|
||||
<Dashboard />
|
||||
{history.length > 0 && (
|
||||
<section className="token-box">
|
||||
<h2>Recent journeys</h2>
|
||||
{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
|
||||
</span>
|
||||
<Link
|
||||
className="button"
|
||||
href={`/${s.current_round >= 5 ? "results" : "play"}/${s.id}`}
|
||||
>
|
||||
Open →
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<SignIn />
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
106
app/downloads/page.tsx
Normal file
106
app/downloads/page.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
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() {
|
||||
const fabric = existsSync("public/downloads/minegeo-fabric-1.21.11.jar"),
|
||||
plugin = existsSync("public/downloads/minegeo-plugin-1.21.11.jar");
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">
|
||||
MADE FOR THE PEOPLE WHO KNOW THEIR WORLDS
|
||||
</span>
|
||||
<h1>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.
|
||||
</p>
|
||||
<div className="tool-grid">
|
||||
<article className="tool-card">
|
||||
<Blocks size={34} />
|
||||
<span className="eyebrow">FOR PLAYERS</span>
|
||||
<h2>Fabric capture mod.</h2>
|
||||
<p className="muted">
|
||||
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 at least five viewpoints, then upload</li>
|
||||
<li>Only terrain received by your client enters the map</li>
|
||||
</ul>
|
||||
{fabric ? (
|
||||
<a
|
||||
className="button primary"
|
||||
href="/downloads/minegeo-fabric-1.21.11.jar"
|
||||
download
|
||||
>
|
||||
<Download size={16} /> Download Fabric mod
|
||||
</a>
|
||||
) : (
|
||||
<p className="muted">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.
|
||||
</p>
|
||||
</article>
|
||||
<article className="tool-card">
|
||||
<Server size={34} />
|
||||
<span className="eyebrow">FOR SERVER ADMINISTRATORS</span>
|
||||
<h2>Server export plugin.</h2>
|
||||
<p className="muted">
|
||||
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>Automatically choose safe surface viewpoints</li>
|
||||
<li>Link a contributor token and upload resumably</li>
|
||||
<li>Vanilla blocks/fluids, simplified lighting</li>
|
||||
</ul>
|
||||
{plugin ? (
|
||||
<a
|
||||
className="button primary"
|
||||
href="/downloads/minegeo-plugin-1.21.11.jar"
|
||||
download
|
||||
>
|
||||
<Download size={16} /> Download server plugin
|
||||
</a>
|
||||
) : (
|
||||
<p className="muted">The plugin build is not available yet.</p>
|
||||
)}
|
||||
<pre>
|
||||
/minegeo area world -256 -256 256 256{"\n"}/minegeo export My
|
||||
Overworld{"\n"}/minegeo link <dashboard token>{"\n"}/minegeo
|
||||
upload
|
||||
</pre>
|
||||
<p className="muted" style={{ fontSize: 11 }}>
|
||||
Put the JAR in plugins/ and restart. Set website and server-name
|
||||
in plugins/MineGeo/config.yml. Permission: minegeo.admin.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<div className="contribute-banner">
|
||||
<div>
|
||||
<h2>Your map, your credit.</h2>
|
||||
<p>
|
||||
Uploaders are credited separately. Plugin exports and player
|
||||
captures stay clearly labeled.
|
||||
</p>
|
||||
</div>
|
||||
<Link className="button" href="/dashboard">
|
||||
Open my dashboard <ExternalLink size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
app/error.tsx
Normal file
15
app/error.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
export default function ErrorPage({ reset }: { reset: () => void }) {
|
||||
return (
|
||||
<main className="section page-content">
|
||||
<h1>Lost connection.</h1>
|
||||
<p>
|
||||
We couldn’t load this part of the world. Check your connection and try
|
||||
again.
|
||||
</p>
|
||||
<button className="button primary" onClick={reset}>
|
||||
Try again
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
1457
app/globals.css
Normal file
1457
app/globals.css
Normal file
File diff suppressed because it is too large
Load Diff
21
app/layout.tsx
Normal file
21
app/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
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({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
19
app/moderation/page.tsx
Normal file
19
app/moderation/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Moderation } from "@/components/Moderation";
|
||||
export default function Review() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">THE COMMUNITY’S COMPASS</span>
|
||||
<h1>Map moderation.</h1>
|
||||
<p className="lead">
|
||||
Review map quality and coverage before a world enters the public
|
||||
catalog.
|
||||
</p>
|
||||
<Moderation />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
app/not-found.tsx
Normal file
13
app/not-found.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Link from "next/link";
|
||||
export default function NotFound() {
|
||||
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>
|
||||
<Link className="button primary" href="/servers">
|
||||
Explore worlds →
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
195
app/page.tsx
Normal file
195
app/page.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
Compass,
|
||||
Cuboid,
|
||||
Map,
|
||||
MapPin,
|
||||
Move,
|
||||
Timer,
|
||||
} from "lucide-react";
|
||||
import { Header, Footer, EmptyState } from "@/components/Shell";
|
||||
import { MapCard } from "@/components/MapCard";
|
||||
import { catalog } from "@/lib/catalog";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function Home() {
|
||||
const maps = await catalog();
|
||||
const serverCount = new Set(maps.map((m) => m.server_id)).size;
|
||||
const locations = maps.reduce((n, m) => n + m.panorama_count, 0);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
<section className="hero">
|
||||
<div className="hero-grid" />
|
||||
<div className="hero-copy">
|
||||
<div className="status-label">
|
||||
<span className="live-dot" /> MINECRAFT WORLDS. REAL EXPLORATION.
|
||||
</div>
|
||||
<h1>
|
||||
Get lost in a<br />
|
||||
<span>familiar world.</span>
|
||||
</h1>
|
||||
<p>
|
||||
Somewhere between the blocks, there’s a place you know.
|
||||
<br className="desktop" /> Look around. Trust your instincts. Make
|
||||
your guess.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Link href="/servers" className="button primary">
|
||||
Find your next world <ArrowRight size={18} />
|
||||
</Link>
|
||||
<Link href="/about" className="text-link">
|
||||
How it works <ArrowUpRight size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hero-notes">
|
||||
<span>
|
||||
<Compass size={15} /> 360° panoramas
|
||||
</span>
|
||||
<span>
|
||||
<Move size={15} /> No roads. No moving.
|
||||
</span>
|
||||
<span>
|
||||
<Cuboid size={15} /> Just you and the blocks.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="hero-art"
|
||||
aria-label="Minecraft terrain map illustration"
|
||||
>
|
||||
<div className="artifact-caption">
|
||||
YOUR NEXT ADVENTURE IS OUT THERE
|
||||
</div>
|
||||
<div className="floating-coord">
|
||||
<span className="square-dot" /> OVERWORLD
|
||||
<span className="coord-muted">X ? · Z ?</span>
|
||||
</div>
|
||||
<div className="map-art-frame">
|
||||
<Image
|
||||
src="/generated/paper-proof/map.webp"
|
||||
alt="Top-down map of the real Paper renderer test world"
|
||||
width={1024}
|
||||
height={1024}
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
<div className="map-grid" />
|
||||
<div className="map-pin">
|
||||
<MapPin size={38} fill="currentColor" />
|
||||
</div>
|
||||
<div className="map-corner top-left" />
|
||||
<div className="map-corner bottom-right" />
|
||||
</div>
|
||||
<span className="compass-art">
|
||||
<Compass size={42} />
|
||||
<small>N</small>
|
||||
</span>
|
||||
<div className="floating-tag">
|
||||
<MapPin size={15} /> ONE WORLD. COUNTLESS POSSIBILITIES.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="grass-divider">
|
||||
<span />
|
||||
</div>
|
||||
<section className="world-stats">
|
||||
<div>
|
||||
<strong>{serverCount}</strong>
|
||||
<span>COMMUNITY SERVERS</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{maps.length}</strong>
|
||||
<span>WORLDS TO EXPLORE</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{locations}</strong>
|
||||
<span>PLACES TO GET LOST</span>
|
||||
</div>
|
||||
<div className="stat-note">
|
||||
<span className="live-dot" />
|
||||
Built by explorers.
|
||||
<br />
|
||||
For explorers.
|
||||
</div>
|
||||
</section>
|
||||
<section className="section catalog-section">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="eyebrow">PICK A WORLD. DROP A PIN.</span>
|
||||
<h2>Where to today?</h2>
|
||||
</div>
|
||||
<Link className="text-link" href="/servers">
|
||||
All worlds <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
{maps.length ? (
|
||||
<div className="cards-grid">
|
||||
{maps.slice(0, 3).map((m, i) => (
|
||||
<MapCard map={m} index={i} key={m.id} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="The world is yours to map">
|
||||
The catalog is waiting for its first approved community map.
|
||||
Capture a world with our mod or server plugin.
|
||||
</EmptyState>
|
||||
)}
|
||||
</section>
|
||||
<section className="section journey">
|
||||
<div className="journey-intro">
|
||||
<span className="eyebrow">A NEW WAY TO SEE MINECRAFT</span>
|
||||
<h2>
|
||||
Every block
|
||||
<br />
|
||||
tells a story.
|
||||
</h2>
|
||||
<p>
|
||||
That mountain. That oddly placed tower. That very suspicious dirt
|
||||
hut. You’ve been here before… right?
|
||||
</p>
|
||||
<Link href="/about" className="text-link">
|
||||
Learn how to play <ArrowRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="steps">
|
||||
<article>
|
||||
<span>01</span>
|
||||
<Compass />
|
||||
<h3>Take a look around</h3>
|
||||
<p>Find clues in a full 360° view of a Minecraft world.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span>02</span>
|
||||
<Map />
|
||||
<h3>Trust your coordinates</h3>
|
||||
<p>Open the map and pin the spot where you think you are.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span>03</span>
|
||||
<Timer />
|
||||
<h3>Beat the clock</h3>
|
||||
<p>Five rounds. One world. How close can you get?</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section className="section contribute-banner">
|
||||
<div>
|
||||
<span className="eyebrow">YOUR SERVER DESERVES TO BE EXPLORED</span>
|
||||
<h2>Put your world on the map.</h2>
|
||||
<p>
|
||||
Bring your builds, your landscapes, and your community to MineGeo.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/downloads" className="button primary">
|
||||
Create a map <ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
app/pair/page.tsx
Normal file
32
app/pair/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Pairing } from "@/components/Pairing";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import { user } from "@/lib/supabase";
|
||||
export default async function Pair({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ code?: string }>;
|
||||
}) {
|
||||
const { code } = await searchParams;
|
||||
const account = await user();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
{!account ? (
|
||||
<>
|
||||
<p className="muted">
|
||||
Sign in first, then reopen the pairing link from Minecraft.
|
||||
</p>
|
||||
<SignIn />
|
||||
</>
|
||||
) : code && /^[A-F0-9]{8}$/.test(code) ? (
|
||||
<Pairing code={code} />
|
||||
) : (
|
||||
<p>Open the pairing link shown by your Fabric mod.</p>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
app/play/[id]/page.tsx
Normal file
8
app/play/[id]/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Game } from "@/components/Game";
|
||||
export default async function Play({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
return <Game id={(await params).id} />;
|
||||
}
|
||||
17
app/results/[id]/page.tsx
Normal file
17
app/results/[id]/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Results } from "@/components/Results";
|
||||
export default async function ResultPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<Results id={(await params).id} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
app/servers/[id]/page.tsx
Normal file
54
app/servers/[id]/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { StartGame } from "@/components/StartGame";
|
||||
import { getMap } from "@/lib/catalog";
|
||||
export default async function ServerPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
let map;
|
||||
try {
|
||||
map = await getMap(id);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">
|
||||
{map.source === "plugin" ? "SERVER EXPORT" : "PLAYER CONTRIBUTION"} ·
|
||||
MINECRAFT 1.21.11
|
||||
</span>
|
||||
<h1>{map.name}</h1>
|
||||
<p className="lead">{map.description}</p>
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
<div className="detail-map">
|
||||
<Image
|
||||
src={map.mapImage || map.image || ""}
|
||||
alt={`${map.name} surface map`}
|
||||
width={1024}
|
||||
height={1024}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="card-meta">
|
||||
<span>{map.panorama_count} panoramas</span>
|
||||
<span>Overworld surface</span>
|
||||
<span>
|
||||
{map.bounds.maxX - map.bounds.minX} ×{" "}
|
||||
{map.bounds.maxZ - map.bounds.minZ} blocks
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<StartGame datasetId={id} />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
app/servers/page.tsx
Normal file
28
app/servers/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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 maps = await catalog();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<span className="eyebrow">THE COMMUNITY AT YOUR COORDINATES</span>
|
||||
<h1>Explore the worlds.</h1>
|
||||
<p className="lead">
|
||||
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>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user