Improve MineGeo exploration and mapping features
This commit is contained in:
286
app/account.css
Normal file
286
app/account.css
Normal file
@@ -0,0 +1,286 @@
|
||||
/* Shared account and authentication surfaces, layered after the Minecraft theme. */
|
||||
.auth-page {
|
||||
padding-top: 48px;
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
.auth-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr;
|
||||
gap: clamp(32px, 7vw, 100px);
|
||||
align-items: center;
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.auth-story h1 {
|
||||
font-size: clamp(42px, 4.4vw, 64px);
|
||||
line-height: 1.05;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.auth-story > p {
|
||||
color: var(--muted);
|
||||
max-width: 420px;
|
||||
line-height: 1.8;
|
||||
font-size: 15px;
|
||||
}
|
||||
.auth-story-map {
|
||||
position: relative;
|
||||
height: 210px;
|
||||
border: 1px solid #4b5c43;
|
||||
margin: 30px 0 22px;
|
||||
padding: 26px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: #c0e7a2;
|
||||
background:
|
||||
linear-gradient(90deg, #101a1cd9, #11192388),
|
||||
url("/images/example-map.webp") center/cover;
|
||||
box-shadow: inset 0 0 70px #09121977;
|
||||
}
|
||||
.auth-story-map span {
|
||||
font:
|
||||
24px MineGeoPixel,
|
||||
monospace;
|
||||
}
|
||||
.auth-benefits {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.auth-benefits span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.auth-benefits svg {
|
||||
color: var(--green);
|
||||
}
|
||||
.auth-layout .auth-card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 32px;
|
||||
background: #121c28f5;
|
||||
border: 1px solid #3a4958;
|
||||
box-shadow: 0 20px 60px #0003;
|
||||
}
|
||||
.auth-card h2 {
|
||||
font-size: 38px;
|
||||
line-height: 1.1;
|
||||
margin: 14px 0 10px;
|
||||
}
|
||||
.auth-card .field {
|
||||
font-size: 13px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.field small {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.auth-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
background: #0a121c;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.auth-tabs button {
|
||||
padding: 10px 8px;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.auth-tabs button[aria-pressed="true"] {
|
||||
color: var(--text);
|
||||
background: #223342;
|
||||
box-shadow: 0 2px 7px #0004;
|
||||
}
|
||||
.auth-recovery {
|
||||
display: block;
|
||||
margin: -4px 0 22px auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.auth-card .text-link {
|
||||
cursor: pointer;
|
||||
color: var(--green);
|
||||
}
|
||||
.auth-alternative {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 20px;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.auth-footnote {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
.auth-back {
|
||||
display: block;
|
||||
margin: 22px 0 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.auth-card input,
|
||||
.settings-card input {
|
||||
min-height: 46px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.reset-card {
|
||||
max-width: 520px;
|
||||
padding: 32px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
.reset-card h1 {
|
||||
font-size: 46px;
|
||||
}
|
||||
.account-page-heading {
|
||||
margin: 28px 0 36px;
|
||||
}
|
||||
.account-page-heading h1 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.settings-card {
|
||||
border: 1px solid #3a4958;
|
||||
background: #121c28e8;
|
||||
padding: 28px;
|
||||
box-shadow: 0 12px 30px #0002;
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.settings-heading > svg {
|
||||
margin-top: 6px;
|
||||
color: var(--green);
|
||||
flex: none;
|
||||
}
|
||||
.settings-heading h2 {
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.settings-heading p {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.account-email {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 24px 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: #0d1520;
|
||||
min-width: 0;
|
||||
}
|
||||
.account-email svg {
|
||||
flex: none;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.account-email div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
.account-email span,
|
||||
.account-email small {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.account-email strong {
|
||||
font-weight: 400;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.settings-recovery {
|
||||
margin-top: 28px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.settings-recovery h3 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settings-recovery p {
|
||||
font-size: 12px;
|
||||
}
|
||||
.map-credit {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin: 14px 0 0;
|
||||
}
|
||||
.dashboard-toolbar .inline-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.auth-layout {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 520px;
|
||||
gap: 24px;
|
||||
}
|
||||
.auth-story-map,
|
||||
.auth-benefits {
|
||||
display: none;
|
||||
}
|
||||
.auth-story h1 {
|
||||
font-size: 42px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.auth-story > p {
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.auth-page {
|
||||
padding-top: 28px;
|
||||
}
|
||||
.auth-layout .auth-card,
|
||||
.settings-card,
|
||||
.reset-card {
|
||||
padding: 22px;
|
||||
}
|
||||
.auth-story h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
.auth-card h2 {
|
||||
font-size: 32px;
|
||||
}
|
||||
.dashboard-toolbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
.dashboard-toolbar .inline-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.preview-toolbar {
|
||||
padding: 10px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authClient } from "@/lib/supabase";
|
||||
import { requestOrigin } from "@/lib/request-origin";
|
||||
import { authDestination } from "@/shared/account";
|
||||
import { createRecoveryGrant, recoveryCookie } from "@/lib/password-recovery";
|
||||
import { accountProfile } from "@/lib/profile";
|
||||
export async function GET(request: NextRequest) {
|
||||
const origin = requestOrigin(request, process.env.MINEGEO_URL);
|
||||
const code = request.nextUrl.searchParams.get("code");
|
||||
if (code) {
|
||||
const tokenHash = request.nextUrl.searchParams.get("token_hash");
|
||||
const type = request.nextUrl.searchParams.get("type");
|
||||
const next = authDestination(request.nextUrl.searchParams.get("next"));
|
||||
if (
|
||||
code ||
|
||||
(tokenHash &&
|
||||
(type === "email" || type === "recovery" || type === "magiclink"))
|
||||
) {
|
||||
const client = await authClient();
|
||||
const { error } = await client.auth.exchangeCodeForSession(code);
|
||||
if (!error) return NextResponse.redirect(new URL("/dashboard", origin));
|
||||
const { data, error } = code
|
||||
? await client.auth.exchangeCodeForSession(code)
|
||||
: await client.auth.verifyOtp({
|
||||
token_hash: tokenHash!,
|
||||
type: type as "email" | "recovery" | "magiclink",
|
||||
});
|
||||
if (!error && data.user) {
|
||||
await accountProfile(data.user);
|
||||
const destination = type === "recovery" ? "/auth/reset-password" : next;
|
||||
const response = NextResponse.redirect(new URL(destination, origin));
|
||||
if (destination === "/auth/reset-password") {
|
||||
const { data: claims } = await client.auth.getClaims();
|
||||
const sessionId = claims?.claims.session_id;
|
||||
if (typeof sessionId === "string")
|
||||
response.cookies.set(
|
||||
recoveryCookie,
|
||||
createRecoveryGrant(
|
||||
data.user.id,
|
||||
sessionId,
|
||||
process.env.SUPABASE_SECRET_KEY!,
|
||||
),
|
||||
{
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: new URL(origin).protocol === "https:",
|
||||
path: "/",
|
||||
maxAge: 600,
|
||||
},
|
||||
);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
return NextResponse.redirect(new URL("/dashboard?auth=failed", origin));
|
||||
return NextResponse.redirect(new URL("/log-in?auth=failed", origin));
|
||||
}
|
||||
|
||||
1
app/auth/confirm/route.ts
Normal file
1
app/auth/confirm/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GET } from "../callback/route";
|
||||
56
app/auth/reset-password/page.tsx
Normal file
56
app/auth/reset-password/page.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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 default async function ResetPassword() {
|
||||
const [account, jar, client] = await Promise.all([
|
||||
user(),
|
||||
cookies(),
|
||||
authClient(),
|
||||
]);
|
||||
const { data } = account ? await client.auth.getClaims() : { data: null };
|
||||
const sessionId = data?.claims.session_id;
|
||||
const valid =
|
||||
account &&
|
||||
typeof sessionId === "string" &&
|
||||
verifyRecoveryGrant(
|
||||
jar.get(recoveryCookie)?.value,
|
||||
account.id,
|
||||
sessionId,
|
||||
process.env.SUPABASE_SECRET_KEY!,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<section className="auth-card reset-card">
|
||||
<span className="eyebrow">ACCOUNT RECOVERY</span>
|
||||
<h1>A fresh start.</h1>
|
||||
{valid ? (
|
||||
<>
|
||||
<p className="muted">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.
|
||||
</p>
|
||||
<Link
|
||||
className="button primary"
|
||||
href={account ? "/settings" : "/log-in"}
|
||||
>
|
||||
Request a new reset link →
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { DraftEditor } from "@/components/DraftEditor";
|
||||
import { user } from "@/lib/supabase";
|
||||
import { redirect } from "next/navigation";
|
||||
export default async function Draft({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
if (!(await user()))
|
||||
redirect(`/log-in?next=${encodeURIComponent("/dashboard/" + id)}`);
|
||||
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} />
|
||||
<DraftEditor id={id} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
@@ -3,9 +3,11 @@ import { SignIn } from "@/components/SignIn";
|
||||
import { Dashboard } from "@/components/Dashboard";
|
||||
import { admin, user, required } from "@/lib/supabase";
|
||||
import Link from "next/link";
|
||||
import { accountProfile } from "@/lib/profile";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function DashboardPage() {
|
||||
const account = await user();
|
||||
const profile = account ? await accountProfile(account) : null;
|
||||
const history = account
|
||||
? required(
|
||||
await admin()
|
||||
@@ -24,6 +26,7 @@ export default async function DashboardPage() {
|
||||
<>
|
||||
<span className="eyebrow">THE CARTOGRAPHER’S DESK</span>
|
||||
<h1>My expedition.</h1>
|
||||
<p className="lead">Welcome back, {profile?.display_name}.</p>
|
||||
<Dashboard />
|
||||
{history.length > 0 && (
|
||||
<section className="token-box">
|
||||
|
||||
@@ -31,8 +31,12 @@ export default function Downloads() {
|
||||
<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>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
|
||||
</li>
|
||||
<li>Only terrain received by your client enters the map</li>
|
||||
<li>Contributions check the recorded server address</li>
|
||||
</ul>
|
||||
{fabric ? (
|
||||
<a
|
||||
@@ -61,8 +65,12 @@ export default function Downloads() {
|
||||
<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>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>
|
||||
</ul>
|
||||
{plugin ? (
|
||||
@@ -78,7 +86,7 @@ export default function Downloads() {
|
||||
)}
|
||||
<pre>
|
||||
{
|
||||
"/minegeo website https://your-minegeo-site.example\n/minegeo link <dashboard token>\n/minegeo area world -64 -64 64 64\n/minegeo export My Overworld\n/minegeo status\n# Wait for Export complete, then:\n/minegeo upload"
|
||||
"/minegeo website https://your-minegeo-site.example\n/minegeo server play.your-server.example\n/minegeo link <dashboard token>\n/minegeo area world -64 -64 64 64\n/minegeo export My Overworld\n/minegeo status\n# Wait for Export complete, then:\n/minegeo upload"
|
||||
}
|
||||
</pre>
|
||||
<p className="muted">
|
||||
@@ -88,9 +96,10 @@ export default function Downloads() {
|
||||
completed export; it does not capture the world.
|
||||
</p>
|
||||
<p className="muted" style={{ fontSize: 11 }}>
|
||||
Put the JAR in plugins/ and restart. Set website and server-name
|
||||
in plugins/MineGeo/config.yml, then run /minegeo reload. HTTP LAN
|
||||
addresses are supported for testing. Permission: minegeo.admin.
|
||||
Put the JAR in plugins/ and restart. Set website, server-name and
|
||||
server-address in plugins/MineGeo/config.yml, then run /minegeo
|
||||
reload. HTTP LAN addresses are supported for testing. Permission:
|
||||
minegeo.admin.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
@@ -98,8 +107,9 @@ export default function Downloads() {
|
||||
<div>
|
||||
<h2>Your map, your credit.</h2>
|
||||
<p>
|
||||
Uploaders are credited separately. Plugin exports and player
|
||||
captures stay clearly labeled.
|
||||
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">
|
||||
|
||||
167
app/globals.css
167
app/globals.css
@@ -1457,32 +1457,173 @@ pre {
|
||||
}
|
||||
/* Contributor and moderator previews use the same projection as the game. */
|
||||
.panorama-preview {
|
||||
width: min(1100px, 96vw);
|
||||
max-width: 96vw;
|
||||
padding: 16px;
|
||||
background: #0b1119;
|
||||
color: #edf2ef;
|
||||
border: 1px solid #58684b;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
width: min(1180px, calc(100vw - 64px));
|
||||
height: min(820px, calc(100dvh - 64px));
|
||||
max-width: none;
|
||||
max-height: calc(100dvh - 32px);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
border: 1px solid #455663;
|
||||
box-shadow: 0 30px 100px #0009;
|
||||
}
|
||||
.panorama-preview[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panorama-preview.is-expanded {
|
||||
width: calc(100vw - 24px);
|
||||
height: calc(100dvh - 24px);
|
||||
max-height: none;
|
||||
}
|
||||
.panorama-preview::backdrop {
|
||||
background: #000b;
|
||||
background: #03070edb;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
flex: none;
|
||||
}
|
||||
.preview-header h2 {
|
||||
font-size: 34px;
|
||||
line-height: 1;
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
.preview-header .eyebrow {
|
||||
font-size: 10px;
|
||||
}
|
||||
.preview-header-actions,
|
||||
.preview-navigation {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.icon-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: none;
|
||||
border: 1px solid #40505d;
|
||||
background: #1b2834;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon-button:hover {
|
||||
background: #2b3d4a;
|
||||
}
|
||||
.icon-button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
:where(button, a, input, textarea, select):focus-visible {
|
||||
outline: 2px solid var(--green);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.preview-location {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.preview-counter {
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font: 12px monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.preview-footer {
|
||||
padding: 15px 24px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
}
|
||||
.preview-footer span:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.preview-viewport .panorama-controls {
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
}
|
||||
.preview-viewport .panorama-controls button {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: #0d1724e8;
|
||||
}
|
||||
.preview-viewport .panorama {
|
||||
cursor: grab;
|
||||
}
|
||||
.preview-viewport .panorama:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.preview-status {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.panorama-preview {
|
||||
width: calc(100vw - 20px);
|
||||
height: calc(100dvh - 20px);
|
||||
max-height: none;
|
||||
}
|
||||
.preview-header {
|
||||
padding: 16px;
|
||||
}
|
||||
.preview-header h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.preview-footer {
|
||||
padding: 12px 16px;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.preview-location {
|
||||
flex: 1;
|
||||
}
|
||||
.preview-location > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.preview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
gap: 14px;
|
||||
padding: 12px 24px;
|
||||
flex: none;
|
||||
}
|
||||
.preview-toolbar select {
|
||||
background: #141e27;
|
||||
color: inherit;
|
||||
padding: 8px;
|
||||
color: var(--text);
|
||||
border: 1px solid #40505d;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
max-width: 300px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.preview-viewport {
|
||||
position: relative;
|
||||
height: min(65vh, 650px);
|
||||
flex: 1;
|
||||
min-height: 140px;
|
||||
overflow: hidden;
|
||||
background: #080f18;
|
||||
}
|
||||
.preview-status {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import "./block-theme.css";
|
||||
import "./account.css";
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "MineGeo — Get lost in a familiar world",
|
||||
|
||||
32
app/log-in/page.tsx
Normal file
32
app/log-in/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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 default async function LogIn({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ next?: string; auth?: string }>;
|
||||
}) {
|
||||
const query = await searchParams;
|
||||
const next = authDestination(query.next);
|
||||
if (await user())
|
||||
redirect(next === "/auth/reset-password" ? "/settings" : next);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content auth-page">
|
||||
<SignIn
|
||||
next={next}
|
||||
initialError={
|
||||
query.auth === "failed"
|
||||
? "This sign-in link expired or could not be verified. Request a new link below."
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,13 @@ export default async function Pair({
|
||||
<p className="muted">
|
||||
Sign in first, then reopen the pairing link from Minecraft.
|
||||
</p>
|
||||
<SignIn />
|
||||
<SignIn
|
||||
next={
|
||||
code && /^[A-F0-9]{8}$/.test(code)
|
||||
? `/pair?code=${code}`
|
||||
: "/dashboard"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : code && /^[A-F0-9]{8}$/.test(code) ? (
|
||||
<Pairing code={code} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from "next/image";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { StartGame } from "@/components/StartGame";
|
||||
import { getMap } from "@/lib/catalog";
|
||||
import { ContributionInstructions } from "@/components/ContributionInstructions";
|
||||
export default async function ServerPage({
|
||||
params,
|
||||
}: {
|
||||
@@ -25,6 +26,10 @@ export default async function ServerPage({
|
||||
</span>
|
||||
<h1>{map.name}</h1>
|
||||
<p className="lead">{map.description}</p>
|
||||
<p className="map-credit">Created by {map.credit}</p>
|
||||
<p className="muted">
|
||||
Map ID: <code>{map.id}</code>
|
||||
</p>
|
||||
<div className="detail-grid">
|
||||
<div>
|
||||
<div className="detail-map">
|
||||
@@ -47,6 +52,13 @@ export default async function ServerPage({
|
||||
</div>
|
||||
<StartGame datasetId={id} />
|
||||
</div>
|
||||
{map.allow_contributions && (
|
||||
<ContributionInstructions
|
||||
id={id}
|
||||
bounds={map.bounds}
|
||||
serverAddress={map.contribution_server_address}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
40
app/settings/page.tsx
Normal file
40
app/settings/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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 default async function Settings({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ password?: string }>;
|
||||
}) {
|
||||
const account = await user();
|
||||
if (!account) redirect("/log-in?next=%2Fsettings");
|
||||
const [profile, query] = await Promise.all([
|
||||
accountProfile(account),
|
||||
searchParams,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<Link className="text-link" href="/dashboard">
|
||||
← 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>
|
||||
</div>
|
||||
<AccountSettings
|
||||
email={account.email ?? ""}
|
||||
displayName={profile.display_name}
|
||||
passwordUpdated={query.password === "updated"}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user