The first commit, the core and all files is adding to repo

This commit is contained in:
2026-10-04 14:20:13 +05:00
commit 53d7b263c4
94 changed files with 10668 additions and 0 deletions

48
app/about/page.tsx Normal file
View 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 />
</>
);
}

View 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 };

View 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));
}

View 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
View 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
View 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 &lt;dashboard token&gt;{"\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
View 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

File diff suppressed because it is too large Load Diff

21
app/layout.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 />
</>
);
}