wip tournament list

This commit is contained in:
yohlo
2025-09-13 14:59:02 -05:00
parent a35c688a64
commit 3be2284da9
3 changed files with 183 additions and 76 deletions

View File

@@ -28,7 +28,7 @@ const Avatar = ({
color="initials" color="initials"
size={size} size={size}
radius={radius} radius={radius}
w="fit-content" w={size}
styles={{ styles={{
image: { image: {
objectFit: "contain", objectFit: "contain",

View File

@@ -1,8 +1,10 @@
import { Badge, Card, Text, Image, Stack, Flex } from "@mantine/core"; import { Badge, Card, Text, Stack, Group, Box, ThemeIcon } from "@mantine/core";
import { Tournament } from "@/features/tournaments/types"; import { Tournament } from "@/features/tournaments/types";
import { useMemo } from "react"; import { useMemo } from "react";
import { CaretRightIcon } from "@phosphor-icons/react"; import { TrophyIcon, CalendarIcon, MapPinIcon, UsersIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import Avatar from "@/components/avatar";
import { motion } from "framer-motion";
interface TournamentCardProps { interface TournamentCardProps {
tournament: Tournament; tournament: Tournament;
@@ -10,64 +12,68 @@ interface TournamentCardProps {
export const TournamentCard = ({ tournament }: TournamentCardProps) => { export const TournamentCard = ({ tournament }: TournamentCardProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const displayDate = useMemo(() => { const displayDate = useMemo(() => {
if (!tournament.start_time) return null; if (!tournament.start_time) return null;
const date = new Date(tournament.start_time); const date = new Date(tournament.start_time);
if (isNaN(date.getTime())) return null; if (isNaN(date.getTime())) return null;
return date.toLocaleDateString("en-US", { return date.toLocaleDateString(undefined, {
year: "numeric", month: 'short',
month: "long", day: 'numeric',
day: "numeric", year: 'numeric'
}); });
}, [tournament.start_time]); }, [tournament.start_time]);
const enrollmentDeadline = tournament.enroll_time
? new Date(tournament.enroll_time)
: new Date(tournament.start_time);
const isEnrollmentOpen = enrollmentDeadline > new Date();
const enrolledTeamsCount = tournament.teams?.length || 0;
return ( return (
<Card <motion.div
shadow="sm" whileHover={{ y: -4, scale: 1.02 }}
padding="lg" whileTap={{ scale: 0.98 }}
radius="md" transition={{ type: "spring", stiffness: 300 }}
withBorder
style={{ cursor: "pointer" }}
onClick={() => navigate({ to: `/tournaments/${tournament.id}` })}
> >
<Stack> <Card
<Flex align="center" gap="md"> withBorder
<Image radius="lg"
maw={100} p="lg"
mah={100} style={{
fit="contain" cursor: "pointer",
src={ height: "100%",
tournament.logo transition: "box-shadow 0.2s ease"
? `/api/files/tournaments/${tournament.id}/${tournament.logo}` }}
: undefined onClick={() => navigate({ to: `/tournaments/${tournament.id}` })}
} onMouseEnter={(e) => {
alt={tournament.name} e.currentTarget.style.boxShadow = "var(--mantine-shadow-md)";
fallbackSrc={"TODO"} }}
/> onMouseLeave={(e) => {
<Stack ta="center" mx="auto" gap="0"> e.currentTarget.style.boxShadow = "none";
<Text size="lg" fw={800}> }}
{tournament.name} <CaretRightIcon size={12} weight="bold" /> >
<Stack gap="md" h="100%">
<Group gap="md" align="flex-start">
<Avatar
size={60}
radius="md"
name={tournament.name}
src={
tournament.logo
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
: undefined
}
>
<TrophyIcon size={24} />
</Avatar>
<Text fw={700} size="lg" lineClamp={2} my='auto'>
{tournament.name}
</Text> </Text>
{displayDate && ( </Group>
<Text c="dimmed" size="xs" fw={600}> </Stack>
{displayDate} </Card>
</Text> </motion.div>
)}
<Stack gap={4} mt={4}>
{/* TODO: Add medalists when data is available */}
<Badge variant="dot" color="gold">
Longer Team Name Goes Here
</Badge>
<Badge variant="dot" color="silver">
Some Team
</Badge>
<Badge variant="dot" color="orange">
Medium Team Name
</Badge>
</Stack>
</Stack>
</Flex>
</Stack>
</Card>
); );
}; };

View File

@@ -1,8 +1,10 @@
import { List, ListItem, Skeleton, Text } from "@mantine/core"; import { Stack, Skeleton, Text, Group, Box, ThemeIcon } from "@mantine/core";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import Avatar from "@/components/avatar"; import Avatar from "@/components/avatar";
import { TournamentInfo } from "../types"; import { TournamentInfo } from "../types";
import { useCallback } from "react"; import { useCallback } from "react";
import { motion } from "framer-motion";
import { TrophyIcon, CalendarIcon, MapPinIcon } from "@phosphor-icons/react";
interface TournamentListProps { interface TournamentListProps {
tournaments: TournamentInfo[]; tournaments: TournamentInfo[];
@@ -12,31 +14,130 @@ interface TournamentListProps {
const TournamentList = ({ tournaments, loading = false }: TournamentListProps) => { const TournamentList = ({ tournaments, loading = false }: TournamentListProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleClick = useCallback((tournamentId: string) => const handleClick = useCallback((tournamentId: string) =>
navigate({ to: `/tournaments/${tournamentId}` }), [navigate]); navigate({ to: `/tournaments/${tournamentId}` }), [navigate]);
if (loading) return <List>
{Array.from({ length: 10 }).map((_, i) => (
<ListItem py='xs' key={`skeleton-${i}`}
icon={<Skeleton height={40} width={40} />}
>
<Skeleton height={20} width={200} />
</ListItem>
))}
</List>
return <List> if (loading) {
{tournaments?.map((tournament) => ( return (
<ListItem key={tournament.id} <Stack gap="sm">
py='xs' {Array.from({ length: 6 }).map((_, i) => (
icon={<Avatar radius='xs' size={40} name={`${tournament.name}`} src={`/api/files/tournaments/${tournament.id}/${tournament.logo}`} />} <Box key={`skeleton-${i}`} p="md">
style={{ cursor: 'pointer' }} <Group gap="md">
onClick={() => handleClick(tournament.id)} <Skeleton height={60} width={60} radius="md" />
> <Stack gap="xs" style={{ flex: 1 }}>
<Text fw={500}>{`${tournament.name}`}</Text> <Skeleton height={20} width="60%" />
</ListItem> <Skeleton height={16} width="40%" />
))} <Skeleton height={16} width="30%" />
</List> </Stack>
</Group>
</Box>
))}
</Stack>
);
}
if (!tournaments?.length) {
return (
<Box ta="center" py="xl">
<ThemeIcon size="xl" variant="light" radius="md" mb="md">
<TrophyIcon size={32} />
</ThemeIcon>
<Text c="dimmed" size="lg">
No tournaments found
</Text>
</Box>
);
}
return (
<Stack gap="xs">
{tournaments.map((tournament, index) => {
const startDate = tournament.start_time ? new Date(tournament.start_time) : null;
return (
<motion.div
key={tournament.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
whileHover={{ y: -2 }}
whileTap={{ scale: 0.98 }}
>
<Box
p="md"
style={{
borderRadius: "var(--mantine-radius-md)",
border: "1px solid var(--mantine-color-gray-3)",
cursor: "pointer",
transition: "all 0.2s ease",
}}
onClick={() => handleClick(tournament.id)}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "var(--mantine-color-gray-0)";
e.currentTarget.style.borderColor = "var(--mantine-primary-color-filled)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
e.currentTarget.style.borderColor = "var(--mantine-color-gray-3)";
}}
>
<Group gap="md" wrap="nowrap">
<Avatar
size={60}
radius="md"
name={tournament.name}
src={
tournament.logo
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
: undefined
}
style={{
border: "2px solid var(--mantine-primary-color-light)",
}}
>
<TrophyIcon size={24} />
</Avatar>
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
<Text fw={600} size="lg" lineClamp={1}>
{tournament.name}
</Text>
<Group gap="lg" wrap="wrap">
{tournament.location && (
<Group gap="xs">
<ThemeIcon size="xs" variant="light" radius="sm">
<MapPinIcon size={12} />
</ThemeIcon>
<Text size="sm" c="dimmed" lineClamp={1}>
{tournament.location}
</Text>
</Group>
)}
{startDate && !isNaN(startDate.getTime()) && (
<Group gap="xs">
<ThemeIcon size="xs" variant="light" radius="sm">
<CalendarIcon size={12} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{startDate.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</Text>
</Group>
)}
</Group>
</Stack>
</Group>
</Box>
</motion.div>
);
})}
</Stack>
);
} }
export default TournamentList; export default TournamentList;