refactor(admin): align admin ui with app design language

- dashboard: card grid replaced by status strip + nav-tile section + list
- nav-tile promoted to shared component
- stat tiles and usage screen de-carded, sections divided like settings
- hub run tournament back to a list row
- telemetry link restored in tools list
This commit is contained in:
2026-08-25 22:24:36 -07:00
parent 895737febc
commit 1cc75591fa
22 changed files with 511 additions and 799 deletions
@@ -1,87 +0,0 @@
import { Card, Group, Stack, Text } from "@mantine/core";
import { Trans } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react";
import { useServerQuery } from "@/lib/tanstack-query/hooks";
import { searchActivities } from "@/features/activities/server";
import { activityKeys } from "@/features/activities/queries";
import { RelativeTime } from "@/features/telemetry/components/shared";
import type { PlayerInfo } from "@/features/players/types";
const FEED_PARAMS = { perPage: 10 } as const;
const playerName = (player?: string | PlayerInfo): string | null => {
if (!player || typeof player === "string") return null;
return [player.first_name, player.last_name].filter(Boolean).join(" ") || null;
};
export const ActivityFeed = () => {
const navigate = useNavigate();
const { data } = useServerQuery({
queryKey: activityKeys.search(FEED_PARAMS),
queryFn: () => searchActivities({ data: FEED_PARAMS }),
options: {
refetchInterval: 30_000,
},
});
return (
<Card withBorder radius="md" p="md">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600} size="sm">
<Trans>Recent activity</Trans>
</Text>
</Group>
<Stack gap="xs">
{data?.items.map((activity) => {
const name = playerName(activity.player);
return (
<Group
key={activity.id}
gap="xs"
wrap="nowrap"
onClick={() => navigate({ to: "/admin/telemetry/functions" })}
style={{ cursor: "pointer" }}
>
{activity.success ? (
<CheckCircleIcon
size={14}
color="var(--mantine-color-green-6)"
style={{ flexShrink: 0 }}
/>
) : (
<XCircleIcon
size={14}
color="var(--mantine-color-red-6)"
style={{ flexShrink: 0 }}
/>
)}
<Text size="xs" truncate style={{ flex: 1, minWidth: 0 }}>
{activity.name}
{name && (
<Text component="span" size="xs" c="dimmed">
{" "}
· {name}
</Text>
)}
</Text>
<Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
<RelativeTime date={activity.created} />
</Text>
</Group>
);
})}
{data && data.items.length === 0 && (
<Text size="sm" c="dimmed">
<Trans>No activity yet</Trans>
</Text>
)}
</Stack>
</Stack>
</Card>
);
};
export default ActivityFeed;
@@ -1,62 +0,0 @@
import { Badge, Card, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import { Trans } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { CheckCircleIcon, WarningCircleIcon } from "@phosphor-icons/react";
import { useClientErrorGroups } from "@/features/telemetry/queries";
import { RelativeTime } from "@/features/telemetry/components/shared";
import { lastNDays } from "@/features/telemetry/range";
export const ErrorsCard = () => {
const navigate = useNavigate();
const range = lastNDays(30);
const { data: groups } = useClientErrorGroups({ from: range.from, to: range.to });
return (
<UnstyledButton w="100%" onClick={() => navigate({ to: "/admin/telemetry/errors" })}>
<Card withBorder radius="md" p="md">
<Stack gap="sm">
<Group justify="space-between" wrap="nowrap">
<Text fw={600} size="sm">
<Trans>Client errors</Trans>
</Text>
{groups && groups.length > 0 ? (
<Badge color="red" variant="light">
{groups.length}
</Badge>
) : (
<CheckCircleIcon size={18} color="var(--mantine-color-green-6)" weight="fill" />
)}
</Group>
{groups && groups.length > 0 ? (
<Stack gap="xs">
{groups.slice(0, 3).map((group) => (
<Group key={group.group_hash} gap="xs" wrap="nowrap" align="flex-start">
<WarningCircleIcon
size={14}
color="var(--mantine-color-red-6)"
style={{ flexShrink: 0, marginTop: 2 }}
/>
<Stack gap={0} style={{ minWidth: 0 }}>
<Text size="xs" lineClamp={1}>
{group.sample.message}
</Text>
<Text size="xs" c="dimmed">
<RelativeTime date={group.lastSeen} />
</Text>
</Stack>
</Group>
))}
</Stack>
) : (
<Text size="sm" c="dimmed">
<Trans>No unresolved errors</Trans>
</Text>
)}
</Stack>
</Card>
</UnstyledButton>
);
};
export default ErrorsCard;
@@ -1,57 +1,51 @@
import { Suspense } from "react";
import { Box, SimpleGrid, Stack } from "@mantine/core";
import { CardSkeleton, StatTileSkeleton } from "@/components/skeletons";
import { Group, SimpleGrid, Skeleton, Stack } from "@mantine/core";
import QuietBoundary from "./quiet-boundary";
import NowStrip from "./now-strip";
import TournamentCard from "./tournament-card";
import UsageCard from "./usage-card";
import ErrorsCard from "./errors-card";
import ActivityFeed from "./activity-feed";
import SectionBox from "./section-box";
import StatusStrip from "./status-strip";
import TournamentSection from "./tournament-section";
import ToolsList from "./tools-list";
const NowStripSkeleton = () => (
<SimpleGrid cols={3} spacing="xs">
<StatTileSkeleton />
<StatTileSkeleton />
<StatTileSkeleton />
</SimpleGrid>
const StripSkeleton = () => (
<SectionBox>
<Group grow>
{[0, 1, 2].map((i) => (
<Stack key={i} gap={4} align="center">
<Skeleton height={22} width={32} radius="sm" />
<Skeleton height={10} width={56} radius="sm" />
</Stack>
))}
</Group>
</SectionBox>
);
const TournamentSkeleton = () => (
<SectionBox>
<Skeleton height={24} width={180} radius="sm" />
<SimpleGrid cols={3} spacing="sm" mt="sm">
<Skeleton height={72} radius="md" />
<Skeleton height={72} radius="md" />
<Skeleton height={72} radius="md" />
</SimpleGrid>
</SectionBox>
);
export const AdminDashboard = () => (
<Stack gap="md" pb="md">
<Stack gap="md" px="md" pt="md">
<QuietBoundary>
<Suspense fallback={<NowStripSkeleton />}>
<NowStrip />
</Suspense>
</QuietBoundary>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<QuietBoundary>
<Suspense fallback={<CardSkeleton />}>
<TournamentCard />
</Suspense>
</QuietBoundary>
<QuietBoundary>
<Suspense fallback={<CardSkeleton />}>
<ErrorsCard />
</Suspense>
</QuietBoundary>
</SimpleGrid>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<QuietBoundary>
<Suspense fallback={<CardSkeleton />}>
<UsageCard />
</Suspense>
</QuietBoundary>
<QuietBoundary>
<ActivityFeed />
</QuietBoundary>
</SimpleGrid>
</Stack>
<Box>
<ToolsList />
</Box>
</Stack>
<>
<QuietBoundary>
<Suspense fallback={<StripSkeleton />}>
<SectionBox>
<StatusStrip />
</SectionBox>
</Suspense>
</QuietBoundary>
<QuietBoundary>
<Suspense fallback={<TournamentSkeleton />}>
<TournamentSection />
</Suspense>
</QuietBoundary>
<ToolsList />
</>
);
export default AdminDashboard;
@@ -1,61 +0,0 @@
import { useMemo } from "react";
import { SimpleGrid } from "@mantine/core";
import { useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import {
useClientErrorGroups,
useRollupRange,
useTelemetryRuntimeStatus,
} from "@/features/telemetry/queries";
import { StatTile } from "@/features/telemetry/components/charts";
import { isoDay, lastNDays, listDays } from "@/features/telemetry/range";
export const NowStrip = () => {
const { t } = useLingui();
const navigate = useNavigate();
const range = useMemo(() => lastNDays(7), []);
const { data: status } = useTelemetryRuntimeStatus();
const { data: errorGroups } = useClientErrorGroups({
from: lastNDays(30).from,
to: range.to,
});
const { data: rollups } = useRollupRange({
metrics: ["server_fn.count"],
from: range.from,
to: range.to,
});
const { callsToday, callsSeries } = useMemo(() => {
const byDate = new Map<string, number>();
for (const row of rollups ?? []) {
byDate.set(row.date, (byDate.get(row.date) ?? 0) + row.value);
}
return {
callsToday: byDate.get(isoDay(new Date())) ?? 0,
callsSeries: listDays(range).map((day) => byDate.get(day) ?? 0),
};
}, [rollups, range]);
const unresolved = errorGroups?.length ?? 0;
return (
<SimpleGrid cols={3} spacing="xs">
<StatTile label={t`Online now`} value={status?.activeSseConnections ?? 0} />
<StatTile
label={t`Open errors`}
value={unresolved}
caption={undefined}
onClick={() => navigate({ to: "/admin/telemetry/errors" })}
/>
<StatTile
label={t`Calls today`}
value={callsToday.toLocaleString()}
spark={callsSeries}
onClick={() => navigate({ to: "/admin/telemetry/usage" })}
/>
</SimpleGrid>
);
};
export default NowStrip;
@@ -1,5 +1,5 @@
import { Component, ReactNode } from "react";
import { Card, Text } from "@mantine/core";
import { Text } from "@mantine/core";
import { Trans } from "@lingui/react/macro";
interface QuietBoundaryProps {
@@ -20,11 +20,9 @@ export class QuietBoundary extends Component<QuietBoundaryProps, QuietBoundarySt
render() {
if (this.state.hasError) {
return (
<Card withBorder radius="md" p="md">
<Text size="sm" c="dimmed">
<Trans>Unavailable right now</Trans>
</Text>
</Card>
<Text size="sm" c="dimmed">
<Trans>Unavailable right now</Trans>
</Text>
);
}
return this.props.children;
@@ -0,0 +1,14 @@
import { ReactNode } from "react";
import { Box } from "@mantine/core";
export const SectionBox = ({ children }: { children: ReactNode }) => (
<Box
px="md"
py="sm"
style={{ borderBottom: "1px solid var(--mantine-color-default-border)" }}
>
{children}
</Box>
);
export default SectionBox;
@@ -0,0 +1,74 @@
import { useMemo } from "react";
import { Group, Stack, Text, UnstyledButton } from "@mantine/core";
import { useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import {
useClientErrorGroups,
useRollupRange,
useTelemetryRuntimeStatus,
} from "@/features/telemetry/queries";
import { isoDay, lastNDays } from "@/features/telemetry/range";
interface StatProps {
value: string | number;
label: string;
alert?: boolean;
onClick: () => void;
}
const Stat = ({ value, label, alert, onClick }: StatProps) => (
<UnstyledButton className="flxn-press" onClick={onClick}>
<Stack gap={0} align="center">
<Text fz="lg" fw={700} lh={1.3} c={alert ? "red" : undefined}>
{value}
</Text>
<Text size="xs" c="dimmed">
{label}
</Text>
</Stack>
</UnstyledButton>
);
export const StatusStrip = () => {
const { t } = useLingui();
const navigate = useNavigate();
const today = useMemo(() => isoDay(new Date()), []);
const month = useMemo(() => lastNDays(30), []);
const { data: status } = useTelemetryRuntimeStatus();
const { data: errorGroups } = useClientErrorGroups({ from: month.from, to: month.to });
const { data: rollups } = useRollupRange({
metrics: ["server_fn.count"],
from: today,
to: today,
});
const callsToday = useMemo(
() => (rollups ?? []).reduce((sum, row) => sum + row.value, 0),
[rollups]
);
const openErrors = errorGroups?.length ?? 0;
return (
<Group grow>
<Stat
value={status?.activeSseConnections ?? 0}
label={t`Online`}
onClick={() => navigate({ to: "/admin/telemetry/players" })}
/>
<Stat
value={openErrors}
label={t`Open errors`}
alert={openErrors > 0}
onClick={() => navigate({ to: "/admin/telemetry/errors" })}
/>
<Stat
value={callsToday.toLocaleString()}
label={t`Calls today`}
onClick={() => navigate({ to: "/admin/telemetry/usage" })}
/>
</Group>
);
};
export default StatusStrip;
@@ -6,6 +6,7 @@ import {
TrophyIcon,
MedalIcon,
CrownIcon,
PulseIcon,
} from "@phosphor-icons/react";
import ListLink from "@/components/list-link";
import ListButton from "@/components/list-button";
@@ -50,6 +51,11 @@ export const ToolsList = () => {
Icon={TrophyIcon}
to="/admin/tournaments"
/>
<ListLink
label={t`Telemetry`}
Icon={PulseIcon}
to="/admin/telemetry/functions"
/>
<ListLink
label={t`Award Badges`}
Icon={CrownIcon}
@@ -1,76 +0,0 @@
import { Button, Card, Group, Stack, Text } from "@mantine/core";
import { Trans, useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { PlayIcon, TrophyIcon, UsersThreeIcon, GearIcon } from "@phosphor-icons/react";
import { useCurrentTournament } from "@/features/tournaments/queries";
import { Countdown } from "@/components/countdown";
export const TournamentCard = () => {
const { t } = useLingui();
const navigate = useNavigate();
const { data: tournament } = useCurrentTournament();
if (!tournament) {
return (
<Card withBorder radius="md" p="md">
<Stack gap="sm">
<Text size="sm" c="dimmed">
<Trans>No tournaments yet</Trans>
</Text>
<Button
variant="light"
leftSection={<TrophyIcon size={16} />}
onClick={() => navigate({ to: "/admin/tournaments" })}
>
<Trans>Manage Tournaments</Trans>
</Button>
</Stack>
</Card>
);
}
const upcoming = !!tournament.start_time && Date.parse(tournament.start_time) > Date.now();
return (
<Card withBorder radius="md" p="md">
<Stack gap="sm">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={2}>
<Text fw={600} truncate>
{tournament.name}
</Text>
{upcoming && (
<Countdown date={new Date(tournament.start_time)} label={t`Starts in`} />
)}
</Stack>
<TrophyIcon size={20} />
</Group>
<Button
leftSection={<PlayIcon size={16} weight="fill" />}
fullWidth
onClick={() => navigate({ to: `/admin/tournaments/run/${tournament.id}` })}
>
<Trans>Run Tournament</Trans>
</Button>
<Group grow>
<Button
variant="light"
leftSection={<UsersThreeIcon size={16} />}
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}/enrollment` })}
>
<Trans>Enrollment</Trans>
</Button>
<Button
variant="light"
leftSection={<GearIcon size={16} />}
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}` })}
>
<Trans>Manage</Trans>
</Button>
</Group>
</Stack>
</Card>
);
};
export default TournamentCard;
@@ -0,0 +1,48 @@
import { SimpleGrid, Text, Title } from "@mantine/core";
import { useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { GearIcon, PlayIcon, UsersThreeIcon } from "@phosphor-icons/react";
import { useCurrentTournament } from "@/features/tournaments/queries";
import { NavTile } from "@/components/nav-tile";
import { Countdown } from "@/components/countdown";
import SectionBox from "./section-box";
export const TournamentSection = () => {
const { t } = useLingui();
const navigate = useNavigate();
const { data: tournament } = useCurrentTournament();
if (!tournament) return null;
const upcoming = !!tournament.start_time && Date.parse(tournament.start_time) > Date.now();
return (
<SectionBox>
<Title order={3}>{tournament.name}</Title>
{upcoming && (
<Text size="sm" c="dimmed" component="div">
<Countdown date={new Date(tournament.start_time)} label={t`Starts in`} />
</Text>
)}
<SimpleGrid cols={3} spacing="sm" mt="sm">
<NavTile
label={t`Run`}
Icon={PlayIcon}
onClick={() => navigate({ to: `/admin/tournaments/run/${tournament.id}` })}
/>
<NavTile
label={t`Enrollment`}
Icon={UsersThreeIcon}
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}/enrollment` })}
/>
<NavTile
label={t`Manage`}
Icon={GearIcon}
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}` })}
/>
</SimpleGrid>
</SectionBox>
);
};
export default TournamentSection;
@@ -1,55 +0,0 @@
import { useMemo } from "react";
import { Card, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import { Trans, useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { useRollupRange } from "@/features/telemetry/queries";
import { Sparkline } from "@/features/telemetry/components/charts";
import { fillDailySeries, lastNDays, listDays } from "@/features/telemetry/range";
export const UsageCard = () => {
const { t } = useLingui();
const navigate = useNavigate();
const range = useMemo(() => lastNDays(7), []);
const { data: rollups } = useRollupRange({
metrics: ["dau", "page_view.count"],
from: range.from,
to: range.to,
});
const { dauSeries, pageViewSeries } = useMemo(() => {
const days = listDays(range);
return {
dauSeries: fillDailySeries(rollups ?? [], "dau", "", days),
pageViewSeries: fillDailySeries(rollups ?? [], "page_view.count", "", days),
};
}, [rollups, range]);
return (
<UnstyledButton w="100%" onClick={() => navigate({ to: "/admin/telemetry/usage" })}>
<Card withBorder radius="md" p="md">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600} size="sm">
<Trans>Usage, 7 days</Trans>
</Text>
</Group>
<Stack gap={4}>
<Text size="xs" c="dimmed">
<Trans>Active players</Trans>
</Text>
<Sparkline data={dauSeries} height={36} showArea label={t`Daily active players`} />
</Stack>
<Stack gap={4}>
<Text size="xs" c="dimmed">
<Trans>Page views</Trans>
</Text>
<Sparkline data={pageViewSeries} height={36} showArea label={t`Daily page views`} />
</Stack>
</Stack>
</Card>
</UnstyledButton>
);
};
export default UsageCard;
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Box, Card, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import { Box, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import Sparkline from "./sparkline";
interface StatTileProps {
@@ -13,12 +13,9 @@ interface StatTileProps {
export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatTileProps) => {
const content = (
<Stack gap={4}>
<Text size="xs" c="dimmed">
{label}
</Text>
<Stack gap={2}>
<Group gap="xs" align="baseline" wrap="nowrap">
<Text fz={24} fw={600} lh={1.2}>
<Text fz="xl" fw={700} lh={1.2}>
{value}
</Text>
{delta !== undefined && delta !== 0 && (
@@ -28,6 +25,9 @@ export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatT
</Text>
)}
</Group>
<Text size="xs" c="dimmed">
{label}
</Text>
{caption && <Box>{caption}</Box>}
{spark && spark.length > 0 && <Sparkline data={spark} height={28} />}
</Stack>
@@ -35,19 +35,13 @@ export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatT
if (onClick) {
return (
<UnstyledButton onClick={onClick} w="100%">
<Card withBorder radius="md" p="md">
{content}
</Card>
<UnstyledButton className="flxn-press" onClick={onClick} w="100%">
{content}
</UnstyledButton>
);
}
return (
<Card withBorder radius="md" p="md">
{content}
</Card>
);
return content;
};
export default StatTile;
@@ -2,7 +2,7 @@ import { Suspense, useMemo, useState } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import {
Badge,
Card,
Divider,
Group,
SegmentedControl,
SimpleGrid,
@@ -10,7 +10,7 @@ import {
Text,
Title,
} from "@mantine/core";
import { CardSkeleton, StatTileSkeleton } from "@/components/skeletons";
import { ListSkeleton, StatTileSkeleton } from "@/components/skeletons";
import { useRollupRange } from "../queries";
import { fillDailySeries, lastNDays, latestValue, listDays, sumByDim } from "../range";
import { BarList, Sparkline, StatTile } from "./charts";
@@ -171,30 +171,30 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
/>
<StatTile label={<Trans>Sessions</Trans>} value={sessions.toLocaleString()} />
</SimpleGrid>
<Card withBorder radius="md" p="md">
<Stack gap="xs">
<Text size="xs" c="dimmed">
<Trans>Page views by route</Trans>
</Text>
<BarList items={pageViewItems} />
</Stack>
</Card>
<Stack gap="xs">
<Text size="xs" c="dimmed">
<Trans>Page views by route</Trans>
</Text>
<BarList items={pageViewItems} />
</Stack>
</Stack>
<Divider />
<Stack gap="sm">
<Title order={4}>
<Trans>Server functions</Trans>
</Title>
<Card withBorder radius="md" p="md">
<Stack gap="xs">
<Text size="xs" c="dimmed">
<Trans>Top functions by call count</Trans>
</Text>
<BarList items={serverFnItems} />
</Stack>
</Card>
<Stack gap="xs">
<Text size="xs" c="dimmed">
<Trans>Top functions by call count</Trans>
</Text>
<BarList items={serverFnItems} />
</Stack>
</Stack>
<Divider />
<Stack gap="sm">
<Title order={4}>
<Trans>Errors</Trans>
@@ -206,6 +206,8 @@ const UsageSections = ({ from, to }: { from: string; to: string }) => {
/>
</Stack>
<Divider />
<Stack gap="sm">
<Title order={4}>
<Trans>Vitals</Trans>
@@ -261,14 +263,13 @@ export const UsageScreen = () => {
<Suspense
fallback={
<Stack gap="lg">
<StatTileSkeleton withCard align="row" />
<StatTileSkeleton align="row" />
<SimpleGrid cols={3} spacing="sm">
<StatTileSkeleton withCard />
<StatTileSkeleton withCard />
<StatTileSkeleton withCard />
<StatTileSkeleton />
<StatTileSkeleton />
<StatTileSkeleton />
</SimpleGrid>
<CardSkeleton withAvatar={false} withBadge={false} />
<CardSkeleton withAvatar={false} withBadge={false} />
<ListSkeleton rows={6} withTrailing="text" />
</Stack>
}
>
@@ -1,5 +1,5 @@
import { useTournament } from "../queries";
import { Box, List } from "@mantine/core";
import { List } from "@mantine/core";
import ListButton from "@/components/list-button";
import Sheet from "@/components/sheet/sheet";
import TournamentForm from "./tournament-form";
@@ -12,9 +12,7 @@ import {
import { useSheet } from "@/hooks/use-sheet";
import ListLink from "@/components/list-link";
import EditRules from "./edit-rules";
import Button from "@/components/button";
import { Trans, useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
import { useLingui } from "@lingui/react/macro";
interface ManageTournamentProps {
tournamentId: string;
@@ -22,7 +20,6 @@ interface ManageTournamentProps {
const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
const { t } = useLingui();
const navigate = useNavigate();
const { data: tournament } = useTournament(tournamentId);
if (!tournament) throw new Error("Tournament not found.");
@@ -39,18 +36,12 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
return (
<>
<Box px="md" py="md">
<Button
leftSection={<TreeStructureIcon weight="bold" size={20} />}
onClick={() =>
navigate({ to: `/admin/tournaments/run/${tournamentId}` })
}
>
<Trans>Run Tournament</Trans>
</Button>
</Box>
<List p="0">
<ListLink
label={t`Run Tournament`}
Icon={TreeStructureIcon}
to={`/admin/tournaments/run/${tournamentId}`}
/>
<ListLink
label={t`Enrollment & Teams`}
Icon={UsersThreeIcon}
@@ -30,28 +30,9 @@ const RulesContent = lazy(
() => import("../../upcoming-tournament/rules-content")
);
interface NavTileProps {
label: string;
Icon: Icon;
onClick?: () => void;
disabled?: boolean;
}
import { NavTile } from "@/components/nav-tile";
/** A single square-ish nav tile with icon-over-label and press feedback. */
export const NavTile = ({ label, Icon, onClick, disabled }: NavTileProps) => (
<UnstyledButton
className={`${classes.tile} flxn-tile`}
onClick={onClick}
disabled={disabled}
>
<span className={`${classes.icon} flxn-tile-icon`}>
<Icon size={22} weight="bold" />
</span>
<Text size="xs" fw={600} lineClamp={2} lh={1.2}>
{label}
</Text>
</UnstyledButton>
);
export { NavTile };
interface NavGridProps {
tournament: Tournament;