Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AdminNav from "../components/admin/AdminNav";
import AdminPanel from "../components/admin/AdminPanel";

export default function AdminPage() {
Expand All @@ -6,6 +7,7 @@ export default function AdminPage() {
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl text-[var(--tfmc-cream)] sm:text-4xl">
Staff panel
</h1>
<AdminNav current="accounts" />
<AdminPanel />
</main>
);
Expand Down
21 changes: 21 additions & 0 deletions frontend/app/admin/players/[uuid]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from "next/link";
import AdminNav from "../../../components/admin/AdminNav";
import PlayerProfile from "../../../components/admin/PlayerProfile";

export default async function AdminPlayerPage({ params }: { params: Promise<{ uuid: string }> }) {
const { uuid } = await params;
const id = decodeURIComponent(uuid);
return (
<main className="relative mx-auto flex min-h-[calc(100dvh-var(--tfmc-header-h))] max-w-4xl flex-col px-6 py-16">
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl text-[var(--tfmc-cream)] sm:text-4xl">
Staff panel
</h1>
<AdminNav current="players" />
<Link href="/admin/players" className="mt-4 text-sm text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]">
← All players
</Link>
{/* Keyed so moving between players starts afresh, with no earlier player's responses landing. */}
<PlayerProfile key={id} uuid={id} />
</main>
);
}
26 changes: 26 additions & 0 deletions frontend/app/admin/players/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import AdminNav from "../../components/admin/AdminNav";
import PlayersDirectory from "../../components/admin/PlayersDirectory";
import { parsePlayerSort } from "../../../lib/admin/api";

type SearchParams = Promise<Record<string, string | string[] | undefined>>;

function first(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}

export default async function AdminPlayersPage({ searchParams }: { searchParams: SearchParams }) {
const params = await searchParams;
return (
<main className="relative mx-auto flex min-h-[calc(100dvh-var(--tfmc-header-h))] max-w-5xl flex-col px-6 py-16">
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl text-[var(--tfmc-cream)] sm:text-4xl">
Staff panel
</h1>
<AdminNav current="players" />
<PlayersDirectory
initialQuery={first(params.q) ?? ""}
initialSort={parsePlayerSort(first(params.sort))}
initialPage={Number.parseInt(first(params.page) ?? "1", 10) || 1}
/>
</main>
);
}
27 changes: 27 additions & 0 deletions frontend/app/components/admin/AdminNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from "next/link";

const LINKS = [
{ href: "/admin", label: "Accounts", key: "accounts" },
{ href: "/admin/players", label: "Players", key: "players" },
] as const;

export default function AdminNav({ current }: { current: (typeof LINKS)[number]["key"] }) {
return (
<nav aria-label="Staff panel" className="mt-4 flex gap-5 border-b border-[color-mix(in_srgb,var(--tfmc-cream)_14%,transparent)]">
{LINKS.map((link) => (
<Link
key={link.key}
href={link.href}
aria-current={link.key === current ? "page" : undefined}
className={`-mb-px border-b-2 pb-2 text-sm font-semibold transition-colors ${
link.key === current
? "border-[var(--tfmc-accent)] text-[var(--tfmc-cream)]"
: "border-transparent text-[var(--tfmc-stone)] hover:text-[var(--tfmc-cream)]"
}`}
>
{link.label}
</Link>
))}
</nav>
);
}
42 changes: 4 additions & 38 deletions frontend/app/components/admin/AdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import Link from "next/link";
import { FormEvent, useCallback, useEffect, useState } from "react";
import { AccountApiError } from "../../../lib/account/api";
import {
Expand All @@ -14,6 +13,7 @@ import {
type AdminMe,
} from "../../../lib/admin/api";
import AccountActions from "./AccountActions";
import { StaffGateMessage, gateKind, type GateKind } from "./StaffGate";

const panelClass =
"mt-6 rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_18%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_28%,transparent)] p-5";
Expand All @@ -23,13 +23,7 @@ const buttonClass =
const inputClass =
"rounded-sm border border-[color-mix(in_srgb,var(--tfmc-cream)_25%,transparent)] bg-[color-mix(in_srgb,var(--tfmc-forest)_40%,transparent)] px-3 py-2 text-[var(--tfmc-cream)] outline-none placeholder:text-[color-mix(in_srgb,var(--tfmc-mist)_60%,transparent)] focus:border-[var(--tfmc-accent)]";

type Load =
| { kind: "loading" }
| { kind: "signed_out" }
| { kind: "forbidden" }
| { kind: "unavailable" }
| { kind: "error" }
| { kind: "ready"; me: AdminMe; staff: AdminAccount[] };
type Load = { kind: "loading" } | { kind: GateKind } | { kind: "ready"; me: AdminMe; staff: AdminAccount[] };

export default function AdminPanel() {
const [load, setLoad] = useState<Load>({ kind: "loading" });
Expand All @@ -43,13 +37,7 @@ export default function AdminPanel() {
const [me, staff] = await Promise.all([getAdminMe(), getStaff()]);
setLoad({ kind: "ready", me, staff });
} catch (err) {
const status = err instanceof AccountApiError ? err.status : 0;
setLoad(
status === 401 ? { kind: "signed_out" }
: status === 403 ? { kind: "forbidden" }
: status === 503 ? { kind: "unavailable" }
: { kind: "error" }
);
setLoad({ kind: gateKind(err) });
}
}, []);

Expand Down Expand Up @@ -88,29 +76,7 @@ export default function AdminPanel() {
}

if (load.kind === "loading") return <p className="mt-6 text-[var(--tfmc-mist)]">Loading…</p>;
if (load.kind === "signed_out") {
return (
<p className="mt-6 text-[var(--tfmc-mist)]">
<Link href="/account" className="text-[var(--tfmc-accent)] underline-offset-2 hover:underline">
Sign in with Discord
</Link>{" "}
to use the staff panel.
</p>
);
}
if (load.kind === "forbidden") {
return <p className="mt-6 text-[var(--tfmc-mist)]">This page is for TFMC staff only.</p>;
}
if (load.kind === "unavailable") {
return <p className="mt-6 text-[var(--tfmc-mist)]">The staff panel isn’t available yet.</p>;
}
if (load.kind === "error") {
return (
<p className="mt-6 text-sm text-[#e8a0a0]" role="alert">
We couldn’t load the staff panel. Please refresh the page.
</p>
);
}
if (load.kind !== "ready") return <StaffGateMessage kind={load.kind} />;

const { me, staff } = load;

Expand Down
Loading