diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts
index 78b6498..1c86190 100644
--- a/src/app/routeTree.gen.ts
+++ b/src/app/routeTree.gen.ts
@@ -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 AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
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 AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
@@ -118,6 +119,12 @@ const AuthedAdminTournamentsIndexRoute =
path: '/tournaments/',
getParentRoute: () => AuthedAdminRoute,
} as any)
+const AuthedTournamentsIdBracketRoute =
+ AuthedTournamentsIdBracketRouteImport.update({
+ id: '/tournaments/$id/bracket',
+ path: '/tournaments/$id/bracket',
+ getParentRoute: () => AuthedRoute,
+ } as any)
const AuthedAdminTournamentsIdRoute =
AuthedAdminTournamentsIdRouteImport.update({
id: '/tournaments/$id',
@@ -206,6 +213,7 @@ export interface FileRoutesByFullPath {
'/admin/': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
+ '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
}
@@ -223,6 +231,7 @@ export interface FileRoutesByTo {
'/admin': typeof AuthedAdminIndexRoute
'/tournaments': typeof AuthedTournamentsIndexRoute
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
+ '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
}
@@ -243,6 +252,7 @@ export interface FileRoutesById {
'/_authed/admin/': typeof AuthedAdminIndexRoute
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
+ '/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
}
@@ -263,6 +273,7 @@ export interface FileRouteTypes {
| '/admin/'
| '/tournaments'
| '/admin/tournaments/$id'
+ | '/tournaments/$id/bracket'
| '/admin/tournaments'
| '/admin/tournaments/run/$id'
fileRoutesByTo: FileRoutesByTo
@@ -280,6 +291,7 @@ export interface FileRouteTypes {
| '/admin'
| '/tournaments'
| '/admin/tournaments/$id'
+ | '/tournaments/$id/bracket'
| '/admin/tournaments'
| '/admin/tournaments/run/$id'
id:
@@ -299,6 +311,7 @@ export interface FileRouteTypes {
| '/_authed/admin/'
| '/_authed/tournaments/'
| '/_authed/admin/tournaments/$id'
+ | '/_authed/tournaments/$id/bracket'
| '/_authed/admin/tournaments/'
| '/_authed/admin/tournaments/run/$id'
fileRoutesById: FileRoutesById
@@ -512,6 +525,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport
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': {
id: '/_authed/admin/tournaments/$id'
path: '/tournaments/$id'
@@ -639,6 +659,7 @@ interface AuthedRouteChildren {
AuthedTeamsTeamIdRoute: typeof AuthedTeamsTeamIdRoute
AuthedTournamentsTournamentIdRoute: typeof AuthedTournamentsTournamentIdRoute
AuthedTournamentsIndexRoute: typeof AuthedTournamentsIndexRoute
+ AuthedTournamentsIdBracketRoute: typeof AuthedTournamentsIdBracketRoute
}
const AuthedRouteChildren: AuthedRouteChildren = {
@@ -650,6 +671,7 @@ const AuthedRouteChildren: AuthedRouteChildren = {
AuthedTeamsTeamIdRoute: AuthedTeamsTeamIdRoute,
AuthedTournamentsTournamentIdRoute: AuthedTournamentsTournamentIdRoute,
AuthedTournamentsIndexRoute: AuthedTournamentsIndexRoute,
+ AuthedTournamentsIdBracketRoute: AuthedTournamentsIdBracketRoute,
}
const AuthedRouteWithChildren =
diff --git a/src/app/routes/_authed/index.tsx b/src/app/routes/_authed/index.tsx
index 3cdf42f..853cc4e 100644
--- a/src/app/routes/_authed/index.tsx
+++ b/src/app/routes/_authed/index.tsx
@@ -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 ;
}
diff --git a/src/app/routes/_authed/profile.$playerId.tsx b/src/app/routes/_authed/profile.$playerId.tsx
index f108e88..d88e56f 100644
--- a/src/app/routes/_authed/profile.$playerId.tsx
+++ b/src/app/routes/_authed/profile.$playerId.tsx
@@ -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 ;
+ return }>
+
+ ;
},
});
diff --git a/src/app/routes/_authed/tournaments/$id.bracket.tsx b/src/app/routes/_authed/tournaments/$id.bracket.tsx
new file mode 100644
index 0000000..24a810d
--- /dev/null
+++ b/src/app/routes/_authed/tournaments/$id.bracket.tsx
@@ -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();
+ const losersMap = new Map();
+
+ 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 (
+
+
+
+ );
+}
diff --git a/src/app/routes/api/spotify/callback.ts b/src/app/routes/api/spotify/callback.ts
index f1b887b..0fbcaae 100644
--- a/src/app/routes/api/spotify/callback.ts
+++ b/src/app/routes/api/spotify/callback.ts
@@ -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);
diff --git a/src/components/rich-text-editor.tsx b/src/components/rich-text-editor.tsx
index 74fd872..a08103c 100644
--- a/src/components/rich-text-editor.tsx
+++ b/src/components/rich-text-editor.tsx
@@ -34,7 +34,7 @@ export function RichTextEditor({
-
+
);
}
diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx
index 80e8d5c..6f1fa3e 100644
--- a/src/features/matches/components/match-card.tsx
+++ b/src/features/matches/components/match-card.tsx
@@ -36,7 +36,7 @@ const MatchCard = ({ match }: MatchCardProps) => {
color="red"
processing
position="top-end"
- offset={2}
+ offset={24}
>
{
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default HeaderSkeleton;
\ No newline at end of file
diff --git a/src/features/players/components/profile/header.tsx b/src/features/players/components/profile/header.tsx
index ad82b04..c51cc90 100644
--- a/src/features/players/components/profile/header.tsx
+++ b/src/features/players/components/profile/header.tsx
@@ -33,7 +33,7 @@ const Header = ({ player }: HeaderProps) => {
return (
<>
-
+
{name}
diff --git a/src/features/players/components/profile/skeleton.tsx b/src/features/players/components/profile/skeleton.tsx
new file mode 100644
index 0000000..0373ca4
--- /dev/null
+++ b/src/features/players/components/profile/skeleton.tsx
@@ -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 = () => (
+
+
+
+)
+
+const ProfileSkeleton = () => {
+ const tabs = [
+ {
+ label: "Overview",
+ content: ,
+ },
+ {
+ label: "Matches",
+ content: ,
+ },
+ {
+ label: "Teams",
+ content: ,
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default ProfileSkeleton;
diff --git a/src/features/teams/components/team-form/index.tsx b/src/features/teams/components/team-form/index.tsx
index acad779..6bd189a 100644
--- a/src/features/teams/components/team-form/index.tsx
+++ b/src/features/teams/components/team-form/index.tsx
@@ -87,9 +87,9 @@ const TeamForm = ({
const form = useForm(config);
const queryClient = useQueryClient();
- const { mutate: createTournament, isPending: createPending } =
+ const { mutate: createTeam, isPending: createPending } =
useCreateTeam();
- const { mutate: updateTournament, isPending: updatePending } = useUpdateTeam(
+ const { mutate: updateTeam, isPending: updatePending } = useUpdateTeam(
teamId!
);
@@ -99,7 +99,7 @@ const TeamForm = ({
async (values: TeamInput) => {
const { logo, ...teamData } = values;
- const mutation = isEditMode ? updateTournament : createTournament;
+ const mutation = isEditMode ? updateTeam : createTeam;
const errorMessage = isEditMode
? "Failed to update team"
: "Failed to create team";
@@ -156,7 +156,7 @@ const TeamForm = ({
},
});
},
- [isEditMode, createTournament, updateTournament, queryClient]
+ [isEditMode, createTeam, updateTeam, queryClient]
);
return (
diff --git a/src/features/tournaments/components/edit-rules.tsx b/src/features/tournaments/components/edit-rules.tsx
new file mode 100644
index 0000000..51816d0
--- /dev/null
+++ b/src/features/tournaments/components/edit-rules.tsx
@@ -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 (
+
+
+
+
+
+ );
+};
+
+export default EditRules;
diff --git a/src/features/tournaments/components/manage-tournament.tsx b/src/features/tournaments/components/manage-tournament.tsx
index 783b0ad..584e4da 100644
--- a/src/features/tournaments/components/manage-tournament.tsx
+++ b/src/features/tournaments/components/manage-tournament.tsx
@@ -14,6 +14,7 @@ import EditEnrolledTeams from "./edit-enrolled-teams";
import ListLink from "@/components/list-link";
import { RichTextEditor } from "@/components/rich-text-editor";
import React from "react";
+import EditRules from "./edit-rules";
interface ManageTournamentProps {
tournamentId: string;
@@ -90,9 +91,7 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
opened={editRulesOpened}
onChange={closeEditRules}
>
-
-
- {v}
+
= ({
tournament,
@@ -42,8 +43,6 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
queryClient.invalidateQueries({ queryKey: tournamentKeys.current })
}
- console.log(userTeam)
-
return (
@@ -102,7 +101,12 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
Icon={UsersIcon}
/>
)}
- {}} />
+
+
diff --git a/src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx b/src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx
new file mode 100644
index 0000000..23791c6
--- /dev/null
+++ b/src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx
@@ -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 = ({ tournamentId }) => {
+ const { data: tournament } = useTournament(tournamentId);
+ const { open, isOpen, toggle } = useSheet();
+
+ const editor = useEditor({
+ extensions: [StarterKit],
+ content: tournament?.rules || '',
+ editable: false,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default RulesListButton;
\ No newline at end of file