more for the overhaul
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m49s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 30s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 9m40s

This commit is contained in:
yohlo
2026-07-22 16:41:44 -07:00
parent 86cda294f9
commit 22c282d8fa
13 changed files with 419 additions and 250 deletions
@@ -108,7 +108,8 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({
<ScoreReportCta matches={myLiveMatches} />
{isTournamentOver && (
<Box px="lg" w="100%">
<Box>
<SectionHeading label="Final Results" />
<Podium tournament={tournament} />
</Box>
)}
@@ -27,6 +27,11 @@
background-color: var(--mantine-color-default-hover);
}
.tile:disabled {
opacity: 0.5;
cursor: default;
}
@media (prefers-reduced-motion: no-preference) {
.tile {
transition: background-color 120ms ease-out, transform 120ms cubic-bezier(0.32, 0.72, 0, 1);
@@ -31,12 +31,13 @@ const RulesContent = lazy(
interface NavTileProps {
label: string;
Icon: Icon;
onClick: () => void;
onClick?: () => void;
disabled?: boolean;
}
/** A single square-ish nav tile with icon-over-label and press feedback. */
const NavTile = ({ label, Icon, onClick }: NavTileProps) => (
<UnstyledButton className={classes.tile} onClick={onClick}>
export const NavTile = ({ label, Icon, onClick, disabled }: NavTileProps) => (
<UnstyledButton className={classes.tile} onClick={onClick} disabled={disabled}>
<span className={classes.icon}>
<Icon size={22} weight="bold" />
</span>
@@ -175,25 +175,27 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Alert>
)}
{!tournament.regional && <Podium tournament={tournament} />}
{hasGroupStage && (
<Stack gap={0}>
{hasGroupStage && (
<ListLink
label={`View Groups`}
to={`/tournaments/${tournament.id}/groups`}
Icon={ListDashes}
/>
)}
<ListLink
label={`View Groups`}
to={`/tournaments/${tournament.id}/groups`}
Icon={ListDashes}
label={`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon}
/>
)}
<ListLink
label={`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon}
/>
{isTournamentPredictable(tournament) && (
<ListLink
label={`View Predictions`}
to={`/tournaments/${tournament.id}/predictions`}
Icon={WizardOrbIcon}
/>
)}
{isTournamentPredictable(tournament) && (
<ListLink
label={`View Predictions`}
to={`/tournaments/${tournament.id}/predictions`}
Icon={WizardOrbIcon}
/>
)}
</Stack>
{renderTeamStatsTable()}
</Stack>
</Container>
@@ -1,4 +1,3 @@
import ListButton from "@/components/list-button";
import Sheet from "@/components/sheet/sheet";
import { useSheet } from "@/hooks/use-sheet";
import { UserListIcon } from "@phosphor-icons/react";
@@ -7,6 +6,7 @@ import { useFreeAgents } from "../../queries";
import { Text } from "@mantine/core";
import PlayerList from "@/features/players/components/player-list";
import { Player } from "@/features/players/types";
import { NavTile } from "../started-tournament/sections/nav-grid";
interface EnrolledPlayersListButtonProps {
tournamentId: string;
@@ -24,11 +24,7 @@ const EnrolledPlayersListButton: React.FC<EnrolledPlayersListButtonProps> = ({ t
return (
<>
<ListButton
label={`View Enrolled Players (${count})`}
Icon={UserListIcon}
onClick={open}
/>
<NavTile label={`Players (${count})`} Icon={UserListIcon} onClick={open} />
<Sheet title="Enrolled Players" opened={isOpen} onChange={toggle}>
{count === 0 ? (
@@ -1,71 +0,0 @@
import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core";
import { Tournament } from "../../types";
import GlitchAvatar from "@/components/glitch-avatar";
import {
CalendarIcon,
MapPinIcon,
TrophyIcon,
UsersIcon,
} from "@phosphor-icons/react";
import { useMemo } from "react";
const Header = ({ tournament }: { tournament: Tournament }) => {
const tournamentStart = useMemo(
() => new Date(tournament.start_time),
[tournament.start_time]
);
return (
<Stack align="center" gap={16}>
<GlitchAvatar
name={tournament.name}
src={
tournament.logo
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
: undefined
}
glitchSrc={
tournament.glitch_logo
? `/api/files/tournaments/${tournament.id}/${tournament.glitch_logo}`
: undefined
}
radius="md"
size={300}
>
<TrophyIcon size={32} />
</GlitchAvatar>
<Flex gap="xs" direction="column" justify="space-around" align="center">
{tournament.location && (
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<MapPinIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournament.location}
</Text>
</Group>
)}
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<CalendarIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournamentStart.toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
})}{" "}
at{" "}
{tournamentStart.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</Group>
</Flex>
</Stack>
);
};
export default Header;
@@ -1,15 +1,17 @@
import { Suspense, useMemo } from "react";
import { Tournament } from "../../types";
import { useAuth } from "@/contexts/auth-context";
import { Box, Card, Divider, Group, Stack, Text, Title } from "@mantine/core";
import { Box, Divider, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
import Countdown from "@/components/countdown";
import ListLink from "@/components/list-link";
import { TreeStructureIcon, UsersIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import EnrollTeam from "./enroll-team";
import EnrollFreeAgent from "./enroll-free-agent";
import TeamListButton from "./team-list-button";
import EnrolledPlayersListButton from "./enrolled-players-list-button";
import Header from "./header";
import Header from "../started-tournament/header";
import SectionHeading from "../started-tournament/sections/section-heading";
import { NavTile } from "../started-tournament/sections/nav-grid";
import TeamCardSkeleton from "@/features/teams/components/team-card-skeleton";
import TeamCard from "@/features/teams/components/team-card";
import RegionalTeamCard from "@/features/teams/components/regional-team-card";
@@ -24,6 +26,7 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
tournament,
}) => {
const { user, roles } = useAuth();
const navigate = useNavigate();
const isAdmin = useMemo(() => roles.includes("Admin"), [roles]);
const userTeam = useMemo(
@@ -52,121 +55,109 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
<Stack gap="lg">
<Header tournament={tournament} />
<Stack px="xs">
{tournament.desc && <Text px="md" ta="center" size="sm" style={{ whiteSpace: 'pre-wrap' }}>{tournament.desc}</Text>}
{tournament.desc && (
<Text px="md" size="sm" c="dimmed" style={{ whiteSpace: "pre-wrap" }}>
{tournament.desc}
</Text>
)}
<Card
withBorder
p="lg"
style={{
borderRadius:
"2px 2px var(--mantine-radius-lg) var(--mantine-radius-lg)",
borderTop: "3px solid var(--mantine-primary-color-filled)",
}}
>
<Stack gap="xs">
<Group gap="xs" align="center">
<UsersIcon size={16} />
<Title mt={4} order={5}>
Enrollment
</Title>
{isEnrollmentOpen && (
<Box ml="auto">
<Countdown
date={enrollmentDeadline}
label="Time left"
color="yellow"
/>
</Box>
<Box>
<SectionHeading
label="Enrollment"
action={
isEnrollmentOpen ? (
<Countdown date={enrollmentDeadline} />
) : undefined
}
/>
<Box px="md">
<Paper withBorder p="md">
<Stack gap="sm">
{!isUserEnrolled && !isEnrollmentOpen && (
<Text fw={600} c="dimmed" size="sm">
Enrollment has been closed for this tournament.
</Text>
)}
</Group>
{!isUserEnrolled && !isEnrollmentOpen && (
<Text fw={600} c="dimmed" size="sm">
Enrollment has been closed for this tournament.
</Text>
)}
{!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
<>
{!tournament.regional && (
<>
<EnrollTeam
tournamentId={tournament.id}
onSubmit={handleSubmit}
/>
<Divider my={0} label="or" />
</>
)}
<EnrollFreeAgent
tournamentId={tournament.id}
isRegional={tournament.regional}
/>
</>
)}
{isUserEnrolled && (
<>
<Suspense fallback={<TeamCardSkeleton />}>
{tournament.regional === true ? (
<RegionalTeamCard teamId={userTeam.id} />
) : (
<TeamCard teamId={userTeam.id} />
)}
</Suspense>
{tournament.regional !== true && (
<>
<UpdateTeam tournamentId={tournament.id} teamId={userTeam.id} />
{isEnrollmentOpen && (
<UnenrollTeam
{!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
<>
{!tournament.regional && (
<>
<EnrollTeam
tournamentId={tournament.id}
teamId={userTeam.id}
onSubmit={handleSubmit}
/>
)}
</>
)}
</>
)}
<Divider my={0} label="or" />
</>
)}
<EnrollFreeAgent
tournamentId={tournament.id}
isRegional={tournament.regional}
/>
</>
)}
{
isFreeAgent && isEnrollmentOpen && (
{isUserEnrolled && (
<>
<Suspense fallback={<TeamCardSkeleton />}>
{tournament.regional === true ? (
<RegionalTeamCard teamId={userTeam.id} />
) : (
<TeamCard teamId={userTeam.id} />
)}
</Suspense>
{tournament.regional !== true && (
<>
<UpdateTeam tournamentId={tournament.id} teamId={userTeam.id} />
{isEnrollmentOpen && (
<UnenrollTeam
tournamentId={tournament.id}
teamId={userTeam.id}
onSubmit={handleSubmit}
/>
)}
</>
)}
</>
)}
{isFreeAgent && isEnrollmentOpen && (
<EnrolledFreeAgent
tournamentId={tournament.id}
isRegional={tournament.regional}
/>
)
}
</Stack>
</Card>
</Stack>
)}
</Stack>
</Paper>
</Box>
</Box>
<Box>
<Divider />
{isAdmin && (
<ListLink
label={`Manage ${tournament.name}`}
to={`/admin/tournaments/${tournament.id}`}
Icon={UsersIcon}
/>
)}
<ListLink
label={`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon}
disabled
/>
{tournament.regional === true ? (
(tournament.teams && tournament.teams.length > 0) ? (
<TeamListButton teams={tournament.teams} isRegional={true} />
) : (
<EnrolledPlayersListButton tournamentId={tournament.id} />
)
) : (
<TeamListButton teams={tournament.teams || []} isRegional={false} />
)}
<RulesListButton tournamentId={tournament.id} />
<Divider mb="sm" />
<Box px="md">
<SimpleGrid cols={3} spacing="sm" verticalSpacing="sm">
{isAdmin && (
<NavTile
label="Manage"
Icon={UsersIcon}
onClick={() =>
navigate({ to: `/admin/tournaments/${tournament.id}` })
}
/>
)}
<NavTile label="Bracket" Icon={TreeStructureIcon} disabled />
{tournament.regional === true ? (
(tournament.teams && tournament.teams.length > 0) ? (
<TeamListButton teams={tournament.teams} isRegional={true} />
) : (
<EnrolledPlayersListButton tournamentId={tournament.id} />
)
) : (
<TeamListButton teams={tournament.teams || []} isRegional={false} />
)}
<RulesListButton tournamentId={tournament.id} />
</SimpleGrid>
</Box>
</Box>
</Stack>
);
@@ -1,10 +1,10 @@
import ListButton from "@/components/list-button"
import Sheet from "@/components/sheet/sheet"
import { useSheet } from "@/hooks/use-sheet"
import { ListIcon } from "@phosphor-icons/react"
import { useTournament } from "../../queries"
import { Button, Flex, Loader, Stack } from "@mantine/core"
import { lazy, Suspense } from "react"
import { NavTile } from "../started-tournament/sections/nav-grid"
const RulesContent = lazy(() => import("./rules-content"));
@@ -18,11 +18,7 @@ const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
return (
<>
<ListButton
label={`View Rules`}
Icon={ListIcon}
onClick={open}
/>
<NavTile label="Rules" Icon={ListIcon} onClick={open} />
<Sheet title="Tournament Rules" opened={isOpen} onChange={toggle}>
<Stack gap="xs">
@@ -1,44 +1,38 @@
import { Box, Card, Divider, Flex, Group, Skeleton, Stack } from "@mantine/core";
import { Box, Divider, Group, Paper, SimpleGrid, Skeleton, Stack } from "@mantine/core";
const UpcomingTournamentSkeleton = () => {
return (
<Stack gap="lg">
<Flex px="md" justify="center" w="100%">
<Skeleton height={318} width={318} radius="lg" />
</Flex>
<Stack align="center" gap={2}>
<Skeleton height={16} w="30%" mb="md" />
<Skeleton height={16} w="30%" />
<Group px="md" gap="md" wrap="nowrap" align="center">
<Skeleton height={64} width={64} radius="md" />
<Stack gap={8} style={{ flex: 1 }}>
<Skeleton height={14} width="55%" />
<Skeleton height={14} width="40%" />
</Stack>
</Group>
<Stack px="md">
<Card
withBorder
p="lg"
style={{
borderRadius:
"2px 2px var(--mantine-radius-lg) var(--mantine-radius-lg)",
}}
>
<Skeleton height={14} width="80%" mb={16} />
<Group mb="sm" gap="xs" align="center">
<Skeleton height={32} width={16} />
<Skeleton height={32} width="20%" />
<Box ml="auto">
<Skeleton height={32} width={80} radius="sm" />
</Box>
</Group>
<Group mb="sm" gap="xs" align="center">
<Skeleton height={32} width={16} />
<Skeleton height={32} width="20%" />
<Box ml="auto">
<Skeleton height={32} width={80} radius="sm" />
</Box>
</Group>
</Card>
</Stack>
<Box>
<Skeleton height={12} width={90} ml="md" mb={10} />
<Box px="md">
<Paper withBorder p="md">
<Stack gap="sm">
<Skeleton height={36} radius="sm" />
<Skeleton height={36} radius="sm" />
</Stack>
</Paper>
</Box>
</Box>
<Box px="md">
<Divider mb="sm" />
<SimpleGrid cols={3} spacing="sm" verticalSpacing="sm">
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} height={72} radius="md" />
))}
</SimpleGrid>
</Box>
</Stack>
);
};
export default UpcomingTournamentSkeleton;
export default UpcomingTournamentSkeleton;
@@ -1,10 +1,10 @@
import ListButton from "@/components/list-button"
import Sheet from "@/components/sheet/sheet"
import TeamList from "@/features/teams/components/team-list"
import { TeamInfo } from "@/features/teams/types"
import { useSheet } from "@/hooks/use-sheet"
import { UsersIcon } from "@phosphor-icons/react"
import { useMemo } from "react"
import { NavTile } from "../started-tournament/sections/nav-grid"
interface TeamListButtonProps {
teams: TeamInfo[]
@@ -16,11 +16,7 @@ const TeamListButton: React.FC<TeamListButtonProps> = ({ teams, isRegional }) =>
const { open, isOpen, toggle } = useSheet();
return (
<>
<ListButton
label={`View Teams (${count})`}
Icon={UsersIcon}
onClick={open}
/>
<NavTile label={`Teams (${count})`} Icon={UsersIcon} onClick={open} />
<Sheet title="Enrolled Teams" opened={isOpen} onChange={toggle}>
<TeamList teams={teams} isRegional={isRegional} />
@@ -29,4 +25,4 @@ const TeamListButton: React.FC<TeamListButtonProps> = ({ teams, isRegional }) =>
)
}
export default TeamListButton;
export default TeamListButton;