various improvements
This commit is contained in:
@@ -12,6 +12,8 @@ import {
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import EditEnrolledTeams from "./edit-enrolled-teams";
|
||||
import ListLink from "@/components/list-link";
|
||||
import { RichTextEditor } from "@/components/rich-text-editor";
|
||||
import React from "react";
|
||||
|
||||
interface ManageTournamentProps {
|
||||
tournamentId: string;
|
||||
@@ -37,6 +39,8 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
close: closeEditTeams,
|
||||
} = useSheet();
|
||||
|
||||
const [v, setV] = React.useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
@@ -86,7 +90,9 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
opened={editRulesOpened}
|
||||
onChange={closeEditRules}
|
||||
>
|
||||
<p>Test</p>
|
||||
<RichTextEditor value={v} onChange={setV} />
|
||||
|
||||
{v}
|
||||
</Sheet>
|
||||
|
||||
<Sheet
|
||||
|
||||
@@ -32,7 +32,7 @@ const Profile = ({ id }: ProfileProps) => {
|
||||
|
||||
return <>
|
||||
<Header tournament={tournament} />
|
||||
<Box m='sm' mt='lg'>
|
||||
<Box mt='lg'>
|
||||
<SwipeableTabs tabs={tabs} />
|
||||
</Box>
|
||||
</>;
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { Badge, Card, Text, Stack, Group, Box, ThemeIcon } from "@mantine/core";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Text,
|
||||
Stack,
|
||||
Group,
|
||||
Box,
|
||||
ThemeIcon,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Tournament } from "@/features/tournaments/types";
|
||||
import { useMemo } from "react";
|
||||
import { TrophyIcon, CalendarIcon, MapPinIcon, UsersIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
TrophyIcon,
|
||||
CalendarIcon,
|
||||
MapPinIcon,
|
||||
UsersIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface TournamentCardProps {
|
||||
tournament: Tournament;
|
||||
@@ -18,9 +31,9 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
const date = new Date(tournament.start_time);
|
||||
if (isNaN(date.getTime())) return null;
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}, [tournament.start_time]);
|
||||
|
||||
@@ -31,33 +44,42 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
const enrolledTeamsCount = tournament.teams?.length || 0;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -4, scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
<UnstyledButton
|
||||
w="100%"
|
||||
onClick={() => navigate({ to: `/tournaments/${tournament.id}` })}
|
||||
style={{ borderRadius: "var(--mantine-radius-md)" }}
|
||||
styles={{
|
||||
root: {
|
||||
"&:hover": {
|
||||
transform: "translateY(-2px)",
|
||||
transition: "transform 0.15s ease",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
withBorder
|
||||
radius="lg"
|
||||
radius="md"
|
||||
p="lg"
|
||||
w="100%"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
height: "100%",
|
||||
transition: "box-shadow 0.2s ease"
|
||||
transition: "all 0.15s ease",
|
||||
border: "1px solid var(--mantine-color-default-border)",
|
||||
}}
|
||||
onClick={() => navigate({ to: `/tournaments/${tournament.id}` })}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.boxShadow = "var(--mantine-shadow-md)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
styles={{
|
||||
root: {
|
||||
"&:hover": {
|
||||
borderColor: "var(--mantine-primary-color-filled)",
|
||||
boxShadow: "var(--mantine-shadow-sm)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" h="100%">
|
||||
<Group gap="md" align="flex-start">
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="md" align="center">
|
||||
<Avatar
|
||||
size={60}
|
||||
radius="md"
|
||||
size={120}
|
||||
radius="sm"
|
||||
name={tournament.name}
|
||||
src={
|
||||
tournament.logo
|
||||
@@ -65,15 +87,41 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TrophyIcon size={24} />
|
||||
<TrophyIcon size={20} />
|
||||
</Avatar>
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="lg" lineClamp={2}>
|
||||
{tournament.name}
|
||||
</Text>
|
||||
{displayDate && (
|
||||
<Group gap="xs">
|
||||
<ThemeIcon
|
||||
size="sm"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color="gray"
|
||||
>
|
||||
<CalendarIcon size={12} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed">
|
||||
{displayDate}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Text fw={700} size="lg" lineClamp={2} my='auto'>
|
||||
{tournament.name}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size="sm" variant="light" radius="sm" color="gray">
|
||||
<UsersIcon size={12} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed">
|
||||
{enrolledTeamsCount} team
|
||||
{enrolledTeamsCount !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</UnstyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack, Skeleton, Text, Group, Box, ThemeIcon } from "@mantine/core";
|
||||
import { List, ListItem, Divider, Skeleton, Text, Group, Box, ThemeIcon, Stack } from "@mantine/core";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { TournamentInfo } from "../types";
|
||||
import { useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import React from "react";
|
||||
import { TrophyIcon, CalendarIcon, MapPinIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface TournamentListProps {
|
||||
@@ -11,6 +11,50 @@ interface TournamentListProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface TournamentListItemProps {
|
||||
tournament: TournamentInfo;
|
||||
}
|
||||
|
||||
const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps) => {
|
||||
const startDate = tournament.start_time ? new Date(tournament.start_time) : null;
|
||||
|
||||
return (
|
||||
<Group justify="space-between" w="100%">
|
||||
<Stack gap={2}>
|
||||
<Text fw={500} size="sm">
|
||||
{tournament.name}
|
||||
</Text>
|
||||
<Group gap="md">
|
||||
{tournament.location && (
|
||||
<Group gap={4}>
|
||||
<ThemeIcon size="xs" variant="light" radius="sm" color="gray">
|
||||
<MapPinIcon size={10} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" c="dimmed">
|
||||
{tournament.location}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{startDate && !isNaN(startDate.getTime()) && (
|
||||
<Group gap={4}>
|
||||
<ThemeIcon size="xs" variant="light" radius="sm" color="gray">
|
||||
<CalendarIcon size={10} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" c="dimmed">
|
||||
{startDate.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
});
|
||||
|
||||
const TournamentList = ({ tournaments, loading = false }: TournamentListProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -19,20 +63,23 @@ const TournamentList = ({ tournaments, loading = false }: TournamentListProps) =
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Box key={`skeleton-${i}`} p="md">
|
||||
<Group gap="md">
|
||||
<Skeleton height={60} width={60} radius="md" />
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
<Skeleton height={20} width="60%" />
|
||||
<Skeleton height={16} width="40%" />
|
||||
<Skeleton height={16} width="30%" />
|
||||
</Stack>
|
||||
</Group>
|
||||
</Box>
|
||||
<List>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<ListItem
|
||||
key={`skeleton-${i}`}
|
||||
py="xs"
|
||||
icon={<Skeleton height={40} width={40} radius="sm" />}
|
||||
>
|
||||
<Stack gap={4}>
|
||||
<Skeleton height={16} width={200} />
|
||||
<Group gap="md">
|
||||
<Skeleton height={12} width={80} />
|
||||
<Skeleton height={12} width={100} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
))}
|
||||
</Stack>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,96 +97,40 @@ const TournamentList = ({ tournaments, loading = false }: TournamentListProps) =
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<AnimatePresence>
|
||||
{tournaments.map((tournament, index) => {
|
||||
const startDate = tournament.start_time ? new Date(tournament.start_time) : null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={`tournament-${tournament.id}-${index}`}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.01 }}
|
||||
whileHover={{ y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
<List>
|
||||
{tournaments.map((tournament) => (
|
||||
<>
|
||||
<ListItem
|
||||
key={tournament.id}
|
||||
p="xs"
|
||||
icon={
|
||||
<Avatar
|
||||
radius="sm"
|
||||
size={40}
|
||||
name={tournament.name}
|
||||
src={
|
||||
tournament.logo
|
||||
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TrophyIcon size={16} />
|
||||
</Avatar>
|
||||
}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleClick(tournament.id)}
|
||||
styles={{
|
||||
itemWrapper: { width: "100%" },
|
||||
itemLabel: { width: "100%" }
|
||||
}}
|
||||
w="100%"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</Stack>
|
||||
<TournamentListItem tournament={tournament} />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Group, Stack, ThemeIcon, Text } from "@mantine/core";
|
||||
import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core";
|
||||
import { Tournament } from "../../types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack align="center" gap="lg">
|
||||
<Stack align="center" gap={0}>
|
||||
<Avatar
|
||||
name={tournament.name}
|
||||
src={
|
||||
@@ -29,13 +29,13 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
: undefined
|
||||
}
|
||||
radius="md"
|
||||
size={200}
|
||||
size={300}
|
||||
px="xs"
|
||||
withBorder={false}
|
||||
>
|
||||
<TrophyIcon size={32} />
|
||||
</Avatar>
|
||||
<Stack gap="xs">
|
||||
<Flex gap="xs" direction="row" wrap="wrap" justify="space-around">
|
||||
{tournament.location && (
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size="sm" variant="light" radius="sm">
|
||||
@@ -64,16 +64,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size="sm" variant="light" radius="sm">
|
||||
<UsersIcon size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed">
|
||||
{teamCount} teams enrolled
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user