rules, bracket page

This commit is contained in:
yohlo
2025-09-18 18:17:56 -05:00
parent 285a33c488
commit 602e6e3473
15 changed files with 273 additions and 24 deletions

View File

@@ -26,6 +26,7 @@ import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$t
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId' import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview' import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index' import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id' import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id'
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id' import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo' import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
@@ -118,6 +119,12 @@ const AuthedAdminTournamentsIndexRoute =
path: '/tournaments/', path: '/tournaments/',
getParentRoute: () => AuthedAdminRoute, getParentRoute: () => AuthedAdminRoute,
} as any) } as any)
const AuthedTournamentsIdBracketRoute =
AuthedTournamentsIdBracketRouteImport.update({
id: '/tournaments/$id/bracket',
path: '/tournaments/$id/bracket',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedAdminTournamentsIdRoute = const AuthedAdminTournamentsIdRoute =
AuthedAdminTournamentsIdRouteImport.update({ AuthedAdminTournamentsIdRouteImport.update({
id: '/tournaments/$id', id: '/tournaments/$id',
@@ -206,6 +213,7 @@ export interface FileRoutesByFullPath {
'/admin/': typeof AuthedAdminIndexRoute '/admin/': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
} }
@@ -223,6 +231,7 @@ export interface FileRoutesByTo {
'/admin': typeof AuthedAdminIndexRoute '/admin': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
} }
@@ -243,6 +252,7 @@ export interface FileRoutesById {
'/_authed/admin/': typeof AuthedAdminIndexRoute '/_authed/admin/': typeof AuthedAdminIndexRoute
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute '/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute '/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute '/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
} }
@@ -263,6 +273,7 @@ export interface FileRouteTypes {
| '/admin/' | '/admin/'
| '/tournaments' | '/tournaments'
| '/admin/tournaments/$id' | '/admin/tournaments/$id'
| '/tournaments/$id/bracket'
| '/admin/tournaments' | '/admin/tournaments'
| '/admin/tournaments/run/$id' | '/admin/tournaments/run/$id'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
@@ -280,6 +291,7 @@ export interface FileRouteTypes {
| '/admin' | '/admin'
| '/tournaments' | '/tournaments'
| '/admin/tournaments/$id' | '/admin/tournaments/$id'
| '/tournaments/$id/bracket'
| '/admin/tournaments' | '/admin/tournaments'
| '/admin/tournaments/run/$id' | '/admin/tournaments/run/$id'
id: id:
@@ -299,6 +311,7 @@ export interface FileRouteTypes {
| '/_authed/admin/' | '/_authed/admin/'
| '/_authed/tournaments/' | '/_authed/tournaments/'
| '/_authed/admin/tournaments/$id' | '/_authed/admin/tournaments/$id'
| '/_authed/tournaments/$id/bracket'
| '/_authed/admin/tournaments/' | '/_authed/admin/tournaments/'
| '/_authed/admin/tournaments/run/$id' | '/_authed/admin/tournaments/run/$id'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@@ -512,6 +525,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport
parentRoute: typeof AuthedAdminRoute parentRoute: typeof AuthedAdminRoute
} }
'/_authed/tournaments/$id/bracket': {
id: '/_authed/tournaments/$id/bracket'
path: '/tournaments/$id/bracket'
fullPath: '/tournaments/$id/bracket'
preLoaderRoute: typeof AuthedTournamentsIdBracketRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/admin/tournaments/$id': { '/_authed/admin/tournaments/$id': {
id: '/_authed/admin/tournaments/$id' id: '/_authed/admin/tournaments/$id'
path: '/tournaments/$id' path: '/tournaments/$id'
@@ -639,6 +659,7 @@ interface AuthedRouteChildren {
AuthedTeamsTeamIdRoute: typeof AuthedTeamsTeamIdRoute AuthedTeamsTeamIdRoute: typeof AuthedTeamsTeamIdRoute
AuthedTournamentsTournamentIdRoute: typeof AuthedTournamentsTournamentIdRoute AuthedTournamentsTournamentIdRoute: typeof AuthedTournamentsTournamentIdRoute
AuthedTournamentsIndexRoute: typeof AuthedTournamentsIndexRoute AuthedTournamentsIndexRoute: typeof AuthedTournamentsIndexRoute
AuthedTournamentsIdBracketRoute: typeof AuthedTournamentsIdBracketRoute
} }
const AuthedRouteChildren: AuthedRouteChildren = { const AuthedRouteChildren: AuthedRouteChildren = {
@@ -650,6 +671,7 @@ const AuthedRouteChildren: AuthedRouteChildren = {
AuthedTeamsTeamIdRoute: AuthedTeamsTeamIdRoute, AuthedTeamsTeamIdRoute: AuthedTeamsTeamIdRoute,
AuthedTournamentsTournamentIdRoute: AuthedTournamentsTournamentIdRoute, AuthedTournamentsTournamentIdRoute: AuthedTournamentsTournamentIdRoute,
AuthedTournamentsIndexRoute: AuthedTournamentsIndexRoute, AuthedTournamentsIndexRoute: AuthedTournamentsIndexRoute,
AuthedTournamentsIdBracketRoute: AuthedTournamentsIdBracketRoute,
} }
const AuthedRouteWithChildren = const AuthedRouteWithChildren =

View File

@@ -22,7 +22,7 @@ export const Route = createFileRoute("/_authed/")({
function Home() { function Home() {
const { data: tournament } = useCurrentTournament(); const { data: tournament } = useCurrentTournament();
if (!tournament.matches || tournament.matches.length === 0) { if (!tournament.matches || tournament.matches.length !== 0) {
return <UpcomingTournament tournament={tournament} />; return <UpcomingTournament tournament={tournament} />;
} }

View File

@@ -1,7 +1,10 @@
import Profile from "@/features/players/components/profile"; import Profile from "@/features/players/components/profile";
import HeaderSkeleton from "@/features/players/components/profile/header-skeleton";
import ProfileSkeleton from "@/features/players/components/profile/skeleton";
import { playerKeys, playerQueries } from "@/features/players/queries"; import { playerKeys, playerQueries } from "@/features/players/queries";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
import { z } from "zod"; import { z } from "zod";
const searchSchema = z.object({ const searchSchema = z.object({
@@ -35,6 +38,8 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
}), }),
component: () => { component: () => {
const { playerId } = Route.useParams(); const { playerId } = Route.useParams();
return <Profile id={playerId} />; return <Suspense fallback={<ProfileSkeleton />}>
<Profile id={playerId} />
</Suspense>;
}, },
}); });

View File

@@ -0,0 +1,82 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import {
tournamentQueries,
useTournament,
} from "@/features/tournaments/queries";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import SeedTournament from "@/features/tournaments/components/seed-tournament";
import { Container } from "@mantine/core";
import { useMemo } from "react";
import { BracketData } from "@/features/bracket/types";
import { Match } from "@/features/matches/types";
import BracketView from "@/features/bracket/components/bracket-view";
import { SpotifyControlsBar } from "@/features/spotify/components";
export const Route = createFileRoute("/_authed/tournaments/$id/bracket")({
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
const tournament = await ensureServerQueryData(
queryClient,
tournamentQueries.details(params.id)
);
if (!tournament) throw redirect({ to: "/admin/tournaments" });
return {
tournament,
};
},
loader: ({ context }) => ({
fullWidth: true,
withPadding: false,
showSpotifyPanel: true,
header: {
withBackButton: true,
title: `${context.tournament.name}`,
},
}),
component: RouteComponent,
});
function RouteComponent() {
const { id } = Route.useParams();
const { data: tournament } = useTournament(id);
const bracket: BracketData = useMemo(() => {
if (!tournament.matches || tournament.matches.length === 0) {
return { winners: [], losers: [] };
}
const winnersMap = new Map<number, Match[]>();
const losersMap = new Map<number, Match[]>();
tournament.matches
.sort((a, b) => a.lid - b.lid)
.forEach((match) => {
if (!match.is_losers_bracket) {
if (!winnersMap.has(match.round)) {
winnersMap.set(match.round, []);
}
winnersMap.get(match.round)!.push(match);
} else {
if (!losersMap.has(match.round)) {
losersMap.set(match.round, []);
}
losersMap.get(match.round)!.push(match);
}
});
const winners = Array.from(winnersMap.entries())
.sort(([a], [b]) => a - b)
.map(([, matches]) => matches);
const losers = Array.from(losersMap.entries())
.sort(([a], [b]) => a - b)
.map(([, matches]) => matches);
return { winners, losers };
}, [tournament.matches]);
return (
<Container size="md" px={0}>
<BracketView bracket={bracket} />
</Container>
);
}

View File

@@ -7,7 +7,6 @@ const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!
export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({ export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({
GET: async ({ request }: { request: Request }) => { GET: async ({ request }: { request: Request }) => {
// Helper function to get return path from state parameter
const getReturnPath = (state: string | null): string => { const getReturnPath = (state: string | null): string => {
if (!state) return '/'; if (!state) return '/';
try { try {
@@ -26,7 +25,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
const returnPath = getReturnPath(state); const returnPath = getReturnPath(state);
// Check for OAuth errors
if (error) { if (error) {
console.error('Spotify OAuth error:', error) console.error('Spotify OAuth error:', error)
return new Response(null, { return new Response(null, {
@@ -54,7 +52,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
has_state: !!state, has_state: !!state,
}) })
// Exchange code for tokens
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', { const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -77,7 +74,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
redirect_uri: SPOTIFY_REDIRECT_URI, redirect_uri: SPOTIFY_REDIRECT_URI,
}) })
// Return more detailed error info
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`) const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`)
return new Response(null, { return new Response(null, {
status: 302, status: 302,
@@ -97,7 +93,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
console.log('Decoded return path:', returnPath); console.log('Decoded return path:', returnPath);
// Create response with redirect to original path
const response = new Response(null, { const response = new Response(null, {
status: 302, status: 302,
headers: { headers: {
@@ -105,14 +100,12 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
}, },
}) })
// Set secure cookies for tokens
const isSecure = process.env.NODE_ENV === 'production' const isSecure = process.env.NODE_ENV === 'production'
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}` const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`) response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`)
if (tokens.refresh_token) { if (tokens.refresh_token) {
// Refresh token doesn't expire, set longer max age
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days
response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`) response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`)
} }
@@ -120,7 +113,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
return response return response
} catch (error) { } catch (error) {
console.error('Spotify callback error:', error) console.error('Spotify callback error:', error)
// Try to get return path from query params if available, otherwise use default
const url = new URL(request.url); const url = new URL(request.url);
const state = url.searchParams.get('state'); const state = url.searchParams.get('state');
const returnPath = getReturnPath(state); const returnPath = getReturnPath(state);

View File

@@ -34,7 +34,7 @@ export function RichTextEditor({
</MantineRichTextEditor.ControlsGroup> </MantineRichTextEditor.ControlsGroup>
</MantineRichTextEditor.Toolbar> </MantineRichTextEditor.Toolbar>
<MantineRichTextEditor.Content /> <MantineRichTextEditor.Content h="45vh" />
</MantineRichTextEditor> </MantineRichTextEditor>
); );
} }

View File

@@ -36,7 +36,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
color="red" color="red"
processing processing
position="top-end" position="top-end"
offset={2} offset={24}
> >
<Box style={{ position: "relative" }}> <Box style={{ position: "relative" }}>
<Paper <Paper

View File

@@ -0,0 +1,14 @@
import { Flex, Skeleton } from "@mantine/core";
const HeaderSkeleton = () => {
return (
<Flex h="10vh" px='xl' w='100%' align='self-end' gap='md'>
<Skeleton opacity={0} height={100} width={100} radius="50%" />
<Flex align='center' justify='center' gap={4} pb={20} w='100%'>
<Skeleton height={24} width={200} />
</Flex>
</Flex>
);
};
export default HeaderSkeleton;

View File

@@ -33,7 +33,7 @@ const Header = ({ player }: HeaderProps) => {
return ( return (
<> <>
<Flex px='xl' w='100%' align='self-end' gap='md'> <Flex h="10vh" px='xl' w='100%' align='self-end' gap='md'>
<Avatar name={name} size={100} /> <Avatar name={name} size={100} />
<Flex align='center' justify='center' gap={4} pb={20} w='100%'> <Flex align='center' justify='center' gap={4} pb={20} w='100%'>
<Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title> <Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title>

View File

@@ -0,0 +1,42 @@
import { Box, Flex, Loader } from "@mantine/core";
import Header from "./header";
import SwipeableTabs from "@/components/swipeable-tabs";
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
import TeamList from "@/features/teams/components/team-list";
import StatsOverview from "@/shared/components/stats-overview";
import MatchList from "@/features/matches/components/match-list";
import HeaderSkeleton from "./header-skeleton";
const SkeletonLoader = () => (
<Flex h="30vh" w="100%" align="center" justify="center">
<Loader />
</Flex>
)
const ProfileSkeleton = () => {
const tabs = [
{
label: "Overview",
content: <SkeletonLoader />,
},
{
label: "Matches",
content: <SkeletonLoader />,
},
{
label: "Teams",
content: <SkeletonLoader />,
},
];
return (
<>
<HeaderSkeleton />
<Box mt="lg">
<SwipeableTabs tabs={tabs} />
</Box>
</>
);
};
export default ProfileSkeleton;

View File

@@ -87,9 +87,9 @@ const TeamForm = ({
const form = useForm(config); const form = useForm(config);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate: createTournament, isPending: createPending } = const { mutate: createTeam, isPending: createPending } =
useCreateTeam(); useCreateTeam();
const { mutate: updateTournament, isPending: updatePending } = useUpdateTeam( const { mutate: updateTeam, isPending: updatePending } = useUpdateTeam(
teamId! teamId!
); );
@@ -99,7 +99,7 @@ const TeamForm = ({
async (values: TeamInput) => { async (values: TeamInput) => {
const { logo, ...teamData } = values; const { logo, ...teamData } = values;
const mutation = isEditMode ? updateTournament : createTournament; const mutation = isEditMode ? updateTeam : createTeam;
const errorMessage = isEditMode const errorMessage = isEditMode
? "Failed to update team" ? "Failed to update team"
: "Failed to create team"; : "Failed to create team";
@@ -156,7 +156,7 @@ const TeamForm = ({
}, },
}); });
}, },
[isEditMode, createTournament, updateTournament, queryClient] [isEditMode, createTeam, updateTeam, queryClient]
); );
return ( return (

View File

@@ -0,0 +1,44 @@
import {
Stack,
Button
} from "@mantine/core";
import { useState, useCallback } from "react";
import { useTournament } from "../queries";
import useUpdateTournament from "../hooks/use-update-tournament";
import { RichTextEditor } from "@/components/rich-text-editor";
interface EditRulesProps {
tournamentId: string;
onClose?: () => void
}
const EditRules = ({ tournamentId, onClose }: EditRulesProps) => {
const [search, setSearch] = useState("");
const { data: tournament, isLoading: tournamentLoading } =
useTournament(tournamentId);
const { mutate: updateTournament, isPending: updatePending } = useUpdateTournament(tournamentId);
const [value, setValue] = useState(tournament.rules);
const handleSubmit = useCallback(
(rules?: string) => {
updateTournament({ rules }, {
onSuccess: () => {
onClose?.();
}
});
},
[updateTournament, tournamentId]
);
return (
<Stack gap="xs" w="100%">
<RichTextEditor value={value || ""} onChange={setValue} />
<Button onClick={() => handleSubmit(value)}>Submit</Button>
<Button variant="subtle" color="red" onClick={onClose}>Cancel</Button>
</Stack>
);
};
export default EditRules;

View File

@@ -14,6 +14,7 @@ import EditEnrolledTeams from "./edit-enrolled-teams";
import ListLink from "@/components/list-link"; import ListLink from "@/components/list-link";
import { RichTextEditor } from "@/components/rich-text-editor"; import { RichTextEditor } from "@/components/rich-text-editor";
import React from "react"; import React from "react";
import EditRules from "./edit-rules";
interface ManageTournamentProps { interface ManageTournamentProps {
tournamentId: string; tournamentId: string;
@@ -90,9 +91,7 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
opened={editRulesOpened} opened={editRulesOpened}
onChange={closeEditRules} onChange={closeEditRules}
> >
<RichTextEditor value={v} onChange={setV} /> <EditRules tournamentId={tournamentId} onClose={closeEditRules} />
{v}
</Sheet> </Sheet>
<Sheet <Sheet

View File

@@ -5,7 +5,7 @@ import { Box, Button, Card, Divider, Group, Stack, Text } from "@mantine/core";
import Countdown from "@/components/countdown"; import Countdown from "@/components/countdown";
import ListLink from "@/components/list-link"; import ListLink from "@/components/list-link";
import ListButton from "@/components/list-button"; import ListButton from "@/components/list-button";
import { UsersIcon, ListIcon } from "@phosphor-icons/react"; import { TreeStructureIcon, UsersIcon } from "@phosphor-icons/react";
import EnrollTeam from "./enroll-team"; import EnrollTeam from "./enroll-team";
import EnrollFreeAgent from "./enroll-free-agent"; import EnrollFreeAgent from "./enroll-free-agent";
import TeamListButton from "./team-list-button"; import TeamListButton from "./team-list-button";
@@ -16,6 +16,7 @@ import UpdateTeam from "./update-team";
import UnenrollTeam from "./unenroll-team"; import UnenrollTeam from "./unenroll-team";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "../../queries"; import { tournamentKeys } from "../../queries";
import RulesListButton from "./rules-list-button";
const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
tournament, tournament,
@@ -42,8 +43,6 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
queryClient.invalidateQueries({ queryKey: tournamentKeys.current }) queryClient.invalidateQueries({ queryKey: tournamentKeys.current })
} }
console.log(userTeam)
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<Header tournament={tournament} /> <Header tournament={tournament} />
@@ -102,7 +101,12 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
Icon={UsersIcon} Icon={UsersIcon}
/> />
)} )}
<ListButton label="View Rules" Icon={ListIcon} onClick={() => {}} /> <ListLink
label={`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon}
/>
<RulesListButton tournamentId={tournament.id} />
<TeamListButton teams={tournament.teams || []} /> <TeamListButton teams={tournament.teams || []} />
</Box> </Box>
</Stack> </Stack>

View File

@@ -0,0 +1,45 @@
import ListButton from "@/components/list-button"
import Sheet from "@/components/sheet/sheet"
import { useSheet } from "@/hooks/use-sheet"
import { ListIcon } from "@phosphor-icons/react"
import { useTournament } from "../../queries"
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { RichTextEditor } from '@mantine/tiptap';
import { Button, Stack } from "@mantine/core"
interface RulesListButtonProps {
tournamentId: string;
}
const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
const { data: tournament } = useTournament(tournamentId);
const { open, isOpen, toggle } = useSheet();
const editor = useEditor({
extensions: [StarterKit],
content: tournament?.rules || '',
editable: false,
});
return (
<>
<ListButton
label={`View Rules`}
Icon={ListIcon}
onClick={open}
/>
<Sheet title="Tournament Rules" opened={isOpen} onChange={toggle}>
<Stack gap="xs">
<RichTextEditor editor={editor}>
<RichTextEditor.Content />
</RichTextEditor>
<Button variant="subtle" c="red" onClick={toggle}>Close</Button>
</Stack>
</Sheet>
</>
)
}
export default RulesListButton;