founders card
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user