various improvements, edit tournament, etc
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,4 +18,5 @@ yarn.lock
|
|||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/scripts/
|
/scripts/
|
||||||
/pb_data/
|
/pb_data/
|
||||||
|
/.tanstack/
|
||||||
@@ -23,6 +23,7 @@ import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_aut
|
|||||||
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
||||||
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 AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id'
|
||||||
import { ServerRoute as ApiTestServerRouteImport } from './routes/api/test'
|
import { ServerRoute as ApiTestServerRouteImport } from './routes/api/test'
|
||||||
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||||
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||||
@@ -91,6 +92,12 @@ const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
|
|||||||
path: '/preview',
|
path: '/preview',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedAdminTournamentsIdRoute =
|
||||||
|
AuthedAdminTournamentsIdRouteImport.update({
|
||||||
|
id: '/tournaments/$id',
|
||||||
|
path: '/tournaments/$id',
|
||||||
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
|
} as any)
|
||||||
const ApiTestServerRoute = ApiTestServerRouteImport.update({
|
const ApiTestServerRoute = ApiTestServerRouteImport.update({
|
||||||
id: '/api/test',
|
id: '/api/test',
|
||||||
path: '/api/test',
|
path: '/api/test',
|
||||||
@@ -131,6 +138,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/admin/': typeof AuthedAdminIndexRoute
|
'/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
@@ -143,6 +151,7 @@ export interface FileRoutesByTo {
|
|||||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/admin': typeof AuthedAdminIndexRoute
|
'/admin': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -158,6 +167,7 @@ export interface FileRoutesById {
|
|||||||
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
|
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -173,6 +183,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments/$tournamentId'
|
| '/tournaments/$tournamentId'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
|
| '/admin/tournaments/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/login'
|
| '/login'
|
||||||
@@ -185,6 +196,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments/$tournamentId'
|
| '/tournaments/$tournamentId'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
|
| '/admin/tournaments/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/_authed'
|
| '/_authed'
|
||||||
@@ -199,6 +211,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/tournaments/$tournamentId'
|
| '/_authed/tournaments/$tournamentId'
|
||||||
| '/_authed/admin/'
|
| '/_authed/admin/'
|
||||||
| '/_authed/tournaments/'
|
| '/_authed/tournaments/'
|
||||||
|
| '/_authed/admin/tournaments/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -346,6 +359,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
|
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
|
'/_authed/admin/tournaments/$id': {
|
||||||
|
id: '/_authed/admin/tournaments/$id'
|
||||||
|
path: '/tournaments/$id'
|
||||||
|
fullPath: '/admin/tournaments/$id'
|
||||||
|
preLoaderRoute: typeof AuthedAdminTournamentsIdRouteImport
|
||||||
|
parentRoute: typeof AuthedAdminRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declare module '@tanstack/react-start/server' {
|
declare module '@tanstack/react-start/server' {
|
||||||
@@ -391,11 +411,13 @@ declare module '@tanstack/react-start/server' {
|
|||||||
interface AuthedAdminRouteChildren {
|
interface AuthedAdminRouteChildren {
|
||||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||||
|
AuthedAdminTournamentsIdRoute: typeof AuthedAdminTournamentsIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
||||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||||
|
AuthedAdminTournamentsIdRoute: AuthedAdminTournamentsIdRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'
|
|||||||
import { type QueryClient } from '@tanstack/react-query'
|
import { type QueryClient } from '@tanstack/react-query'
|
||||||
import { ensureSuperTokensFrontend } from '@/lib/supertokens/client'
|
import { ensureSuperTokensFrontend } from '@/lib/supertokens/client'
|
||||||
import { AuthContextType, authQueryConfig } from '@/contexts/auth-context'
|
import { AuthContextType, authQueryConfig } from '@/contexts/auth-context'
|
||||||
import Providers from '@/components/providers'
|
import Providers from '@/features/core/components/providers'
|
||||||
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
|
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
|
||||||
import { HeaderConfig } from '@/features/core/types/header-config';
|
import { HeaderConfig } from '@/features/core/types/header-config';
|
||||||
|
|
||||||
|
|||||||
33
src/app/routes/_authed/admin/tournaments/$id.tsx
Normal file
33
src/app/routes/_authed/admin/tournaments/$id.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useAuth } from '@/contexts/auth-context'
|
||||||
|
import EditTournament from '@/features/admin/components/edit-tournament'
|
||||||
|
import Page from '@/components/page'
|
||||||
|
import { Loader } from '@mantine/core'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/_authed/admin/tournaments/$id')({
|
||||||
|
beforeLoad: async ({ context, params }) => {
|
||||||
|
const { queryClient } = context;
|
||||||
|
await queryClient.ensureQueryData(tournamentQueries.details(params.id))
|
||||||
|
},
|
||||||
|
loader: () => ({
|
||||||
|
header: {
|
||||||
|
withBackButton: true,
|
||||||
|
title: 'Edit Tournament',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const { data: tournament } = useQuery(tournamentQueries.details(id))
|
||||||
|
if (!tournament) throw new Error("Tournament not found.")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<EditTournament tournament={tournament} />
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,11 +2,14 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
import { tournamentQueries } from '@/features/tournaments/queries';
|
import { tournamentQueries } from '@/features/tournaments/queries';
|
||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Box, Button } from '@mantine/core';
|
import { Box, Group, Title } from '@mantine/core';
|
||||||
import { useSheet } from '@/hooks/use-sheet';
|
import { useSheet } from '@/hooks/use-sheet';
|
||||||
import Sheet from '@/components/sheet/sheet';
|
import Sheet from '@/components/sheet/sheet';
|
||||||
import { Tournament } from '@/features/tournaments/types';
|
import { Tournament } from '@/features/tournaments/types';
|
||||||
import TeamList from '@/features/teams/components/team-list';
|
import TeamList from '@/features/teams/components/team-list';
|
||||||
|
import Button from '@/components/button';
|
||||||
|
import Avatar from '@/components/avatar';
|
||||||
|
import Profile from '@/features/tournaments/components/profile';
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||||
beforeLoad: async ({ context, params }) => {
|
beforeLoad: async ({ context, params }) => {
|
||||||
@@ -28,28 +31,8 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId));
|
const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId));
|
||||||
|
|
||||||
const sheet = useSheet()
|
|
||||||
|
|
||||||
return <Page noPadding>
|
return <Page noPadding>
|
||||||
<Box mt='xl' p='md'>
|
<Profile tournament={tournament!} />
|
||||||
|
|
||||||
<h3 style={{ marginTop: 0 }}>
|
|
||||||
{tournament?.name}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<Button onClick={() => sheet.open()}>
|
|
||||||
View Teams
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Sheet
|
|
||||||
{...sheet.props}
|
|
||||||
title='Teams'
|
|
||||||
>
|
|
||||||
<TeamDrawer tournament={tournament!} />
|
|
||||||
</Sheet>
|
|
||||||
</Page>
|
</Page>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
import { Button, Stack } from '@mantine/core'
|
import { Stack } from '@mantine/core'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { TournamentCard } from '@/features/tournaments/components/tournament-card'
|
import { TournamentCard } from '@/features/tournaments/components/tournament-card'
|
||||||
import { tournamentQueries } from '@/features/tournaments/queries'
|
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||||
@@ -9,6 +9,7 @@ import { useSheet } from '@/hooks/use-sheet'
|
|||||||
import Sheet from '@/components/sheet/sheet'
|
import Sheet from '@/components/sheet/sheet'
|
||||||
import CreateTournament from '@/features/admin/components/create-tournament'
|
import CreateTournament from '@/features/admin/components/create-tournament'
|
||||||
import { PlusIcon } from '@phosphor-icons/react'
|
import { PlusIcon } from '@phosphor-icons/react'
|
||||||
|
import Button from '@/components/button'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authed/tournaments/')({
|
export const Route = createFileRoute('/_authed/tournaments/')({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
useMatch,
|
useMatch,
|
||||||
useRouter,
|
useRouter,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
|
redirect,
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
import type { ErrorComponentProps } from '@tanstack/react-router'
|
import type { ErrorComponentProps } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button as MantineButton,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -23,6 +24,7 @@ import { useEffect } from 'react'
|
|||||||
import toast from '@/lib/sonner'
|
import toast from '@/lib/sonner'
|
||||||
import { logger } from '@/lib/logger'
|
import { logger } from '@/lib/logger'
|
||||||
import { ExclamationMarkIcon, XCircleIcon } from '@phosphor-icons/react'
|
import { ExclamationMarkIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||||
|
import Button from './button'
|
||||||
|
|
||||||
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -41,8 +43,8 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|||||||
|
|
||||||
if (errorMessage.toLowerCase().includes('unauthenticated')) {
|
if (errorMessage.toLowerCase().includes('unauthenticated')) {
|
||||||
toast.error('You\'ve been logged out')
|
toast.error('You\'ve been logged out')
|
||||||
navigate({ to: '/login' })
|
router.history.push('/login')
|
||||||
return
|
throw redirect({ to: '/login' })
|
||||||
}
|
}
|
||||||
}, [error, errorMessage, navigate])
|
}, [error, errorMessage, navigate])
|
||||||
|
|
||||||
@@ -73,13 +75,13 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|||||||
>
|
>
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<MantineButton
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/"
|
to="/"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
</Button>
|
</MantineButton>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -137,13 +139,13 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|||||||
Try Again
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
{isRoot ? (
|
{isRoot ? (
|
||||||
<Button
|
<MantineButton
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/"
|
to="/"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
</Button>
|
</MantineButton>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ interface AvatarProps extends Omit<MantineAvatarProps, 'radius' | 'color' | 'siz
|
|||||||
name: string;
|
name: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
radius?: string | number;
|
radius?: string | number;
|
||||||
|
withBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar = ({ name, size = 35, radius = '100%', ...props }: AvatarProps) => {
|
const Avatar = ({ name, size = 35, radius = '100%', withBorder = true, ...props }: AvatarProps) => {
|
||||||
return <Paper p={size / 20} radius={radius} withBorder>
|
return <Paper p={size / 20} radius={radius} withBorder={withBorder}>
|
||||||
<MantineAvatar alt={name} key={name} name={name} color='initials' size={size} radius={radius} {...props} />
|
<MantineAvatar alt={name} key={name} name={name} color='initials' size={size} radius={radius} w='fit-content' styles={{ image: {
|
||||||
|
objectFit: 'contain'
|
||||||
|
} }} {...props} />
|
||||||
</Paper>
|
</Paper>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/components/button.tsx
Normal file
11
src/components/button.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Button as MantineButton, ButtonProps as MantineButtonProps } from '@mantine/core';
|
||||||
|
import { forwardRef, ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
|
type ButtonProps = MantineButtonProps & ComponentPropsWithoutRef<'button'>;
|
||||||
|
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
|
return <MantineButton fullWidth ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
export default Button;
|
||||||
78
src/components/date-input.tsx
Normal file
78
src/components/date-input.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { TextInput, Flex, Box, Button } from "@mantine/core";
|
||||||
|
import { CalendarIcon } from "@phosphor-icons/react";
|
||||||
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
|
import Sheet from "@/components/sheet/sheet";
|
||||||
|
import { DateTimePicker } from "../features/admin/components/date-time-picker";
|
||||||
|
|
||||||
|
interface DateInputProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
withAsterisk?: boolean;
|
||||||
|
error?: React.ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date input that opens a sheet with mantine date time picker when clicked
|
||||||
|
export const DateInputSheet = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
withAsterisk,
|
||||||
|
error,
|
||||||
|
placeholder
|
||||||
|
}: DateInputProps) => {
|
||||||
|
const sheet = useSheet();
|
||||||
|
|
||||||
|
const formatDisplayValue = (dateString: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (newValue: string | null) => {
|
||||||
|
onChange(newValue || '');
|
||||||
|
sheet.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateValue = value ? new Date(value) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label={label}
|
||||||
|
value={formatDisplayValue(value)}
|
||||||
|
onClick={sheet.open}
|
||||||
|
readOnly
|
||||||
|
withAsterisk={withAsterisk}
|
||||||
|
error={error}
|
||||||
|
placeholder={placeholder || "Click to select date"}
|
||||||
|
rightSection={<CalendarIcon size={16} />}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Sheet {...sheet.props} title={`Select ${label}`}>
|
||||||
|
<Flex gap='xs' direction='column' justify='center' p="md">
|
||||||
|
<Box mx='auto'>
|
||||||
|
<DateTimePicker
|
||||||
|
value={dateValue}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button onClick={sheet.close} variant="subtle">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Box, Text, Group, ActionIcon, Button, ScrollArea, Divider } from "@mantine/core";
|
import { Box, Text, Group, ActionIcon, ScrollArea, Divider } from "@mantine/core";
|
||||||
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
|
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
|
||||||
import { useState, ReactNode} from "react";
|
import { useState, ReactNode} from "react";
|
||||||
import { SlidePanelContext, type PanelConfig } from "./slide-panel-context";
|
import { SlidePanelContext, type PanelConfig } from "./slide-panel-context";
|
||||||
|
import Button from "@/components/button";
|
||||||
|
|
||||||
interface SlidePanelProps {
|
interface SlidePanelProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core";
|
import { FloatingIndicator, UnstyledButton, Box, Text, ScrollArea } from "@mantine/core";
|
||||||
import { Carousel } from "@mantine/carousel";
|
import { Carousel } from "@mantine/carousel";
|
||||||
import { useState, useEffect, ReactNode } from "react";
|
import { useState, useEffect, ReactNode } from "react";
|
||||||
|
|
||||||
|
|||||||
193
src/features/admin/components/edit-tournament.tsx
Normal file
193
src/features/admin/components/edit-tournament.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { Box, FileInput, Group, Stack, TextInput, Textarea, Title } from "@mantine/core";
|
||||||
|
import { useForm, UseFormInput } from "@mantine/form";
|
||||||
|
import { LinkIcon } from "@phosphor-icons/react";
|
||||||
|
import { Tournament, TournamentFormInput } from "@/features/tournaments/types";
|
||||||
|
import { DateInputSheet } from "../../../components/date-input";
|
||||||
|
import { isNotEmpty } from "@mantine/form";
|
||||||
|
import toast from '@/lib/sonner';
|
||||||
|
import { logger } from "..";
|
||||||
|
import { useQueryClient, useMutation } from "@tanstack/react-query";
|
||||||
|
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||||
|
import { updateTournament } from "@/features/tournaments/server";
|
||||||
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
import Button from "@/components/button";
|
||||||
|
|
||||||
|
interface EditTournamentProps {
|
||||||
|
tournament: Tournament;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTournament = ({ tournament }: EditTournamentProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const config: UseFormInput<TournamentFormInput> = {
|
||||||
|
initialValues: {
|
||||||
|
name: tournament.name || '',
|
||||||
|
location: tournament.location || '',
|
||||||
|
desc: tournament.desc || '',
|
||||||
|
start_time: tournament.start_time || '',
|
||||||
|
enroll_time: tournament.enroll_time || '',
|
||||||
|
end_time: tournament.end_time || '',
|
||||||
|
rules: tournament.rules || '',
|
||||||
|
logo: undefined, // Always start with no file selected
|
||||||
|
},
|
||||||
|
onSubmitPreventDefault: 'always',
|
||||||
|
validate: {
|
||||||
|
name: isNotEmpty('Name is required'),
|
||||||
|
location: isNotEmpty('Location is required'),
|
||||||
|
start_time: isNotEmpty('Start time is required'),
|
||||||
|
enroll_time: isNotEmpty('Enrollment time is required'),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm(config);
|
||||||
|
|
||||||
|
const { mutate: editTournament, isPending } = useMutation({
|
||||||
|
mutationFn: (data: Partial<TournamentFormInput>) =>
|
||||||
|
updateTournament({ data: { id: tournament.id, updates: data } }),
|
||||||
|
onSuccess: (updatedTournament) => {
|
||||||
|
if (updatedTournament) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentQueries.list().queryKey });
|
||||||
|
queryClient.setQueryData(
|
||||||
|
tournamentQueries.details(updatedTournament.id).queryKey,
|
||||||
|
updatedTournament
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (values: TournamentFormInput) => {
|
||||||
|
const { logo, ...tournamentData } = values;
|
||||||
|
|
||||||
|
editTournament(tournamentData, {
|
||||||
|
onSuccess: async (updatedTournament) => {
|
||||||
|
if (logo && updatedTournament) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('tournamentId', updatedTournament.id);
|
||||||
|
formData.append('logo', logo);
|
||||||
|
|
||||||
|
const response = await fetch('/api/tournaments/upload-logo', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to upload logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
queryClient.setQueryData(
|
||||||
|
tournamentQueries.details(result.tournament!.id).queryKey,
|
||||||
|
result.tournament
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.success('Tournament updated successfully!');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(`Tournament updated but logo upload failed: ${error.message}`);
|
||||||
|
logger.error('Tournament logo upload error', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.success('Tournament updated successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
router.history.back();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(`Failed to update tournament: ${error.message}`);
|
||||||
|
logger.error('Tournament update error', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box w="100%" maw={600} mx="auto" p="lg">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Title order={2}>Edit Tournament</Title>
|
||||||
|
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
withAsterisk
|
||||||
|
key={form.key('name')}
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Location"
|
||||||
|
withAsterisk
|
||||||
|
key={form.key('location')}
|
||||||
|
{...form.getInputProps('location')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Description"
|
||||||
|
key={form.key('desc')}
|
||||||
|
{...form.getInputProps('desc')}
|
||||||
|
minRows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Rules"
|
||||||
|
key={form.key('rules')}
|
||||||
|
{...form.getInputProps('rules')}
|
||||||
|
minRows={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileInput
|
||||||
|
key={form.key('logo')}
|
||||||
|
accept="image/png,image/jpeg,image/gif,image/jpg"
|
||||||
|
label="Change Logo"
|
||||||
|
leftSection={<LinkIcon size={16} />}
|
||||||
|
{...form.getInputProps('logo')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInputSheet
|
||||||
|
label="Start Date"
|
||||||
|
withAsterisk
|
||||||
|
value={form.values.start_time}
|
||||||
|
onChange={(value) => form.setFieldValue('start_time', value)}
|
||||||
|
error={form.errors.start_time}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInputSheet
|
||||||
|
label="Enrollment Due"
|
||||||
|
withAsterisk
|
||||||
|
value={form.values.enroll_time}
|
||||||
|
onChange={(value) => form.setFieldValue('enroll_time', value)}
|
||||||
|
error={form.errors.enroll_time}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInputSheet
|
||||||
|
label="End Date (Optional)"
|
||||||
|
value={form.values.end_time || ''}
|
||||||
|
onChange={(value) => form.setFieldValue('end_time', value)}
|
||||||
|
error={form.errors.end_time}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="lg">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.history.back()}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={isPending}
|
||||||
|
>
|
||||||
|
Update Tournament
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditTournament;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, TextInput } from "@mantine/core";
|
import { TextInput } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import useCreateUser from "../hooks/use-create-user";
|
import useCreateUser from "../hooks/use-create-user";
|
||||||
|
import Button from "@/components/button";
|
||||||
|
|
||||||
const NamePrompt = () => {
|
const NamePrompt = () => {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@@ -41,7 +42,7 @@ const NamePrompt = () => {
|
|||||||
key={form.key('last_name')}
|
key={form.key('last_name')}
|
||||||
{...form.getInputProps('last_name')}
|
{...form.getInputProps('last_name')}
|
||||||
/>
|
/>
|
||||||
<Button loading={isPending} type='submit' w='100%' mt='10px' variant='filled'>Create Account</Button>
|
<Button loading={isPending} type='submit' mt='10px' variant='filled'>Create Account</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Center, ElementProps, SimpleGrid, Text } from "@mantine/core";
|
import Button from "@/components/button";
|
||||||
|
import { Center, ElementProps, SimpleGrid, Text } from "@mantine/core";
|
||||||
import { ChalkboardTeacherIcon } from "@phosphor-icons/react";
|
import { ChalkboardTeacherIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
const ExistingPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
const ExistingPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useState, FormEventHandler, useMemo } from 'react';
|
import { useState, FormEventHandler, useMemo } from 'react';
|
||||||
import { ArrowLeftIcon } from '@phosphor-icons/react';
|
import { ArrowLeftIcon } from '@phosphor-icons/react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Autocomplete, Button, Divider, Flex, Text, TextInput, Title, UnstyledButton } from '@mantine/core';
|
import { Autocomplete, Divider, Flex, Text, TextInput, Title, UnstyledButton } from '@mantine/core';
|
||||||
import ExistingPlayerButton from './existing-player-button';
|
import ExistingPlayerButton from './existing-player-button';
|
||||||
import NewPlayerButton from './new-player-button';
|
import NewPlayerButton from './new-player-button';
|
||||||
import { Player } from '@/features/players/types';
|
import { Player } from '@/features/players/types';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { playerQueries } from '@/features/players/queries';
|
import { playerQueries } from '@/features/players/queries';
|
||||||
import useCreateUser from '../../hooks/use-create-user';
|
import useCreateUser from '../../hooks/use-create-user';
|
||||||
|
import Button from '@/components/button';
|
||||||
|
|
||||||
enum PlayerPromptStage {
|
enum PlayerPromptStage {
|
||||||
returning = 'returning',
|
returning = 'returning',
|
||||||
@@ -109,7 +110,7 @@ const PlayerPrompt = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleNewPlayerChange}
|
onChange={handleNewPlayerChange}
|
||||||
/>
|
/>
|
||||||
<Button type='submit' w='100%' mt='10px' color='green' variant='filled'>Submit</Button>
|
<Button type='submit' mt='10px' color='green' variant='filled'>Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
</> :
|
</> :
|
||||||
<form onSubmit={formSubmitHandler(handlePlayerSubmit)}>
|
<form onSubmit={formSubmitHandler(handlePlayerSubmit)}>
|
||||||
@@ -120,7 +121,7 @@ const PlayerPrompt = () => {
|
|||||||
onChange={handleReturningPlayerChange}
|
onChange={handleReturningPlayerChange}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
<Button type='submit' w='100%' mt='10px' color='green' variant='filled'>Submit</Button>
|
<Button type='submit' mt='10px' color='green' variant='filled'>Submit</Button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Center, ElementProps, SimpleGrid, Text } from "@mantine/core";
|
import Button from "@/components/button";
|
||||||
|
import { Center, ElementProps, SimpleGrid, Text } from "@mantine/core";
|
||||||
import { UserPlusIcon } from "@phosphor-icons/react";
|
import { UserPlusIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
const NewPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
const NewPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ const Profile = ({ player }: ProfileProps) => {
|
|||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <Text p="md">Stats/Badges will go here</Text>
|
content: <Text p="md">Stats/Badges will go here</Text>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Matches",
|
||||||
|
content: <Text p="md">Matches feed will go here</Text>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Teams",
|
label: "Teams",
|
||||||
content: <>
|
content: <>
|
||||||
<TeamList teams={player.teams || []} />
|
<TeamList teams={player.teams || []} />
|
||||||
</>
|
</>
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Tournaments",
|
|
||||||
content: <Text p="md">Panel 3 content</Text>
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { updatePlayer } from "@/features/players/server";
|
import { updatePlayer } from "@/features/players/server";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Button, Stack, TextInput } from "@mantine/core"
|
import { Stack, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import toast from "@/lib/sonner";
|
import toast from "@/lib/sonner";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { Player } from "../../types";
|
import { Player } from "../../types";
|
||||||
|
import Button from "@/components/button";
|
||||||
|
|
||||||
interface NameUpdateFormProps {
|
interface NameUpdateFormProps {
|
||||||
player: Player;
|
player: Player;
|
||||||
@@ -50,8 +51,8 @@ const NameUpdateForm = ({ player, toggle }: NameUpdateFormProps) => {
|
|||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
<TextInput label='First Name' {...form.getInputProps('first_name')} />
|
<TextInput label='First Name' {...form.getInputProps('first_name')} />
|
||||||
<TextInput label='Last Name' {...form.getInputProps('last_name')} />
|
<TextInput label='Last Name' {...form.getInputProps('last_name')} />
|
||||||
<Button fullWidth loading={isPending} type='submit'>Save</Button>
|
<Button loading={isPending} type='submit'>Save</Button>
|
||||||
<Button fullWidth variant='subtle' color='red' onClick={toggle}>Cancel</Button>
|
<Button variant='subtle' color='red' onClick={toggle}>Cancel</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
23
src/features/tournaments/components/profile/header.tsx
Normal file
23
src/features/tournaments/components/profile/header.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Flex, Title } from "@mantine/core";
|
||||||
|
import Avatar from "@/components/avatar";
|
||||||
|
import { Tournament } from "../../types";
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
tournament: Tournament;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ tournament }: HeaderProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex px='xl' w='100%' align='self-end' gap='md'>
|
||||||
|
<Avatar name={tournament.name} radius={0} withBorder={false} size={125} src={`/api/files/tournaments/${tournament.id}/${tournament.logo}`} />
|
||||||
|
<Flex align='center' justify='center' gap={4} pb={20} w='100%'>
|
||||||
|
<Title ta='center' order={2}>{tournament.name}</Title>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
38
src/features/tournaments/components/profile/index.tsx
Normal file
38
src/features/tournaments/components/profile/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Box, Divider, Text } from "@mantine/core";
|
||||||
|
import Header from "./header";
|
||||||
|
import TeamList from "@/features/teams/components/team-list";
|
||||||
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
|
import { Tournament } from "../../types";
|
||||||
|
import { PreviewBracket } from "@/features/bracket/components/preview";
|
||||||
|
|
||||||
|
interface ProfileProps {
|
||||||
|
tournament: Tournament;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Profile = ({ tournament }: ProfileProps) => {
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "Overview",
|
||||||
|
content: <Text p="md">Stats/Badges will go here, bracket link</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Matches",
|
||||||
|
content: <Text p="md">Matches feed will go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Teams",
|
||||||
|
content: <>
|
||||||
|
<TeamList teams={tournament.teams || []} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Header tournament={tournament} />
|
||||||
|
<Box m='sm' mt='lg'>
|
||||||
|
<SwipeableTabs tabs={tabs} />
|
||||||
|
</Box>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
40
src/features/tournaments/components/tournament-list.tsx
Normal file
40
src/features/tournaments/components/tournament-list.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import Avatar from "@/components/avatar";
|
||||||
|
import { Tournament } from "../types";
|
||||||
|
|
||||||
|
interface TournamentListProps {
|
||||||
|
tournaments: Tournament[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TournamentList = ({ tournaments, loading = false }: TournamentListProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (loading) return <List>
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<ListItem py='xs'
|
||||||
|
icon={<Skeleton height={40} width={40} />}
|
||||||
|
>
|
||||||
|
<Skeleton height={20} width={200} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
return <List>
|
||||||
|
{tournaments?.map((tournament) => (
|
||||||
|
<ListItem key={tournament.id}
|
||||||
|
py='xs'
|
||||||
|
icon={<Avatar radius='xs' size={40} name={`${tournament.name}`} src={`/api/files/tournaments/${tournament.id}/${tournament.logo}`} />}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
navigate({ to: `/tournaments/${tournament.id}` });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={500}>{`${tournament.name}`}</Text>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TournamentList;
|
||||||
@@ -33,6 +33,24 @@ export const createTournament = createServerFn()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateTournament = createServerFn()
|
||||||
|
.validator(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
updates: tournamentInputSchema.partial()
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
try {
|
||||||
|
logger.info('Updating tournament', data);
|
||||||
|
|
||||||
|
const tournament = await pbAdmin.updateTournament(data.id, data.updates);
|
||||||
|
return tournament;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating tournament', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const getTournament = createServerFn()
|
export const getTournament = createServerFn()
|
||||||
.validator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
},
|
},
|
||||||
async updateTournament(
|
async updateTournament(
|
||||||
id: string,
|
id: string,
|
||||||
data: TournamentUpdateInput
|
data: Partial<TournamentUpdateInput>
|
||||||
): Promise<Tournament> {
|
): Promise<Tournament> {
|
||||||
const result = await pb
|
const result = await pb
|
||||||
.collection("tournaments")
|
.collection("tournaments")
|
||||||
|
|||||||
Reference in New Issue
Block a user