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

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

View File

@@ -1,7 +1,10 @@
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 { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
import { z } from "zod";
const searchSchema = z.object({
@@ -35,6 +38,8 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
}),
component: () => {
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({
GET: async ({ request }: { request: Request }) => {
// Helper function to get return path from state parameter
const getReturnPath = (state: string | null): string => {
if (!state) return '/';
try {
@@ -26,7 +25,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
const returnPath = getReturnPath(state);
// Check for OAuth errors
if (error) {
console.error('Spotify OAuth error:', error)
return new Response(null, {
@@ -54,7 +52,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
has_state: !!state,
})
// Exchange code for tokens
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
@@ -77,7 +74,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
redirect_uri: SPOTIFY_REDIRECT_URI,
})
// Return more detailed error info
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`)
return new Response(null, {
status: 302,
@@ -97,7 +93,6 @@ export const ServerRoute = createServerFileRoute('/api/spotify/callback').method
console.log('Decoded return path:', returnPath);
// Create response with redirect to original path
const response = new Response(null, {
status: 302,
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 cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`)
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
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
} catch (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 state = url.searchParams.get('state');
const returnPath = getReturnPath(state);