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