From fafe5ca3ec76c143519a46faa597a91e0b48c199 Mon Sep 17 00:00:00 2001 From: yohlo Date: Fri, 3 Oct 2025 02:34:45 -0500 Subject: [PATCH] improvements --- .../sheet/slide-panel/slide-panel.tsx | 2 + .../components/activities-table.tsx | 232 +++++++++++++++--- src/features/core/components/layout.tsx | 2 +- .../matches/components/match-list.tsx | 2 +- .../tournaments/components/podium.tsx | 112 +++++++++ .../components/started-tournament/index.tsx | 21 +- .../components/tournament-stats.tsx | 26 +- src/features/tournaments/server.ts | 9 + src/lib/mantine/mantine-provider.tsx | 2 +- src/lib/mantine/themes/default.ts | 78 ------ src/lib/pocketbase/services/badges.ts | 4 +- 11 files changed, 355 insertions(+), 135 deletions(-) create mode 100644 src/features/tournaments/components/podium.tsx delete mode 100644 src/lib/mantine/themes/default.ts diff --git a/src/components/sheet/slide-panel/slide-panel.tsx b/src/components/sheet/slide-panel/slide-panel.tsx index c2f1f06..2bf5f83 100644 --- a/src/components/sheet/slide-panel/slide-panel.tsx +++ b/src/components/sheet/slide-panel/slide-panel.tsx @@ -172,6 +172,8 @@ const SlidePanel = ({ onChange={setTempValue} {...(panelConfig.componentProps || {})} /> + + )} diff --git a/src/features/activities/components/activities-table.tsx b/src/features/activities/components/activities-table.tsx index 8cb8529..2b07a88 100644 --- a/src/features/activities/components/activities-table.tsx +++ b/src/features/activities/components/activities-table.tsx @@ -8,9 +8,10 @@ import { Container, Divider, UnstyledButton, - Badge, Select, Pagination, + Code, + Alert, } from "@mantine/core"; import { MagnifyingGlassIcon, @@ -18,15 +19,19 @@ import { CaretDownIcon, CheckIcon, XIcon, + ChecksIcon, } from "@phosphor-icons/react"; import { Activity, ActivitySearchParams } from "../types"; import { useActivities } from "../queries"; +import Sheet from "@/components/sheet/sheet"; +import { useSheet } from "@/hooks/use-sheet"; interface ActivityListItemProps { activity: Activity; + onClick: () => void; } -const ActivityListItem = memo(({ activity }: ActivityListItemProps) => { +const ActivityListItem = memo(({ activity, onClick }: ActivityListItemProps) => { const playerName = typeof activity.player === "object" && activity.player ? `${activity.player.first_name} ${activity.player.last_name}` : "System"; @@ -37,7 +42,22 @@ const ActivityListItem = memo(({ activity }: ActivityListItemProps) => { }; return ( - + @@ -68,59 +88,183 @@ const ActivityListItem = memo(({ activity }: ActivityListItemProps) => { )} - + ); }); -const ActivitiesResults = ({ searchParams, page, setPage }: any) => { +ActivityListItem.displayName = "ActivityListItem"; + +interface ActivityDetailsSheetProps { + activity: Activity | null; + isOpen: boolean; + onClose: () => void; +} + +const ActivityDetailsSheet = memo(({ activity, isOpen, onClose }: ActivityDetailsSheetProps) => { + if (!activity) return null; + + const playerName = typeof activity.player === "object" && activity.player + ? `${activity.player.first_name} ${activity.player.last_name}` + : "System"; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleString(); + }; + + return ( + + + + + Function Name + + {activity.name} + + + + + Status + + + {activity.success ? ( + <> + + + Success + + + ) : ( + <> + + + Failed + + + )} + + + + + + Player + + {playerName} + + + + + Duration + + {activity.duration}ms + + + + + Created + + {formatDate(activity.created)} + + + {activity.user_agent && ( + + + User Agent + + + {activity.user_agent} + + + )} + + {activity.error && ( + + + Error Message + + + + {activity.error} + + + + )} + + {activity.arguments && ( + + + Arguments + + + {JSON.stringify(activity.arguments, null, 2)} + + + )} + + + ); +}); + +ActivityDetailsSheet.displayName = "ActivityDetailsSheet"; + +const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => { const { data: result } = useActivities(searchParams); return ( <> - {result.items.map((activity: Activity, index: number) => ( - - - {index < result.items.length - 1 && } - - ))} - - - {result.items.length === 0 && ( - - No activities found - - )} - - {result.totalPages > 1 && ( - - ( + + onActivityClick(activity)} /> - - )} + {index < result.items.length - 1 && } + + ))} + + + {result.items.length === 0 && ( + + No activities found + + )} + + {result.totalPages > 1 && ( + + + + )} - ) + ); }; export const ActivitiesTable = () => { const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(100); const [search, setSearch] = useState(""); const [successFilter, setSuccessFilter] = useState(null); const [sortBy, setSortBy] = useState("-created"); + const [selectedActivity, setSelectedActivity] = useState(null); + + const { + isOpen: detailsOpened, + open: openDetails, + close: closeDetails, + } = useSheet(); const searchParams: ActivitySearchParams = useMemo( () => ({ page, - perPage, + perPage: 100, name: search || undefined, success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined, sortBy, }), - [page, perPage, search, successFilter, sortBy] + [page, search, successFilter, sortBy] ); const { data: result } = useActivities(searchParams); @@ -139,6 +283,16 @@ export const ActivitiesTable = () => { return null; }; + const handleActivityClick = (activity: Activity) => { + setSelectedActivity(activity); + openDetails(); + }; + + const handleCloseDetails = () => { + setSelectedActivity(null); + closeDetails(); + }; + return ( @@ -214,9 +368,19 @@ export const ActivitiesTable = () => { - - + + + ); }; diff --git a/src/features/core/components/layout.tsx b/src/features/core/components/layout.tsx index 6e293b5..87fdb87 100644 --- a/src/features/core/components/layout.tsx +++ b/src/features/core/components/layout.tsx @@ -42,7 +42,7 @@ const Layout: React.FC = ({ children }) => { pos='relative' h='100%' mah='100%' - pb={{ base: 65, md: 0 }} + pb={{ base: 65, sm: 0 }} px={{ base: 0.01, sm: 100, md: 200, lg: 300 }} maw='100dvw' style={{ transition: 'none', overflow: 'hidden' }} diff --git a/src/features/matches/components/match-list.tsx b/src/features/matches/components/match-list.tsx index e883303..d441557 100644 --- a/src/features/matches/components/match-list.tsx +++ b/src/features/matches/components/match-list.tsx @@ -9,7 +9,7 @@ interface MatchListProps { const MatchList = ({ matches }: MatchListProps) => { const filteredMatches = matches?.filter(match => match.home && match.away && !match.bye && match.status != "tbd" - ) || []; + ).sort((a, b) => a.start_time < b.start_time ? 1 : -1) || []; if (!filteredMatches.length) { return undefined; diff --git a/src/features/tournaments/components/podium.tsx b/src/features/tournaments/components/podium.tsx new file mode 100644 index 0000000..4fa5254 --- /dev/null +++ b/src/features/tournaments/components/podium.tsx @@ -0,0 +1,112 @@ +import { Stack, Group, Text, ThemeIcon, Box, Center } from "@mantine/core"; +import { CrownIcon, MedalIcon } from "@phosphor-icons/react"; +import { Tournament } from "../types"; + +interface PodiumProps { + tournament: Tournament; +} + +export const Podium = ({ tournament }: PodiumProps) => { + if (!tournament.first_place) { + return ( + +
+ + Podium will appear here when the tournament is over + +
+
+ ); + } + + return ( + + {tournament.first_place && ( + + + + + + + {tournament.first_place.name} + + + {tournament.first_place.players?.map((player) => ( + + {player.first_name} {player.last_name} + + ))} + + + + )} + + {tournament.second_place && ( + + + + + + + {tournament.second_place.name} + + + {tournament.second_place.players?.map((player) => ( + + {player.first_name} {player.last_name} + + ))} + + + + )} + + {tournament.third_place && ( + + + + + + + {tournament.third_place.name} + + + {tournament.third_place.players?.map((player) => ( + + {player.first_name} {player.last_name} + + ))} + + + + )} + + ); +}; diff --git a/src/features/tournaments/components/started-tournament/index.tsx b/src/features/tournaments/components/started-tournament/index.tsx index 768b0e7..6e23dc6 100644 --- a/src/features/tournaments/components/started-tournament/index.tsx +++ b/src/features/tournaments/components/started-tournament/index.tsx @@ -4,11 +4,12 @@ import { useAuth } from "@/contexts/auth-context"; import { Box, Divider, Stack, Text, Card, Center } from "@mantine/core"; import { Carousel } from "@mantine/carousel"; import ListLink from "@/components/list-link"; -import { TreeStructureIcon, UsersIcon, ClockIcon } from "@phosphor-icons/react"; +import { TreeStructureIcon, UsersIcon, ClockIcon, TrophyIcon } from "@phosphor-icons/react"; import TeamListButton from "../upcoming-tournament/team-list-button"; import RulesListButton from "../upcoming-tournament/rules-list-button"; import MatchCard from "@/features/matches/components/match-card"; import Header from "./header"; +import { Podium } from "../podium"; const StartedTournament: React.FC<{ tournament: Tournament }> = ({ tournament, @@ -22,6 +23,20 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({ [tournament.matches] ); + const isTournamentOver = useMemo(() => { + const matches = tournament.matches || []; + if (matches.length === 0) return false; + + const nonByeMatches = matches.filter((match) => !(match.status === 'tbd' && match.bye === true)); + if (nonByeMatches.length === 0) return false; + + const finalsMatch = nonByeMatches.reduce((highest, current) => + (!highest || current.lid > highest.lid) ? current : highest + ); + + return finalsMatch?.status === 'ended'; + }, [tournament.matches]); + return (
@@ -42,6 +57,10 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({ ))} + ) : isTournamentOver ? ( + + + ) : (
diff --git a/src/features/tournaments/components/tournament-stats.tsx b/src/features/tournaments/components/tournament-stats.tsx index 3681bc4..fa59141 100644 --- a/src/features/tournaments/components/tournament-stats.tsx +++ b/src/features/tournaments/components/tournament-stats.tsx @@ -146,12 +146,11 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => { }; const teamStatsWithCalculations = useMemo(() => { - return sortedTeamStats.map((stat, index) => ({ + return sortedTeamStats.map((stat) => ({ ...stat, - index, winPercentage: stat.matches > 0 ? (stat.wins / stat.matches) * 100 : 0, avgCupsPerMatch: stat.matches > 0 ? stat.total_cups_made / stat.matches : 0, - })); + })).sort((a, b) => b.winPercentage - a.winPercentage);; }, [sortedTeamStats]); const renderTeamStatsTable = () => { @@ -170,23 +169,14 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => { return ( Results - {teamStatsWithCalculations.map((stat) => { + Sorted by win percentage + {teamStatsWithCalculations.map((stat, index) => { return ( @@ -194,12 +184,12 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => { - #{stat.index + 1} + #{index + 1} {stat.team_name} - {stat.index === 0 && isComplete && ( + {index === 0 && isComplete && ( @@ -259,7 +249,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => { - {stat.index < teamStatsWithCalculations.length - 1 && } + {index < teamStatsWithCalculations.length - 1 && } ); })} diff --git a/src/features/tournaments/server.ts b/src/features/tournaments/server.ts index 2b9a9fd..5ec212c 100644 --- a/src/features/tournaments/server.ts +++ b/src/features/tournaments/server.ts @@ -6,6 +6,7 @@ import { logger } from "."; import { z } from "zod"; import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result"; import { serverFnLoggingMiddleware } from "@/utils/activities"; +import { fa } from "zod/v4/locales"; export const listTournaments = createServerFn() .middleware([superTokensFunctionMiddleware]) @@ -64,6 +65,14 @@ export const enrollTeam = createServerFn() // throw new Error('You do not have permission to enroll this team'); //} + const freeAgents = await pbAdmin.getFreeAgents(tournamentId); + for (const player of team.players || []) { + const isFreeAgent = freeAgents.some(fa => fa.player?.id === player.id); + if (isFreeAgent) { + await pbAdmin.unenrollFreeAgent(player.id, tournamentId); + } + } + logger.info('Enrolling team in tournament', { tournamentId, teamId, userId }); const tournament = await pbAdmin.enrollTeam(tournamentId, teamId); return tournament; diff --git a/src/lib/mantine/mantine-provider.tsx b/src/lib/mantine/mantine-provider.tsx index f71c344..2c5c382 100644 --- a/src/lib/mantine/mantine-provider.tsx +++ b/src/lib/mantine/mantine-provider.tsx @@ -57,7 +57,7 @@ const MantineProvider = ({ children }: { children: React.ReactNode }) => { setIsHydrated(true); }, []); - const colorScheme = isHydrated ? metadata.colorScheme || "dark" : "dark"; + const colorScheme = isHydrated ? metadata.colorScheme || "auto" : "auto"; const primaryColor = isHydrated ? metadata.accentColor || "blue" : "blue"; return ( diff --git a/src/lib/mantine/themes/default.ts b/src/lib/mantine/themes/default.ts deleted file mode 100644 index 3052857..0000000 --- a/src/lib/mantine/themes/default.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Card, Container, createTheme, Paper, rem, Select } from "@mantine/core"; -import type { MantineThemeOverride } from "@mantine/core"; - -const CONTAINER_SIZES: Record = { - xxs: rem("200px"), - xs: rem("300px"), - sm: rem("400px"), - md: rem("500px"), - lg: rem("600px"), - xl: rem("1400px"), - xxl: rem("1600px"), -}; - -export const defaultTheme: MantineThemeOverride = createTheme({ - scale: 1.1, - autoContrast: true, - fontSizes: { - xs: rem("12px"), - sm: rem("14px"), - md: rem("16px"), - lg: rem("18px"), - xl: rem("20px"), - "2xl": rem("24px"), - "3xl": rem("30px"), - "4xl": rem("36px"), - "5xl": rem("48px"), - }, - spacing: { - "3xs": rem("4px"), - "2xs": rem("8px"), - xs: rem("10px"), - sm: rem("12px"), - md: rem("16px"), - lg: rem("20px"), - xl: rem("24px"), - "2xl": rem("28px"), - "3xl": rem("32px"), - }, - primaryColor: "red", - components: { - Container: Container.extend({ - vars: (_, { size, fluid }) => ({ - root: { - "--container-size": fluid - ? "100%" - : size !== undefined && size in CONTAINER_SIZES - ? CONTAINER_SIZES[size] - : rem(size), - }, - }), - }), - Paper: Paper.extend({ - defaultProps: { - p: "md", - shadow: "xl", - radius: "md", - withBorder: true, - }, - }), - - Card: Card.extend({ - defaultProps: { - p: "xl", - shadow: "xl", - radius: "var(--mantine-radius-default)", - withBorder: true, - }, - }), - Select: Select.extend({ - defaultProps: { - checkIconPosition: "right", - }, - }), - }, - other: { - style: "mantine", - }, -}); diff --git a/src/lib/pocketbase/services/badges.ts b/src/lib/pocketbase/services/badges.ts index 3ae8546..3fc7dec 100644 --- a/src/lib/pocketbase/services/badges.ts +++ b/src/lib/pocketbase/services/badges.ts @@ -63,7 +63,9 @@ export function createBadgesService(pb: PocketBase) { }, async clearAllBadgeProgress(): Promise { - const existingProgress = await pb.collection("badge_progress").getFullList(); + const existingProgress = await pb.collection("badge_progress").getFullList({ + filter: 'badge.type != "manual"', + }); for (const progress of existingProgress) { await pb.collection("badge_progress").delete(progress.id); }