Merge branch 'style' into development

This commit is contained in:
yohlo
2026-07-12 21:30:02 -07:00
72 changed files with 1547 additions and 472 deletions
+12 -2
View File
@@ -19,6 +19,7 @@ import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import FullScreenLoader from "@/components/full-screen-loader";
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
import mantineCssUrl from '@mantine/core/styles.css?url'
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
@@ -41,11 +42,10 @@ export const Route = createRootRouteWithContext<{
{
name: "viewport",
content:
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=resizes-content",
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=resizes-content, viewport-fit=cover",
},
{ name: 'description', content: 'Amicus meus madidus' },
{ name: 'keywords', content: 'FLXN, beer pong, tournament, sports, statistics, pong' },
{ name: 'theme-color', content: '#1e293b' },
{ property: 'og:title', content: 'FLXN' },
{ property: 'og:description', content: 'Amicus meus madidus' },
{ property: 'og:url', content: 'https://flexxon.app' },
@@ -182,6 +182,16 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
<head>
<HeadContent />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content={CHROME_COLORS.light.base}
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content={CHROME_COLORS.dark.base}
/>
<ColorSchemeScript />
<link rel="stylesheet" href="/styles.css" />
</head>
+16 -4
View File
@@ -1,7 +1,7 @@
import { redirect, createFileRoute, Outlet } from "@tanstack/react-router";
import Layout from "@/features/core/components/layout";
import { useServerEvents } from "@/hooks/use-server-events";
import { Flex, Loader } from "@mantine/core";
import { Group, Skeleton, Stack } from "@mantine/core";
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
@@ -26,9 +26,21 @@ export const Route = createFileRoute("/_authed")({
},
pendingComponent: () => (
<Layout>
<Flex w='100%' h="40dvh" justify="center" align="flex-end">
<Loader size='xl' />
</Flex>
<Stack gap="md" p="md" w="100%">
<Group gap="sm">
<Skeleton height={40} width={40} radius="sm" />
<Skeleton height={24} width="45%" radius="sm" />
</Group>
{Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={`authed-pending-${index}`}
height={96}
w="100%"
radius="md"
style={{ opacity: Math.max(1 - index * 0.18, 0.3) }}
/>
))}
</Stack>
</Layout>
),
});
+32 -4
View File
@@ -2,8 +2,8 @@ import { createFileRoute } from "@tanstack/react-router";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { ActivitiesTable, activityQueries } from "@/features/activities";
import { PlayersActivityTable, playerQueries } from "@/features/players";
import { Tabs } from "@mantine/core";
import { useState } from "react";
import { Box, Divider, Group, Skeleton, Stack, Tabs } from "@mantine/core";
import { Suspense, useState } from "react";
export const Route = createFileRoute("/_authed/admin/activities")({
component: Stats,
@@ -23,6 +23,30 @@ export const Route = createFileRoute("/_authed/admin/activities")({
}),
});
function ActivityRowsSkeleton({ withSearch = false }: { withSearch?: boolean }) {
return (
<Stack gap={0}>
{withSearch && (
<Box px="md" pb="sm">
<Skeleton height={42} radius="sm" />
</Box>
)}
{Array.from({ length: 8 }).map((_, index) => (
<div key={`activity-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
<Group p="md" wrap="nowrap" w="100%" justify="space-between">
<Stack gap={6}>
<Skeleton height={14} width={160} radius="sm" />
<Skeleton height={10} width={100} radius="sm" />
</Stack>
<Skeleton height={10} width={60} radius="sm" />
</Group>
<Divider />
</div>
))}
</Stack>
);
}
function Stats() {
const [activeTab, setActiveTab] = useState<string | null>("server-functions");
@@ -34,11 +58,15 @@ function Stats() {
</Tabs.List>
<Tabs.Panel value="server-functions">
<ActivitiesTable />
<Suspense fallback={<ActivityRowsSkeleton withSearch />}>
<ActivitiesTable />
</Suspense>
</Tabs.Panel>
<Tabs.Panel value="player-activity">
<PlayersActivityTable />
<Suspense fallback={<ActivityRowsSkeleton />}>
<PlayersActivityTable />
</Suspense>
</Tabs.Panel>
</Tabs>
);
@@ -1,7 +1,7 @@
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { Stack, Text, Button, Alert, LoadingOverlay, Group } from "@mantine/core";
import { Stack, Text, Button, Alert, LoadingOverlay, Group, Skeleton } from "@mantine/core";
import { useState } from "react";
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
import useConfirmTeamAssignments from "@/features/tournaments/hooks/use-confirm-team-assignments";
@@ -27,8 +27,23 @@ export const Route = createFileRoute("/_authed/admin/tournaments/$id/assign-part
},
}),
component: RouteComponent,
pendingComponent: AssignPartnersPending,
});
function AssignPartnersPending() {
return (
<Stack gap="lg">
<Stack gap="xs">
<Group gap="xs" align="baseline">
<Skeleton height={28} width={36} radius="sm" />
<Skeleton height={14} width={110} radius="sm" />
</Group>
<Skeleton height={36} w="100%" radius="sm" style={{ opacity: 0.7 }} />
</Stack>
</Stack>
);
}
interface TeamAssignment {
player1: PlayerInfo;
player2: PlayerInfo;
@@ -2,6 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { tournamentQueries } from "@/features/tournaments/queries";
import ManageTournament from "@/features/tournaments/components/manage-tournament";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
beforeLoad: async ({ context, params }) => {
@@ -23,8 +24,26 @@ export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
withPadding: false,
}),
component: RouteComponent,
pendingComponent: ManageTournamentPending,
});
function ManageTournamentPending() {
return (
<Stack gap={0}>
{Array.from({ length: 5 }).map((_, index) => (
<div key={`manage-tournament-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.14, 0.4) }}>
<Group p="md" wrap="nowrap" w="100%">
<Skeleton height={20} width={20} radius="sm" />
<Skeleton height={16} width="45%" radius="sm" />
<Skeleton ml="auto" height={20} width={20} radius="sm" />
</Group>
<Divider />
</div>
))}
</Stack>
);
}
function RouteComponent() {
const { id } = Route.useParams();
return <ManageTournament tournamentId={id} />;
@@ -2,6 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { tournamentQueries } from "@/features/tournaments/queries";
import ManageTeams from "@/features/teams/components/manage-teams";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { Box, Divider, Group, Skeleton, Stack } from "@mantine/core";
export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
beforeLoad: async ({ context, params }) => {
@@ -23,8 +24,37 @@ export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
withPadding: false,
}),
component: RouteComponent,
pendingComponent: ManageTeamsPending,
});
function ManageTeamsPending() {
return (
<Stack gap="xs">
<Box px="md">
<Skeleton height={42} radius="sm" />
</Box>
<Box px="md">
<Skeleton height={12} width={90} radius="sm" />
</Box>
<Stack gap={0}>
{Array.from({ length: 8 }).map((_, index) => (
<div key={`manage-teams-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
<Group p="xs" wrap="nowrap" w="100%">
<Skeleton height={40} width={40} radius="sm" />
<Skeleton height={16} width="45%" radius="sm" />
<Stack ml="auto" gap={6} align="flex-end">
<Skeleton height={10} width={90} radius="sm" />
<Skeleton height={10} width={70} radius="sm" />
</Stack>
</Group>
<Divider />
</div>
))}
</Stack>
</Stack>
);
}
function RouteComponent() {
const { id } = Route.useParams();
const { tournament } = Route.useRouteContext();
@@ -1,12 +1,14 @@
import ManageTournaments from "@/features/admin/components/manage-tournaments";
import { tournamentQueries } from "@/features/tournaments/queries";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
export const Route = createFileRoute("/_authed/admin/tournaments/")({
beforeLoad: async ({ context }) => {
beforeLoad: ({ context }) => {
const { queryClient } = context;
await prefetchServerQuery(queryClient, tournamentQueries.list());
prefetchServerQuery(queryClient, tournamentQueries.list());
},
loader: () => ({
header: {
@@ -19,6 +21,26 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({
component: RouteComponent,
});
function TournamentListSkeleton() {
return (
<Stack gap={0}>
{Array.from({ length: 6 }).map((_, index) => (
<div key={`manage-tournaments-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}>
<Group p="md" wrap="nowrap" w="100%">
<Skeleton height={16} width="55%" radius="sm" />
<Skeleton ml="auto" height={20} width={20} radius="sm" />
</Group>
<Divider />
</div>
))}
</Stack>
);
}
function RouteComponent() {
return <ManageTournaments />;
return (
<Suspense fallback={<TournamentListSkeleton />}>
<ManageTournaments />
</Suspense>
);
}
@@ -7,7 +7,7 @@ import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import SeedTournament from "@/features/tournaments/components/seed-tournament";
import SetupGroupStage from "@/features/tournaments/components/setup-group-stage";
import GroupStageView from "@/features/tournaments/components/group-stage-view";
import { Container, Stack, Divider, Title } from "@mantine/core";
import { Container, Stack, Divider, Title, Box, Card, Group, Skeleton, SimpleGrid } from "@mantine/core";
import { useMemo } from "react";
import { BracketData } from "@/features/bracket/types";
import { Match } from "@/features/matches/types";
@@ -37,8 +37,46 @@ export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
},
}),
component: RouteComponent,
pendingComponent: RunTournamentPending,
});
function RunTournamentPending() {
return (
<Container size="md" px={0}>
<Box p="md">
<Stack gap="md">
<Group gap={0} grow mb="md">
{Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={`run-pending-tab-${index}`}
height={44}
radius="sm"
style={{ opacity: 0.85 - index * 0.1 }}
/>
))}
</Group>
<Card withBorder radius="md" p={0}>
<Group justify="space-between" p="sm">
<Skeleton height={16} width={120} radius="sm" />
<Skeleton height={22} width={22} radius="xl" />
</Group>
</Card>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton
key={`run-pending-match-${index}`}
height={100}
radius="md"
style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}
/>
))}
</SimpleGrid>
</Stack>
</Box>
</Container>
);
}
function RouteComponent() {
const { id } = Route.useParams();
const { data: tournament } = useTournament(id);
+1 -1
View File
@@ -64,7 +64,7 @@ function Stats() {
</Button>
</Group>
<Box style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 150ms' }}>
<Suspense key={deferredViewType} fallback={<PlayerStatsTableSkeleton hideFilters />}>
<Suspense fallback={<PlayerStatsTableSkeleton hideFilters />}>
<PlayerStatsTable viewType={deferredViewType} />
</Suspense>
</Box>
@@ -4,7 +4,7 @@ import {
useTournament,
} from "@/features/tournaments/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { Container } from "@mantine/core";
import { Box, Container, Flex, Skeleton, Stack } from "@mantine/core";
import { useMemo } from "react";
import { BracketData } from "@/features/bracket/types";
import { Match } from "@/features/matches/types";
@@ -31,8 +31,49 @@ export const Route = createFileRoute("/_authed/tournaments/$id/bracket")({
},
}),
component: RouteComponent,
pendingComponent: BracketPending,
});
function BracketPending() {
const columns = [4, 2, 1];
return (
<Container size="md" px={0}>
<Box
p={0}
style={{
overflow: "hidden",
backgroundImage: `radial-gradient(circle, var(--mantine-color-default-border) 1px, transparent 1px)`,
backgroundSize: "16px 16px",
backgroundPosition: "0 0, 8px 8px",
minHeight: "70dvh",
}}
>
<Skeleton height={18} width={140} radius="sm" m={16} />
<Flex gap="xl" px={16} align="stretch">
{columns.map((count, columnIndex) => (
<Stack
key={`bracket-pending-round-${columnIndex}`}
gap="xl"
justify="space-around"
style={{ opacity: 1 - columnIndex * 0.25 }}
>
{Array.from({ length: count }).map((_, matchIndex) => (
<Skeleton
key={`bracket-pending-match-${columnIndex}-${matchIndex}`}
height={84}
width={220}
radius="md"
/>
))}
</Stack>
))}
</Flex>
</Box>
</Container>
);
}
function RouteComponent() {
const { id } = Route.useParams();
const { data: tournament } = useTournament(id);
@@ -5,7 +5,7 @@ import {
} from "@/features/tournaments/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import GroupStageView from "@/features/tournaments/components/group-stage-view";
import { Container } from "@mantine/core";
import { Box, Card, Container, Group, Skeleton, SimpleGrid, Stack } from "@mantine/core";
export const Route = createFileRoute("/_authed/tournaments/$id/groups")({
beforeLoad: async ({ context, params }) => {
@@ -28,8 +28,46 @@ export const Route = createFileRoute("/_authed/tournaments/$id/groups")({
},
}),
component: RouteComponent,
pendingComponent: GroupsPending,
});
function GroupsPending() {
return (
<Container size="md" px={0}>
<Box p="md">
<Stack gap="md">
<Group gap={0} grow mb="md">
{Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={`groups-pending-tab-${index}`}
height={44}
radius="sm"
style={{ opacity: 0.85 - index * 0.1 }}
/>
))}
</Group>
<Card withBorder radius="md" p={0}>
<Group justify="space-between" p="sm">
<Skeleton height={16} width={120} radius="sm" />
<Skeleton height={22} width={22} radius="xl" />
</Group>
</Card>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton
key={`groups-pending-match-${index}`}
height={100}
radius="md"
style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}
/>
))}
</SimpleGrid>
</Stack>
</Box>
</Container>
);
}
function RouteComponent() {
const { id } = Route.useParams();
const { data: tournament } = useTournament(id);
+7 -1
View File
@@ -23,7 +23,13 @@ export const Route = createFileRoute('/_authed/tournaments/')({
function RouteComponent() {
return <Suspense fallback={<Stack gap="md">
{Array(10).fill(null).map((_, index) => (
<Skeleton height="120px" w="100%" />
<Skeleton
key={`tournament-card-skeleton-${index}`}
height="120px"
w="100%"
radius="md"
style={{ opacity: Math.max(1 - index * 0.08, 0.35) }}
/>
))}
</Stack>}>
<TournamentCardList />