some changes

This commit is contained in:
yohlo
2025-09-11 13:35:33 -05:00
parent c74da09bde
commit 22be6682dd
18 changed files with 113 additions and 68 deletions

16
.nitro/types/nitro-config.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
// Generated by nitro
// App Config
import type { Defu } from 'defu'
type UserAppConfig = Defu<{}, []>
declare module "nitropack/types" {
interface AppConfig extends UserAppConfig {}
interface NitroRuntimeConfig {
}
}
export {}

1
.nitro/types/nitro-imports.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {}

8
.nitro/types/nitro-routes.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
// Generated by nitro
import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi {
}
}
export {}

3
.nitro/types/nitro.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference path="./nitro-routes.d.ts" />
/// <reference path="./nitro-config.d.ts" />
/// <reference path="./nitro-imports.d.ts" />

View File

@@ -40,29 +40,23 @@ function RouteComponent() {
return { winners: [], losers: [] } return { winners: [], losers: [] }
} }
console.log('Tournament Matches:', tournament.matches)
const winnersMap = new Map<number, Match[]>() const winnersMap = new Map<number, Match[]>()
const losersMap = new Map<number, Match[]>() const losersMap = new Map<number, Match[]>()
tournament.matches.sort((a, b) => a.lid - b.lid).forEach((match) => { tournament.matches.sort((a, b) => a.lid - b.lid).forEach((match) => {
console.log('Processing Match:', match)
if (!match.is_losers_bracket) { if (!match.is_losers_bracket) {
if (!winnersMap.has(match.round)) { if (!winnersMap.has(match.round)) {
winnersMap.set(match.round, []) winnersMap.set(match.round, [])
} }
winnersMap.get(match.round)!.push(match) winnersMap.get(match.round)!.push(match)
console.log('Added to Winners Bracket:', match)
} else { } else {
if (!losersMap.has(match.round)) { if (!losersMap.has(match.round)) {
losersMap.set(match.round, []) losersMap.set(match.round, [])
} }
losersMap.get(match.round)!.push(match) losersMap.get(match.round)!.push(match)
console.log('Added to Losers Bracket:', match)
} }
}) })
// Convert to dense arrays sorted by round (0, 1, 2, 3...)
const winners = Array.from(winnersMap.entries()) const winners = Array.from(winnersMap.entries())
.sort(([a], [b]) => a - b) .sort(([a], [b]) => a - b)
.map(([, matches]) => matches) .map(([, matches]) => matches)
@@ -70,15 +64,10 @@ function RouteComponent() {
const losers = Array.from(losersMap.entries()) const losers = Array.from(losersMap.entries())
.sort(([a], [b]) => a - b) .sort(([a], [b]) => a - b)
.map(([, matches]) => matches) .map(([, matches]) => matches)
console.log('Winners Bracket (fixed):', winners)
console.log('Losers Bracket (fixed):', losers)
return { winners, losers } return { winners, losers }
}, [tournament.matches]) }, [tournament.matches])
console.log('Bracket Data:', bracket)
const handleSuccess = () => { const handleSuccess = () => {
router.navigate({ router.navigate({
to: '/admin/tournaments/$id', to: '/admin/tournaments/$id',
@@ -86,13 +75,15 @@ function RouteComponent() {
}) })
} }
console.log('Tournament:', tournament) const handleStartMatch = (match: Match) => {
}
return ( return (
<Container size="md"> <Container size="md">
{ {
tournament.matches?.length ? tournament.matches?.length ?
<BracketView bracket={bracket} onAnnounce={console.log} /> <BracketView bracket={bracket} onStartMatch={console.log} />
: ( : (
<SeedTournament <SeedTournament
tournamentId={tournament.id} tournamentId={tournament.id}

View File

@@ -1,5 +1,7 @@
import { Container, ContainerProps } from "@mantine/core"; import { Container, ContainerProps, Box } from "@mantine/core";
import useRouterConfig from "@/features/core/hooks/use-router-config"; import useRouterConfig from "@/features/core/hooks/use-router-config";
import BackButton from "@/features/core/components/back-button";
import SettingsButton from "@/features/core/components/settings-button";
interface PageProps extends ContainerProps, React.PropsWithChildren { interface PageProps extends ContainerProps, React.PropsWithChildren {
noPadding?: boolean; noPadding?: boolean;
@@ -16,8 +18,15 @@ const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => {
m={0} m={0}
maw={fullWidth ? '100%' : 600} maw={fullWidth ? '100%' : 600}
mx="auto" mx="auto"
pos="relative"
{...props} {...props}
> >
{header.collapsed && header.withBackButton && (
<BackButton offsetY={0} />
)}
{header.collapsed && header.settingsLink && (
<SettingsButton to={header.settingsLink} offsetY={0} />
)}
{children} {children}
</Container> </Container>
); );

View File

@@ -109,7 +109,7 @@ function SwipeableTabs({
); );
return ( return (
<Box> <Box style={{ touchAction: "pan-y" }}>
<Box <Box
ref={setRootRef} ref={setRootRef}
pos="sticky" pos="sticky"
@@ -137,7 +137,7 @@ function SwipeableTabs({
onClick={() => changeTab(index)} onClick={() => changeTab(index)}
style={{ style={{
flex: 1, flex: 1,
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-md)", padding: "var(--mantine-spacing-sm) var(--mantine-spacing-xs)",
textAlign: "center", textAlign: "center",
color: color:
activeTab === index activeTab === index
@@ -155,7 +155,7 @@ function SwipeableTabs({
component="span" component="span"
style={{ style={{
display: "inline-block", display: "inline-block",
paddingInline: "1rem", paddingInline: "0.5rem",
paddingBottom: "0.25rem", paddingBottom: "0.25rem",
}} }}
ref={setControlRef(index)} ref={setControlRef(index)}
@@ -176,6 +176,7 @@ function SwipeableTabs({
overflow: "hidden", overflow: "hidden",
height: carouselHeight === "auto" ? "auto" : `${carouselHeight}px`, height: carouselHeight === "auto" ? "auto" : `${carouselHeight}px`,
transition: "height 300ms ease", transition: "height 300ms ease",
touchAction: "pan-y",
}} }}
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (

View File

@@ -1,16 +1,16 @@
import React, { useCallback, useMemo } from "react"; import React, { useMemo } from "react";
import { Text, ScrollArea } from "@mantine/core"; import { Text, ScrollArea } from "@mantine/core";
import { MatchCard } from "./match-card";
import { BracketData } from "../types"; import { BracketData } from "../types";
import { Bracket } from "./bracket"; import { Bracket } from "./bracket";
import useAppShellHeight from "@/hooks/use-appshell-height"; import useAppShellHeight from "@/hooks/use-appshell-height";
import { Match } from "@/features/matches/types";
interface BracketViewProps { interface BracketViewProps {
bracket: BracketData; bracket: BracketData;
onAnnounce?: (teamOne: any, teamTwo: any) => void; onStartMatch?: (match: Match) => void;
} }
const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => { const BracketView: React.FC<BracketViewProps> = ({ bracket, onStartMatch }) => {
const height = useAppShellHeight(); const height = useAppShellHeight();
const orders = useMemo(() => { const orders = useMemo(() => {
const map: Record<number, number> = {}; const map: Record<number, number> = {};
@@ -32,14 +32,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => {
<Text fw={600} size="md" m={16}> <Text fw={600} size="md" m={16}>
Winners Bracket Winners Bracket
</Text> </Text>
<Bracket rounds={bracket.winners} orders={orders} onAnnounce={onAnnounce} /> <Bracket rounds={bracket.winners} orders={orders} onStartMatch={onStartMatch} />
</div> </div>
{bracket.losers && ( {bracket.losers && (
<div> <div>
<Text fw={600} size="md" m={16}> <Text fw={600} size="md" m={16}>
Losers Bracket Losers Bracket
</Text> </Text>
<Bracket rounds={bracket.losers} orders={orders} onAnnounce={onAnnounce} /> <Bracket rounds={bracket.losers} orders={orders} onStartMatch={onStartMatch} />
</div> </div>
)} )}
</ScrollArea> </ScrollArea>

View File

@@ -5,13 +5,13 @@ import { MatchCard } from "./match-card";
interface BracketProps { interface BracketProps {
rounds: Match[][]; rounds: Match[][];
orders: Record<number, number>; orders: Record<number, number>;
onAnnounce?: (teamOne: any, teamTwo: any) => void; onStartMatch?: (match: Match) => void;
} }
export const Bracket: React.FC<BracketProps> = ({ export const Bracket: React.FC<BracketProps> = ({
rounds, rounds,
orders, orders,
onAnnounce, onStartMatch,
}) => { }) => {
return ( return (
<Flex direction="row" gap={24} justify="left" p="xl"> <Flex direction="row" gap={24} justify="left" p="xl">
@@ -32,7 +32,7 @@ export const Bracket: React.FC<BracketProps> = ({
<MatchCard <MatchCard
match={match} match={match}
orders={orders} orders={orders}
onAnnounce={onAnnounce} onStartMatch={onStartMatch}
/> />
</div> </div>
); );

View File

@@ -7,13 +7,13 @@ import { Match } from "@/features/matches/types";
interface MatchCardProps { interface MatchCardProps {
match: Match; match: Match;
orders: Record<number, number>; orders: Record<number, number>;
onAnnounce?: (teamOne: any, teamTwo: any) => void; onStartMatch?: (match: Match) => void;
} }
export const MatchCard: React.FC<MatchCardProps> = ({ export const MatchCard: React.FC<MatchCardProps> = ({
match, match,
orders, orders,
onAnnounce, onStartMatch,
}) => { }) => {
const homeSlot = useMemo( const homeSlot = useMemo(
() => ({ () => ({
@@ -35,13 +35,13 @@ export const MatchCard: React.FC<MatchCardProps> = ({
); );
const showToolbar = useMemo( const showToolbar = useMemo(
() => match.home && match.away && onAnnounce, () => match.home && match.away && onStartMatch,
[match.home, match.away] [match.home, match.away]
); );
const handleAnnounce = useCallback( const handleAnnounce = useCallback(
() => onAnnounce?.(match.home, match.away), () => onStartMatch?.(match),
[onAnnounce, match.home, match.away] [onStartMatch, match]
); );
const handleEdit = useCallback(() => { const handleEdit = useCallback(() => {

View File

@@ -11,11 +11,11 @@ const BackButton = ({ offsetY }: BackButtonProps) => {
return ( return (
<Box <Box
style={{ cursor: 'pointer', zIndex: 1000, transform: `translateY(-${offsetY}px)` }} style={{ cursor: 'pointer', zIndex: 1000 }}
onClick={() => router.history.back()} onClick={() => router.history.back()}
pos='absolute' pos='absolute'
left={{ base: 0, sm: 100, md: 200, lg: 300 }} left={16}
m={20} top={0}
> >
<ArrowLeftIcon weight='bold' size={20} /> <ArrowLeftIcon weight='bold' size={20} />
</Box> </Box>

View File

@@ -13,15 +13,11 @@ const Header = ({ withBackButton, settingsLink, collapsed, title, scrollPosition
}, [collapsed, scrollPosition.y]); }, [collapsed, scrollPosition.y]);
return ( return (
<>
{settingsLink && <SettingsButton to={settingsLink} offsetY={offsetY} />}
{withBackButton && <BackButton offsetY={offsetY} />}
<AppShell.Header id='app-header' display={collapsed ? 'none' : 'block'}> <AppShell.Header id='app-header' display={collapsed ? 'none' : 'block'}>
<Flex justify='center' align='center' h='100%' px='md'> <Flex justify='center' align='center' h='100%' px='md'>
<Title order={2}>{title}</Title> <Title order={2}>{title}</Title>
</Flex> </Flex>
</AppShell.Header> </AppShell.Header>
</>
); );
} }

View File

@@ -40,7 +40,8 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
mah='100%' mah='100%'
pb={{ base: 70, md: 0 }} pb={{ base: 70, md: 0 }}
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }} px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
style={{ transition: 'none' }} maw='100dvw'
style={{ transition: 'none', overflow: 'hidden' }}
> >
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}> <Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
<Page noPadding={!withPadding} fullWidth={fullWidth}> <Page noPadding={!withPadding} fullWidth={fullWidth}>

View File

@@ -12,11 +12,11 @@ const SettingsButton = ({ offsetY, to }: SettingButtonProps) => {
return ( return (
<Box <Box
style={{ cursor: 'pointer', zIndex: 1000, transform: `translateY(-${offsetY}px)` }} style={{ cursor: 'pointer', zIndex: 1000 }}
onClick={() => navigate({ to })} onClick={() => navigate({ to })}
pos='absolute' pos='absolute'
right={{ base: 0, sm: 100, md: 200, lg: 300 }} right={16}
m={20} top={0}
> >
<GearIcon weight='bold' size={20} /> <GearIcon weight='bold' size={20} />
</Box> </Box>

View File

@@ -31,8 +31,8 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
radius='md' radius='md'
> >
<Stack align='center' gap='xs' mb='md'> <Stack align='center' gap='xs' mb='md'>
<TrophyIcon size={150} /> <TrophyIcon size={75} />
<Title order={2} ta='center'>Welcome to FLXN</Title> <Title order={4} ta='center'>Welcome to FLXN</Title>
</Stack> </Stack>
{children} {children}
</Paper> </Paper>

View File

@@ -19,18 +19,15 @@ export const generateTournamentBracket = createServerFn()
toServerResult(async () => { toServerResult(async () => {
logger.info('Generating tournament bracket', { tournamentId, teamCount: orderedTeamIds.length }); logger.info('Generating tournament bracket', { tournamentId, teamCount: orderedTeamIds.length });
// Get tournament with teams
const tournament = await pbAdmin.getTournament(tournamentId); const tournament = await pbAdmin.getTournament(tournamentId);
if (!tournament) { if (!tournament) {
throw new Error('Tournament not found'); throw new Error('Tournament not found');
} }
// Check if tournament already has matches
if (tournament.matches && tournament.matches.length > 0) { if (tournament.matches && tournament.matches.length > 0) {
throw new Error('Tournament already has matches generated'); throw new Error('Tournament already has matches generated');
} }
// Get bracket template based on team count
const teamCount = orderedTeamIds.length; const teamCount = orderedTeamIds.length;
if (!Object.keys(brackets).includes(teamCount.toString())) { if (!Object.keys(brackets).includes(teamCount.toString())) {
throw new Error(`Bracket not available for ${teamCount} teams`); throw new Error(`Bracket not available for ${teamCount} teams`);
@@ -38,16 +35,13 @@ export const generateTournamentBracket = createServerFn()
const bracketTemplate = brackets[teamCount as keyof typeof brackets] as any; const bracketTemplate = brackets[teamCount as keyof typeof brackets] as any;
// Create seed to team mapping (index + 1 = seed)
const seedToTeamId = new Map<number, string>(); const seedToTeamId = new Map<number, string>();
orderedTeamIds.forEach((teamId, index) => { orderedTeamIds.forEach((teamId, index) => {
seedToTeamId.set(index + 1, teamId); seedToTeamId.set(index + 1, teamId);
}); });
// Convert bracket template to match records
const matchInputs: MatchInput[] = []; const matchInputs: MatchInput[] = [];
// Process winners bracket
bracketTemplate.winners.forEach((round: any[]) => { bracketTemplate.winners.forEach((round: any[]) => {
round.forEach((match: any) => { round.forEach((match: any) => {
const matchInput: MatchInput = { const matchInput: MatchInput = {
@@ -67,7 +61,6 @@ export const generateTournamentBracket = createServerFn()
tournament: tournamentId, tournament: tournamentId,
}; };
// Assign teams based on seeds
if (match.home_seed) { if (match.home_seed) {
const teamId = seedToTeamId.get(match.home_seed); const teamId = seedToTeamId.get(match.home_seed);
if (teamId) { if (teamId) {
@@ -88,7 +81,6 @@ export const generateTournamentBracket = createServerFn()
}); });
}); });
// Process losers bracket
bracketTemplate.losers.forEach((round: any[]) => { bracketTemplate.losers.forEach((round: any[]) => {
round.forEach((match: any) => { round.forEach((match: any) => {
const matchInput: MatchInput = { const matchInput: MatchInput = {
@@ -108,17 +100,12 @@ export const generateTournamentBracket = createServerFn()
tournament: tournamentId, tournament: tournamentId,
}; };
// Losers bracket matches don't start with teams
// Teams come from winners bracket losses
matchInputs.push(matchInput); matchInputs.push(matchInput);
}); });
}); });
// Create all matches
const createdMatches = await pbAdmin.createMatches(matchInputs); const createdMatches = await pbAdmin.createMatches(matchInputs);
// Update tournament to include all match IDs in the matches relation
const matchIds = createdMatches.map(match => match.id); const matchIds = createdMatches.map(match => match.id);
await pbAdmin.updateTournamentMatches(tournamentId, matchIds); await pbAdmin.updateTournamentMatches(tournamentId, matchIds);
@@ -134,3 +121,21 @@ export const generateTournamentBracket = createServerFn()
}; };
}) })
); );
export const startMatch = createServerFn()
.validator(z.string())
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult(async () => {
logger.info('Starting match', data);
let match = await pbAdmin.getMatch(data);
if (!match) {
throw new Error('Match not found');
}
match = await pbAdmin.updateMatch(data, {
start_time: new Date().toISOString()
})
}
));

View File

@@ -25,6 +25,9 @@ const Profile = ({ id }: ProfileProps) => {
label: "Teams", label: "Teams",
content: <> content: <>
<TeamList teams={tournament.teams || []} /> <TeamList teams={tournament.teams || []} />
<TeamList teams={tournament.teams || []} />
<TeamList teams={tournament.teams || []} />
<TeamList teams={tournament.teams || []} />
</> </>
} }
]; ];

View File

@@ -1,9 +1,18 @@
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import type { Match, MatchInput } from "@/features/matches/types"; import type { Match, MatchInput } from "@/features/matches/types";
import type PocketBase from "pocketbase"; import type PocketBase from "pocketbase";
import { transformMatch } from "../util/transform-types";
export function createMatchesService(pb: PocketBase) { export function createMatchesService(pb: PocketBase) {
return { return {
async getMatch(id: string): Promise<Match | null> {
logger.info("PocketBase | Getting match", id);
const result = await pb.collection("matches").getOne(id, {
expand: "tournament, home, away",
});
return transformMatch(result);
},
async createMatch(data: MatchInput): Promise<Match> { async createMatch(data: MatchInput): Promise<Match> {
logger.info("PocketBase | Creating match", data); logger.info("PocketBase | Creating match", data);
const result = await pb.collection("matches").create<Match>(data); const result = await pb.collection("matches").create<Match>(data);
@@ -11,9 +20,11 @@ export function createMatchesService(pb: PocketBase) {
}, },
async createMatches(matches: MatchInput[]): Promise<Match[]> { async createMatches(matches: MatchInput[]): Promise<Match[]> {
logger.info("PocketBase | Creating multiple matches", { count: matches.length }); logger.info("PocketBase | Creating multiple matches", {
count: matches.length,
});
const results = await Promise.all( const results = await Promise.all(
matches.map(match => pb.collection("matches").create<Match>(match)) matches.map((match) => pb.collection("matches").create<Match>(match))
); );
return results; return results;
}, },
@@ -32,7 +43,7 @@ export function createMatchesService(pb: PocketBase) {
}); });
await Promise.all( await Promise.all(
matches.map(match => pb.collection("matches").delete(match.id)) matches.map((match) => pb.collection("matches").delete(match.id))
); );
}, },
}; };