various improvements, edit tournament, etc
This commit is contained in:
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;
|
||||
16
src/features/core/components/providers.tsx
Normal file
16
src/features/core/components/providers.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AuthProvider } from "@/contexts/auth-context"
|
||||
import MantineProvider from "@/lib/mantine/mantine-provider"
|
||||
import { Toaster } from "sonner"
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<MantineProvider>
|
||||
<Toaster position='top-center' />
|
||||
{children}
|
||||
</MantineProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Providers;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, TextInput } from "@mantine/core";
|
||||
import { TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import useCreateUser from "../hooks/use-create-user";
|
||||
import Button from "@/components/button";
|
||||
|
||||
const NamePrompt = () => {
|
||||
const form = useForm({
|
||||
@@ -41,7 +42,7 @@ const NamePrompt = () => {
|
||||
key={form.key('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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
const ExistingPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState, FormEventHandler, useMemo } from 'react';
|
||||
import { ArrowLeftIcon } from '@phosphor-icons/react';
|
||||
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 NewPlayerButton from './new-player-button';
|
||||
import { Player } from '@/features/players/types';
|
||||
import { toast } from 'sonner';
|
||||
import { playerQueries } from '@/features/players/queries';
|
||||
import useCreateUser from '../../hooks/use-create-user';
|
||||
import Button from '@/components/button';
|
||||
|
||||
enum PlayerPromptStage {
|
||||
returning = 'returning',
|
||||
@@ -109,7 +110,7 @@ const PlayerPrompt = () => {
|
||||
value={value}
|
||||
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 onSubmit={formSubmitHandler(handlePlayerSubmit)}>
|
||||
@@ -120,7 +121,7 @@ const PlayerPrompt = () => {
|
||||
onChange={handleReturningPlayerChange}
|
||||
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>
|
||||
}
|
||||
</>
|
||||
|
||||
@@ -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";
|
||||
|
||||
const NewPlayerButton: React.FC<ElementProps<"button">> = ({ onClick }) => {
|
||||
|
||||
@@ -15,15 +15,15 @@ const Profile = ({ player }: ProfileProps) => {
|
||||
label: "Overview",
|
||||
content: <Text p="md">Stats/Badges will go here</Text>
|
||||
},
|
||||
{
|
||||
label: "Matches",
|
||||
content: <Text p="md">Matches feed will go here</Text>
|
||||
},
|
||||
{
|
||||
label: "Teams",
|
||||
content: <>
|
||||
<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 { useMutation } from "@tanstack/react-query";
|
||||
import { Button, Stack, TextInput } from "@mantine/core"
|
||||
import { Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form";
|
||||
import toast from "@/lib/sonner";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { Player } from "../../types";
|
||||
import Button from "@/components/button";
|
||||
|
||||
interface NameUpdateFormProps {
|
||||
player: Player;
|
||||
@@ -50,8 +51,8 @@ const NameUpdateForm = ({ player, toggle }: NameUpdateFormProps) => {
|
||||
<Stack gap='xs'>
|
||||
<TextInput label='First Name' {...form.getInputProps('first_name')} />
|
||||
<TextInput label='Last Name' {...form.getInputProps('last_name')} />
|
||||
<Button fullWidth loading={isPending} type='submit'>Save</Button>
|
||||
<Button fullWidth variant='subtle' color='red' onClick={toggle}>Cancel</Button>
|
||||
<Button loading={isPending} type='submit'>Save</Button>
|
||||
<Button variant='subtle' color='red' onClick={toggle}>Cancel</Button>
|
||||
</Stack>
|
||||
</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()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
|
||||
Reference in New Issue
Block a user