Files
MineGeo/components/Dashboard.tsx

201 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { statusMessage } from "@/shared/i18n";
import { useI18n } from "@/components/I18nProvider";
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 { t } = useI18n();
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);
setError("");
} 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!,
);
const { error: signOutError } = await client.auth.signOut();
if (signOutError) {
setError("Could not sign out. Please try again.");
return;
}
router.refresh();
}
return (
<>
<div className="dashboard-toolbar">
<p className="muted">
{t(
"Your maps start privately. You decide when they’re ready for review. Contributions are hidden here after five days. Their panoramas and direct links stay available.",
)}
</p>
<div className="inline-actions">
<Link className="button" href="/settings">
{t("Account settings")}
</Link>
<button className="button" onClick={logout}>
{t("Sign out")}
</button>
</div>
</div>
{error && (
<p role="alert" className="error-message">
{t(error)}
</p>
)}
{!maps.length && (
<div className="empty-state">
<h2>{t("A blank map is a beginning.")}</h2>
<p>
{t(
"Install the Fabric mod to capture while you explore, or the server plugin to export your world.",
)}
</p>
<Link className="button primary" href="/downloads">
{t("Get the capture tools →")}
</Link>
</div>
)}
{maps.map((m) => (
<article className="draft-row" key={m.id}>
<div>
<span className="eyebrow">
{m.source === "plugin" ? t("SERVER EXPORT") : t("PLAYER CAPTURE")}
</span>
<h3>{m.name}</h3>
<span className="badge">
{m.contribution_status
? t("Contribution: {status}", {
status: t(statusMessage(m.contribution_status)),
})
: m.update_state
? t("Update: {status}", {
status: t(statusMessage(m.update_state)),
})
: t(statusMessage(m.state))}
</span>{" "}
<span className="muted">
{m.panorama_count} {t("locations")}
</span>
{m.banned_at ? (
<p className="error-message">
{t(
"Map banned after three confirmed reports. Open your map to appeal.",
)}
</p>
) : m.report_strikes > 0 ? (
<p className="muted">
{m.report_strikes}
{t("/3 confirmed reports")}
</p>
) : null}
<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 ? t("Open contribution →") : t("Open map →")}
</Link>
</article>
))}
<section className="token-box">
<span className="eyebrow">{t("FOR SERVER ADMINISTRATORS")}</span>
<h2>{t("Link your server plugin.")}</h2>
<p className="muted">
{t("Create a contributor token, then run")}{" "}
<code>/minegeo link &lt;token&gt;</code>{" "}
{t("in your server console. Fabric uses browser pairing instead.")}
</p>
<button className="button" disabled={busy} onClick={newToken}>
{t("Generate plugin token")}
</button>
{token && (
<>
<p className="success-message">
{t(
"Copy this token now. It is shown once. Keep it out of public chat.",
)}
</p>
<code>{token}</code>
</>
)}
{tokens.map((entry) => (
<div className="draft-row" key={entry.id}>
<span>
{entry.label}{" "}
<small className="muted">
{entry.revoked ? t("· revoked") : ""}
</small>
</span>
{!entry.revoked && (
<button className="button" onClick={() => revoke(entry.id)}>
{t("Revoke")}
</button>
)}
</div>
))}
</section>
</>
);
}