improvements

This commit is contained in:
yohlo
2025-10-03 02:34:45 -05:00
parent b52c79772f
commit fafe5ca3ec
11 changed files with 355 additions and 135 deletions

View File

@@ -172,6 +172,8 @@ const SlidePanel = ({
onChange={setTempValue}
{...(panelConfig.componentProps || {})}
/>
<Button mt="md" onClick={handleConfirm}>Confirm</Button>
<Button variant="subtle" onClick={closePanel} mt="sm" color="red">Cancel</Button>
</>
)}
</Box>

View File

@@ -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 (
<Box p="md">
<UnstyledButton
w="100%"
p="md"
onClick={onClick}
style={{
borderRadius: 0,
transition: "background-color 0.15s ease",
}}
styles={{
root: {
'&:hover': {
backgroundColor: 'var(--mantine-color-gray-0)',
},
},
}}
>
<Group justify="space-between" align="flex-start" w="100%">
<Stack gap={4} flex={1}>
<Group gap="xs">
@@ -68,59 +88,183 @@ const ActivityListItem = memo(({ activity }: ActivityListItemProps) => {
)}
</Stack>
</Group>
</Box>
</UnstyledButton>
);
});
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 (
<Sheet title="Activity Details" opened={isOpen} onChange={onClose}>
<Stack gap="md" p="md">
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Function Name
</Text>
<Text size="sm">{activity.name}</Text>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Status
</Text>
<Group gap="xs">
{activity.success ? (
<>
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
<Text size="sm" c="green">
Success
</Text>
</>
) : (
<>
<XIcon size={16} color="var(--mantine-color-red-6)" />
<Text size="sm" c="red">
Failed
</Text>
</>
)}
</Group>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Player
</Text>
<Text size="sm">{playerName}</Text>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Duration
</Text>
<Text size="sm">{activity.duration}ms</Text>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Created
</Text>
<Text size="sm">{formatDate(activity.created)}</Text>
</Stack>
{activity.user_agent && (
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
User Agent
</Text>
<Text size="xs" style={{ wordBreak: "break-word" }}>
{activity.user_agent}
</Text>
</Stack>
)}
{activity.error && (
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Error Message
</Text>
<Alert color="red" variant="light">
<Text size="sm" style={{ wordBreak: "break-word" }}>
{activity.error}
</Text>
</Alert>
</Stack>
)}
{activity.arguments && (
<Stack gap="xs">
<Text size="xs" fw={700} c="dimmed">
Arguments
</Text>
<Code block style={{ fontSize: "11px" }}>
{JSON.stringify(activity.arguments, null, 2)}
</Code>
</Stack>
)}
</Stack>
</Sheet>
);
});
ActivityDetailsSheet.displayName = "ActivityDetailsSheet";
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
const { data: result } = useActivities(searchParams);
return (
<>
<Stack gap={0}>
{result.items.map((activity: Activity, index: number) => (
<Box key={activity.id}>
<ActivityListItem activity={activity} />
{index < result.items.length - 1 && <Divider />}
</Box>
))}
</Stack>
{result.items.length === 0 && (
<Text ta="center" c="dimmed" py="xl">
No activities found
</Text>
)}
{result.totalPages > 1 && (
<Group justify="center" py="md">
<Pagination
total={result.totalPages}
value={page}
onChange={setPage}
size="sm"
{result.items.map((activity: Activity, index: number) => (
<Box key={activity.id}>
<ActivityListItem
activity={activity}
onClick={() => onActivityClick(activity)}
/>
</Group>
)}
{index < result.items.length - 1 && <Divider />}
</Box>
))}
</Stack>
{result.items.length === 0 && (
<Text ta="center" c="dimmed" py="xl">
No activities found
</Text>
)}
{result.totalPages > 1 && (
<Group justify="center" py="md">
<Pagination
total={result.totalPages}
value={page}
onChange={setPage}
size="sm"
/>
</Group>
)}
</>
)
);
};
export const ActivitiesTable = () => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(100);
const [search, setSearch] = useState("");
const [successFilter, setSuccessFilter] = useState<string | null>(null);
const [sortBy, setSortBy] = useState("-created");
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(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 (
<Container size="100%" px={0}>
<Stack gap="xs">
@@ -214,9 +368,19 @@ export const ActivitiesTable = () => {
</Group>
</Group>
<ActivitiesResults searchParams={searchParams} page={page} setPage={setPage} />
<ActivitiesResults
searchParams={searchParams}
page={page}
setPage={setPage}
onActivityClick={handleActivityClick}
/>
</Stack>
<ActivityDetailsSheet
activity={selectedActivity}
isOpen={detailsOpened}
onClose={handleCloseDetails}
/>
</Container>
);
};

View File

@@ -42,7 +42,7 @@ const Layout: React.FC<PropsWithChildren> = ({ 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' }}

View File

@@ -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;

View File

@@ -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 (
<Box p="md">
<Center>
<Text c="dimmed" size="sm">
Podium will appear here when the tournament is over
</Text>
</Center>
</Box>
);
}
return (
<Stack gap="xs" px="md">
{tournament.first_place && (
<Group
gap="md"
p="md"
style={{
backgroundColor: 'var(--mantine-color-yellow-light)',
borderRadius: 'var(--mantine-radius-md)',
border: '3px solid var(--mantine-color-yellow-outline)',
boxShadow: 'var(--mantine-shadow-md)',
}}
>
<ThemeIcon size="xl" color="yellow" variant="light" radius="xl">
<CrownIcon size={24} />
</ThemeIcon>
<Stack gap={4} style={{ flex: 1 }}>
<Text size="md" fw={600}>
{tournament.first_place.name}
</Text>
<Group gap="xs">
{tournament.first_place.players?.map((player) => (
<Text key={player.id} size="sm" c="dimmed">
{player.first_name} {player.last_name}
</Text>
))}
</Group>
</Stack>
</Group>
)}
{tournament.second_place && (
<Group
gap="md"
p="xs"
style={{
backgroundColor: 'var(--mantine-color-default)',
borderRadius: 'var(--mantine-radius-md)',
border: '2px solid var(--mantine-color-default-border)',
boxShadow: 'var(--mantine-shadow-sm)',
}}
>
<ThemeIcon size="lg" color="gray" variant="light" radius="xl">
<MedalIcon size={20} />
</ThemeIcon>
<Stack gap={4} style={{ flex: 1 }}>
<Text size="sm" fw={600}>
{tournament.second_place.name}
</Text>
<Group gap="xs">
{tournament.second_place.players?.map((player) => (
<Text key={player.id} size="xs" c="dimmed">
{player.first_name} {player.last_name}
</Text>
))}
</Group>
</Stack>
</Group>
)}
{tournament.third_place && (
<Group
gap="md"
p="xs"
style={{
backgroundColor: 'var(--mantine-color-orange-light)',
borderRadius: 'var(--mantine-radius-md)',
border: '2px solid var(--mantine-color-orange-outline)',
boxShadow: 'var(--mantine-shadow-sm)',
}}
>
<ThemeIcon size="lg" color="orange" variant="light" radius="xl">
<MedalIcon size={18} />
</ThemeIcon>
<Stack gap={4} style={{ flex: 1 }}>
<Text size="sm" fw={600}>
{tournament.third_place.name}
</Text>
<Group gap="xs">
{tournament.third_place.players?.map((player) => (
<Text key={player.id} size="xs" c="dimmed">
{player.first_name} {player.last_name}
</Text>
))}
</Group>
</Stack>
</Group>
)}
</Stack>
);
};

View File

@@ -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 (
<Stack gap="lg">
<Header tournament={tournament} />
@@ -42,6 +57,10 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({
))}
</Carousel>
</Box>
) : isTournamentOver ? (
<Box px="lg" w="100%">
<Podium tournament={tournament} />
</Box>
) : (
<Card withBorder radius="lg" p="xl" mx="md">
<Center>

View File

@@ -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 (
<Stack gap={0}>
<Text px="md" size="lg" fw={600}>Results</Text>
{teamStatsWithCalculations.map((stat) => {
<Text px="md" c="dimmed" size="xs" fw={500}>Sorted by win percentage</Text>
{teamStatsWithCalculations.map((stat, index) => {
return (
<Box key={stat.id}>
<UnstyledButton
w="100%"
p="md"
style={{
borderRadius: 0,
transition: "background-color 0.15s ease",
}}
styles={{
root: {
'&:hover': {
backgroundColor: 'var(--mantine-color-gray-0)',
},
},
}}
style={{ borderRadius: 0 }}
>
<Group justify="space-between" align="center" w="100%">
<Group gap="sm" align="center">
@@ -194,12 +184,12 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
<Stack gap={2}>
<Group gap='xs'>
<Text size="xs" c="dimmed">
#{stat.index + 1}
#{index + 1}
</Text>
<Text size="sm" fw={600}>
{stat.team_name}
</Text>
{stat.index === 0 && isComplete && (
{index === 0 && isComplete && (
<ThemeIcon size="xs" color="yellow" variant="light" radius="xl">
<CrownIcon size={12} />
</ThemeIcon>
@@ -259,7 +249,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Group>
</Group>
</UnstyledButton>
{stat.index < teamStatsWithCalculations.length - 1 && <Divider />}
{index < teamStatsWithCalculations.length - 1 && <Divider />}
</Box>
);
})}

View File

@@ -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;

View File

@@ -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 (

View File

@@ -1,78 +0,0 @@
import { Card, Container, createTheme, Paper, rem, Select } from "@mantine/core";
import type { MantineThemeOverride } from "@mantine/core";
const CONTAINER_SIZES: Record<string, string> = {
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",
},
});

View File

@@ -63,7 +63,9 @@ export function createBadgesService(pb: PocketBase) {
},
async clearAllBadgeProgress(): Promise<number> {
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);
}