167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
"use client";
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { createBrowserClient } from "@supabase/ssr";
|
||
import { useRouter } from "next/navigation";
|
||
import { api } from "./StartGame";
|
||
import type { MapListing } from "@/lib/catalog";
|
||
import type { ProcessingJob } from "@/shared/processing-progress";
|
||
import { ProcessingProgress } from "./ProcessingProgress";
|
||
type Draft = MapListing & {
|
||
upload_jobs: ProcessingJob | null;
|
||
};
|
||
export function Dashboard() {
|
||
const [maps, setMaps] = useState<Draft[]>([]),
|
||
[error, setError] = useState(""),
|
||
[tokens, setTokens] = useState<
|
||
{ id: string; label: string; revoked: boolean }[]
|
||
>([]),
|
||
[token, setToken] = useState(""),
|
||
[busy, setBusy] = useState(false);
|
||
const router = useRouter();
|
||
const load = useCallback(async () => {
|
||
try {
|
||
const [m, t] = await Promise.all([
|
||
api<{ maps: Draft[] }>("datasets"),
|
||
api<{ tokens: { id: string; label: string; revoked: boolean }[] }>(
|
||
"tokens",
|
||
),
|
||
]);
|
||
setMaps(m.maps);
|
||
setTokens(t.tokens);
|
||
} catch (e) {
|
||
setError((e as Error).message);
|
||
}
|
||
}, []);
|
||
useEffect(() => {
|
||
load();
|
||
const timer = setInterval(load, 10000);
|
||
return () => clearInterval(timer);
|
||
}, [load]);
|
||
async function newToken() {
|
||
setBusy(true);
|
||
try {
|
||
const result = await api<{ token: string }>("tokens", {
|
||
label: "Server plugin",
|
||
});
|
||
setToken(result.token);
|
||
await load();
|
||
} catch (e) {
|
||
setError((e as Error).message);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
async function revoke(id: string) {
|
||
try {
|
||
await api("tokens/" + id, undefined, "DELETE");
|
||
await load();
|
||
} catch (e) {
|
||
setError((e as Error).message);
|
||
}
|
||
}
|
||
async function logout() {
|
||
const client = createBrowserClient(
|
||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||
);
|
||
await client.auth.signOut();
|
||
router.refresh();
|
||
}
|
||
return (
|
||
<>
|
||
<div className="dashboard-toolbar">
|
||
<p className="muted">
|
||
Your maps start privately. You decide when they’re ready for review.
|
||
</p>
|
||
<div className="inline-actions">
|
||
<Link className="button" href="/settings">
|
||
Account settings
|
||
</Link>
|
||
<button className="button" onClick={logout}>
|
||
Sign out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{error && (
|
||
<p role="alert" className="error-message">
|
||
{error}
|
||
</p>
|
||
)}
|
||
{!maps.length && (
|
||
<div className="empty-state">
|
||
<h2>A blank map is a beginning.</h2>
|
||
<p>
|
||
Install the Fabric mod to capture while you explore, or the server
|
||
plugin to export your world.
|
||
</p>
|
||
<Link className="button primary" href="/downloads">
|
||
Get the capture tools →
|
||
</Link>
|
||
</div>
|
||
)}
|
||
{maps.map((m) => (
|
||
<article className="draft-row" key={m.id}>
|
||
<div>
|
||
<span className="eyebrow">
|
||
{m.source === "plugin" ? "SERVER EXPORT" : "PLAYER CAPTURE"}
|
||
</span>
|
||
<h3>{m.name}</h3>
|
||
<span className="badge">
|
||
{m.contribution_status
|
||
? `Contribution: ${m.contribution_status}`
|
||
: m.update_state
|
||
? `Update: ${m.update_state}`
|
||
: m.state}
|
||
</span>{" "}
|
||
<span className="muted">{m.panorama_count} locations</span>
|
||
<ProcessingProgress job={m.upload_jobs} />
|
||
{m.rejection_reason && (
|
||
<p className="error-message">{m.rejection_reason}</p>
|
||
)}
|
||
{m.upload_jobs?.error && (
|
||
<p className="muted">{m.upload_jobs.error}</p>
|
||
)}
|
||
</div>
|
||
<Link href={"/dashboard/" + m.id} className="button">
|
||
{m.parent_dataset_id ? "Open contribution →" : "Open map →"}
|
||
</Link>
|
||
</article>
|
||
))}
|
||
<section className="token-box">
|
||
<span className="eyebrow">FOR SERVER ADMINISTRATORS</span>
|
||
<h2>Link your server plugin.</h2>
|
||
<p className="muted">
|
||
Create a contributor token, then run{" "}
|
||
<code>/minegeo link <token></code> in your server console.
|
||
Fabric uses browser pairing instead.
|
||
</p>
|
||
<button className="button" disabled={busy} onClick={newToken}>
|
||
Generate plugin token
|
||
</button>
|
||
{token && (
|
||
<>
|
||
<p className="success-message">
|
||
Copy this token now. It is shown once. Keep it out of public chat.
|
||
</p>
|
||
<code>{token}</code>
|
||
</>
|
||
)}
|
||
{tokens.map((t) => (
|
||
<div className="draft-row" key={t.id}>
|
||
<span>
|
||
{t.label}{" "}
|
||
<small className="muted">{t.revoked ? "· revoked" : ""}</small>
|
||
</span>
|
||
{!t.revoked && (
|
||
<button className="button" onClick={() => revoke(t.id)}>
|
||
Revoke
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</section>
|
||
</>
|
||
);
|
||
}
|