founders card

This commit is contained in:
2026-09-14 13:23:14 +00:00
parent c5084170b8
commit 64759ce17d
57 changed files with 5013 additions and 188 deletions
+5 -8
View File
@@ -7,6 +7,7 @@ import {
updateAdminAccount,
} from "@/app/actions/account-actions";
import {
emptyPagePermissions,
PAGE_KEYS,
type PageKey,
type PagePermissions,
@@ -20,6 +21,7 @@ const PAGE_LABELS: Record<PageKey, string> = {
matchmaker: "Matchmaker",
ledger: "Ledger",
logs: "System logs",
"founders-card": "Founders card",
};
type Props = {
@@ -254,14 +256,9 @@ function AccountEditForm({
}
function AddAccountForm() {
const [permissions, setPermissions] = useState<PagePermissions>({
players: { read: false, write: false },
matches: { read: false, write: false },
analysis: { read: false, write: false },
matchmaker: { read: false, write: false },
ledger: { read: false, write: false },
logs: { read: false, write: false },
});
const [permissions, setPermissions] = useState<PagePermissions>(() =>
emptyPagePermissions(),
);
return (
<form
-143
View File
@@ -254,17 +254,8 @@ type Props = {
geolocationAnalytics: GeolocationAnalyticsResult;
auditEntries: AuditLogEntry[];
auditError: string | null;
pageAccess: {
players: boolean;
matches: boolean;
analysis: boolean;
matchmaker: boolean;
ledger: boolean;
logs: boolean;
};
canWritePlayers: boolean;
canWriteLedger: boolean;
isAdmin: boolean;
/** Match IDs that have a replay JSON file on disk. */
matchIdsWithReplay: number[];
};
@@ -329,10 +320,8 @@ export function AdminDashboard({
geolocationAnalytics,
auditEntries,
auditError,
pageAccess,
canWritePlayers,
canWriteLedger,
isAdmin,
matchIdsWithReplay,
}: Props) {
const replaySet = useMemo(
@@ -451,14 +440,6 @@ export function AdminDashboard({
el?.scrollIntoView({ block: "center", behavior: "smooth" });
}, [tab, highlightId]);
const tabClass = (active: boolean) =>
[
"inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium transition",
active
? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700",
].join(" ");
function editHref(u: DbUser): string {
return buildDashboardHref({
tab,
@@ -468,10 +449,6 @@ export function AdminDashboard({
});
}
const totalUsersLabel =
statsBundle?.stats?.totalUsers ?? users.length;
const totalMatchesLabel =
statsBundle?.stats?.totalMatches ?? matches.length;
const statusLabel = (status: number | null) => {
if (status === 2) return "Final";
if (status === 1) return "Live";
@@ -481,126 +458,6 @@ export function AdminDashboard({
return (
<main className="flex-1 space-y-6 px-6 py-8">
<div className="flex flex-wrap gap-2 border-b border-zinc-200 pb-4 dark:border-zinc-800">
<Link
href={buildDashboardHref({
tab: "dashboard",
highlightId,
participantRaw,
})}
className={tabClass(tab === "dashboard")}
scroll={false}
>
Overview
</Link>
{pageAccess.players ? (
<Link
href={buildDashboardHref({
tab: "players",
highlightId,
participantRaw,
})}
className={tabClass(tab === "players")}
scroll={false}
>
Players (
{totalUsersLabel.toLocaleString("en-US")}
{!statsBundle?.stats
? ` · table ${users.length.toLocaleString("en-US")}`
: null}
)
</Link>
) : null}
{pageAccess.matches ? (
<Link
href={buildDashboardHref({
tab: "matches",
highlightId,
participantRaw,
matchesFrom,
matchesTo,
matchesTzOffsetMinutes,
})}
className={tabClass(tab === "matches")}
scroll={false}
>
Matches (
{totalMatchesLabel.toLocaleString("en-US")}
{!statsBundle?.stats
? ` · table ${matches.length.toLocaleString("en-US")}`
: null}
)
</Link>
) : null}
{pageAccess.analysis ? (
<Link
href={buildDashboardHref({
tab: "analysis",
highlightId,
participantRaw,
analysisFrom,
analysisTo,
analysisPlayers:
analysisPlayerIds.length > 0
? analysisPlayerIds.join(",")
: null,
})}
className={tabClass(tab === "analysis")}
scroll={false}
>
Analysis
</Link>
) : null}
{pageAccess.matchmaker ? (
<Link
href={buildDashboardHref({
tab: "matchmaker",
highlightId,
participantRaw,
})}
className={tabClass(tab === "matchmaker")}
scroll={false}
>
Matchmaker
</Link>
) : null}
{pageAccess.ledger ? (
<Link
href={buildDashboardHref({
tab: "ledger",
highlightId,
participantRaw,
})}
className={tabClass(tab === "ledger")}
scroll={false}
>
Ledger
</Link>
) : null}
{pageAccess.logs ? (
<Link
href={buildDashboardHref({
tab: "logs",
highlightId,
participantRaw,
})}
className={tabClass(tab === "logs")}
scroll={false}
>
System logs
</Link>
) : null}
{isAdmin ? (
<Link
href="/settings"
className={tabClass(false)}
scroll={false}
>
Settings
</Link>
) : null}
</div>
<div className="mx-auto w-full max-w-[1400px]">
{tab === "dashboard" ? (
<section className="space-y-8">
+50 -22
View File
@@ -1,39 +1,67 @@
"use client";
import {
AdminNavTabs,
type AdminNavTab,
} from "@/components/admin-nav-tabs";
import type { NavPageAccess } from "@/lib/auth/permissions";
type Props = {
username?: string;
isAdmin?: boolean;
pageAccess?: NavPageAccess;
activeTab?: AdminNavTab;
playersLabel?: string;
matchesLabel?: string;
};
export function AdminHeader({ username, isAdmin = false }: Props) {
export function AdminHeader({
username,
isAdmin = false,
pageAccess,
activeTab = "overview",
playersLabel,
matchesLabel,
}: Props) {
async function logout() {
await fetch("/api/auth/logout", { method: "POST" });
window.location.href = "/login";
}
return (
<header className="flex flex-wrap items-center justify-between gap-4 border-b border-zinc-200 bg-white px-6 py-4 dark:border-zinc-800 dark:bg-zinc-950">
<div>
<h1 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
Kick Kings Admin Dashboard
</h1>
{username ? (
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
Signed in as{" "}
<span className="font-medium text-zinc-700 dark:text-zinc-300">
{username}
</span>
{isAdmin ? " · admin" : null}
</p>
) : null}
<header className="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
<div className="flex flex-wrap items-center justify-between gap-4 px-6 py-4">
<div>
<h1 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
Kick Kings Admin Dashboard
</h1>
{username ? (
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
Signed in as{" "}
<span className="font-medium text-zinc-700 dark:text-zinc-300">
{username}
</span>
{isAdmin ? " · admin" : null}
</p>
) : null}
</div>
<button
type="button"
onClick={() => void logout()}
className="rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 shadow-sm transition hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800"
>
Log out
</button>
</div>
<button
type="button"
onClick={() => void logout()}
className="rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 shadow-sm transition hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800"
>
Log out
</button>
{pageAccess ? (
<AdminNavTabs
active={activeTab}
pageAccess={pageAccess}
isAdmin={isAdmin}
playersLabel={playersLabel}
matchesLabel={matchesLabel}
/>
) : null}
</header>
);
}
+109
View File
@@ -0,0 +1,109 @@
"use client";
import Link from "next/link";
import type { NavPageAccess } from "@/lib/auth/permissions";
export type AdminNavTab =
| "overview"
| "players"
| "matches"
| "analysis"
| "matchmaker"
| "ledger"
| "logs"
| "founders-card"
| "settings";
type Props = {
active: AdminNavTab;
pageAccess: NavPageAccess;
isAdmin?: boolean;
playersLabel?: string;
matchesLabel?: string;
};
function tabClass(active: boolean) {
return [
"inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium transition",
active
? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700",
].join(" ");
}
export function AdminNavTabs({
active,
pageAccess,
isAdmin = false,
playersLabel = "Players",
matchesLabel = "Matches",
}: Props) {
return (
<nav
className="flex flex-wrap gap-2 border-t border-zinc-200 px-6 py-3 dark:border-zinc-800"
aria-label="Admin pages"
>
<Link href="/" className={tabClass(active === "overview")}>
Overview
</Link>
{pageAccess.players ? (
<Link
href="/?tab=players"
className={tabClass(active === "players")}
>
{playersLabel}
</Link>
) : null}
{pageAccess.matches ? (
<Link
href="/?tab=matches"
className={tabClass(active === "matches")}
>
{matchesLabel}
</Link>
) : null}
{pageAccess.analysis ? (
<Link
href="/?tab=analysis"
className={tabClass(active === "analysis")}
>
Analysis
</Link>
) : null}
{pageAccess.matchmaker ? (
<Link
href="/?tab=matchmaker"
className={tabClass(active === "matchmaker")}
>
Matchmaker
</Link>
) : null}
{pageAccess.ledger ? (
<Link
href="/?tab=ledger"
className={tabClass(active === "ledger")}
>
Ledger
</Link>
) : null}
{pageAccess.logs ? (
<Link href="/?tab=logs" className={tabClass(active === "logs")}>
System logs
</Link>
) : null}
{pageAccess.foundersCard ? (
<Link
href="/founders-card-design"
className={tabClass(active === "founders-card")}
>
Founders card
</Link>
) : null}
{isAdmin ? (
<Link href="/settings" className={tabClass(active === "settings")}>
Settings
</Link>
) : null}
</nav>
);
}