rules, bracket page
This commit is contained in:
@@ -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 =
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
82
src/app/routes/_authed/tournaments/$id.bracket.tsx
Normal file
82
src/app/routes/_authed/tournaments/$id.bracket.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function RichTextEditor({
|
|||||||
</MantineRichTextEditor.ControlsGroup>
|
</MantineRichTextEditor.ControlsGroup>
|
||||||
</MantineRichTextEditor.Toolbar>
|
</MantineRichTextEditor.Toolbar>
|
||||||
|
|
||||||
<MantineRichTextEditor.Content />
|
<MantineRichTextEditor.Content h="45vh" />
|
||||||
</MantineRichTextEditor>
|
</MantineRichTextEditor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
14
src/features/players/components/profile/header-skeleton.tsx
Normal file
14
src/features/players/components/profile/header-skeleton.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
42
src/features/players/components/profile/skeleton.tsx
Normal file
42
src/features/players/components/profile/skeleton.tsx
Normal 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;
|
||||||
@@ -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 (
|
||||||
|
|||||||
44
src/features/tournaments/components/edit-rules.tsx
Normal file
44
src/features/tournaments/components/edit-rules.tsx
Normal 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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user