several changes
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { Title, List, Divider } from "@mantine/core";
|
||||
import ListLink from "@/components/list-link";
|
||||
import Page from "@/components/page";
|
||||
import { TrophyIcon, UsersFourIcon, UsersThreeIcon } from "@phosphor-icons/react";
|
||||
import { DatabaseIcon, TrophyIcon, UsersFourIcon, UsersThreeIcon } from "@phosphor-icons/react";
|
||||
import ListButton from "@/components/list-button";
|
||||
|
||||
const AdminPage = () => {
|
||||
console.log(process.env.VITE_POCKETBASE_URL!)
|
||||
return (
|
||||
<Page noPadding>
|
||||
<Title pl='sm' order={2} mb="md">Admin</Title>
|
||||
<List>
|
||||
<ListLink label="Manage Tournaments" Icon={TrophyIcon} to="/admin/tournaments" />
|
||||
<ListButton label="Open Pocketbase" Icon={DatabaseIcon} onClick={() => window.location.replace(import.meta.env.VITE_POCKETBASE_URL! + "/_/")} />
|
||||
</List>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { DatePicker, TimeInput } from "@mantine/dates";
|
||||
import { ActionIcon, Stack } from "@mantine/core";
|
||||
import { useRef } from "react";
|
||||
import { ClockIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface DateTimePickerProps {
|
||||
value: Date | null;
|
||||
onChange: (date: string | null) => void;
|
||||
label?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const DateTimePicker = ({ value, onChange, label, ...rest }: DateTimePickerProps) => {
|
||||
const timeRef = useRef<HTMLInputElement>(null);
|
||||
const currentDate = value ? new Date(value) : null;
|
||||
|
||||
const formatDate = (date: Date | null): string => {
|
||||
if (!date) return "";
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
const formatTime = (date: Date | null): string => {
|
||||
if (!date) return "";
|
||||
return date.toTimeString().slice(0, 5);
|
||||
};
|
||||
|
||||
const handleDateChange = (dateString: string | null) => {
|
||||
if (!dateString) {
|
||||
onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
const newDate = new Date(dateString + 'T00:00:00');
|
||||
|
||||
if (currentDate) {
|
||||
newDate.setHours(currentDate.getHours());
|
||||
newDate.setMinutes(currentDate.getMinutes());
|
||||
}
|
||||
|
||||
onChange(newDate.toISOString());
|
||||
};
|
||||
|
||||
const handleTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const timeValue = event.target.value;
|
||||
if (!timeValue) return;
|
||||
|
||||
const [hours, minutes] = timeValue.split(':').map(Number);
|
||||
if (isNaN(hours) || isNaN(minutes)) return;
|
||||
|
||||
const baseDate = currentDate || new Date();
|
||||
const newDate = new Date(baseDate);
|
||||
|
||||
newDate.setHours(hours);
|
||||
newDate.setMinutes(minutes);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
|
||||
onChange(newDate.toISOString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<DatePicker
|
||||
size="md"
|
||||
value={formatDate(currentDate)}
|
||||
onChange={handleDateChange}
|
||||
{...rest}
|
||||
/>
|
||||
<TimeInput
|
||||
ref={timeRef}
|
||||
label="Time"
|
||||
size="md"
|
||||
value={formatTime(currentDate)}
|
||||
onChange={handleTimeChange}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => timeRef.current?.showPicker()}
|
||||
>
|
||||
<ClockIcon size={16} />
|
||||
</ActionIcon>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateTimePicker };
|
||||
@@ -1,193 +0,0 @@
|
||||
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,21 +1,14 @@
|
||||
import { List } from "@mantine/core";
|
||||
import Page from "@/components/page";
|
||||
import { TrophyIcon, UsersThreeIcon } from "@phosphor-icons/react";
|
||||
import ListButton from "@/components/list-button";
|
||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import ListLink from "@/components/list-link";
|
||||
|
||||
const ManageTournaments = () => {
|
||||
const { data: tournaments } = useSuspenseQuery(tournamentQueries.list());
|
||||
return (
|
||||
<List>
|
||||
<ListButton
|
||||
label="Edit Enrolled Teams"
|
||||
Icon={UsersThreeIcon}
|
||||
handleClick={console.log}
|
||||
/>
|
||||
{tournaments.map(t => (
|
||||
<ListButton label={t.name} Icon={TrophyIcon} handleClick={console.log} />
|
||||
<ListLink label={t.name} to={`/admin/tournaments/${t.id}`} />
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,4 @@ import { Logger } from "@/lib/logger";
|
||||
|
||||
export const logger = new Logger('Admin');
|
||||
|
||||
export { default as CreateTournament } from './components/create-tournament';
|
||||
export { default as EditTournament } from './components/edit-tournament';
|
||||
export { default as AdminPage } from './components/admin-page';
|
||||
@@ -45,7 +45,7 @@ export const updatePlayer = createServerFn()
|
||||
.validator(playerUpdateSchema)
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ context, data }) => {
|
||||
const userAuthId = (context as any).userAuthId;
|
||||
const userAuthId = context.userAuthId;
|
||||
if (!userAuthId) return;
|
||||
|
||||
try {
|
||||
@@ -78,7 +78,7 @@ export const createPlayer = createServerFn()
|
||||
.validator(playerInputSchema)
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ context, data }) => {
|
||||
const userAuthId = (context as any).userAuthId;
|
||||
const userAuthId = context.userAuthId;
|
||||
if (!userAuthId) return;
|
||||
|
||||
try {
|
||||
@@ -106,7 +106,7 @@ export const associatePlayer = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ context, data }) => {
|
||||
const userAuthId = (context as any).userAuthId;
|
||||
const userAuthId = context.userAuthId;
|
||||
if (!userAuthId) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,23 +3,33 @@ import { useForm, UseFormInput } from "@mantine/form";
|
||||
import { LinkIcon } from "@phosphor-icons/react";
|
||||
import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel";
|
||||
import { TournamentFormInput } from "@/features/tournaments/types";
|
||||
import { DateTimePicker } from "./date-time-picker";
|
||||
import { isNotEmpty } from "@mantine/form";
|
||||
import useCreateTournament from "../hooks/use-create-tournament";
|
||||
import useUpdateTournament from "../hooks/use-update-tournament";
|
||||
import toast from '@/lib/sonner';
|
||||
import { logger } from "..";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
|
||||
const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
interface TournamentFormProps {
|
||||
close: () => void;
|
||||
initialValues?: Partial<TournamentFormInput>;
|
||||
tournamentId?: string;
|
||||
}
|
||||
|
||||
const TournamentForm = ({ close, initialValues, tournamentId }: TournamentFormProps) => {
|
||||
const isEditMode = !!tournamentId;
|
||||
|
||||
const config: UseFormInput<TournamentFormInput> = {
|
||||
initialValues: { // TODO : Remove fake initial values
|
||||
name: 'Test Tournament',
|
||||
location: 'Test Location',
|
||||
desc: 'Test Description',
|
||||
start_time: '2025-01-01T00:00:00Z',
|
||||
enroll_time: '2025-01-01T00:00:00Z',
|
||||
initialValues: {
|
||||
name: initialValues?.name || '',
|
||||
location: initialValues?.location || '',
|
||||
desc: initialValues?.desc || '',
|
||||
start_time: initialValues?.start_time || '',
|
||||
enroll_time: initialValues?.enroll_time || '',
|
||||
end_time: initialValues?.end_time || '',
|
||||
logo: undefined,
|
||||
},
|
||||
onSubmitPreventDefault: 'always',
|
||||
validate: {
|
||||
@@ -33,12 +43,19 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
const form = useForm(config);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createTournament, isPending } = useCreateTournament();
|
||||
const { mutate: createTournament, isPending: createPending } = useCreateTournament();
|
||||
const { mutate: updateTournament, isPending: updatePending } = useUpdateTournament(tournamentId || '');
|
||||
|
||||
const isPending = createPending || updatePending;
|
||||
|
||||
const handleSubmit = async (values: TournamentFormInput) => {
|
||||
const { logo, ...tournamentData } = values;
|
||||
|
||||
createTournament(tournamentData, {
|
||||
const mutation = isEditMode ? updateTournament : createTournament;
|
||||
const successMessage = isEditMode ? 'Tournament updated successfully!' : 'Tournament created successfully!';
|
||||
const errorMessage = isEditMode ? 'Failed to update tournament' : 'Failed to create tournament';
|
||||
|
||||
mutation(tournamentData, {
|
||||
onSuccess: async (tournament) => {
|
||||
if (logo && tournament) {
|
||||
try {
|
||||
@@ -59,18 +76,28 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
const result = await response.json();
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: tournamentQueries.list().queryKey });
|
||||
queryClient.invalidateQueries({ queryKey: tournamentQueries.details(result.tournament!.id).queryKey });
|
||||
queryClient.setQueryData(
|
||||
tournamentQueries.details(result.tournament!.id).queryKey,
|
||||
result.tournament
|
||||
);
|
||||
|
||||
toast.success('Tournament created successfully!');
|
||||
toast.success(successMessage);
|
||||
} catch (error: any) {
|
||||
toast.error(`Tournament created but logo upload failed: ${error.message}`);
|
||||
const logoErrorMessage = isEditMode
|
||||
? `Tournament updated but logo upload failed: ${error.message}`
|
||||
: `Tournament created but logo upload failed: ${error.message}`;
|
||||
toast.error(logoErrorMessage);
|
||||
logger.error('Tournament logo upload error', error);
|
||||
}
|
||||
} else {
|
||||
toast.success(successMessage);
|
||||
}
|
||||
close();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(`${errorMessage}: ${error.message}`);
|
||||
logger.error(`Tournament ${isEditMode ? 'update' : 'create'} error`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -79,7 +106,7 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
<SlidePanel
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
onCancel={close}
|
||||
submitText="Create Tournament"
|
||||
submitText={isEditMode ? "Update Tournament" : "Create Tournament"}
|
||||
cancelText="Cancel"
|
||||
loading={isPending}
|
||||
>
|
||||
@@ -97,16 +124,17 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
{...form.getInputProps('location')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Short Description"
|
||||
<Textarea
|
||||
label="Description"
|
||||
key={form.key('desc')}
|
||||
{...form.getInputProps('desc')}
|
||||
minRows={3}
|
||||
/>
|
||||
|
||||
<FileInput
|
||||
key={form.key('logo')}
|
||||
accept="image/png,image/jpeg,image/gif,image/jpg"
|
||||
label="Logo"
|
||||
label={isEditMode ? "Change Logo" : "Logo"}
|
||||
leftSection={<LinkIcon size={16} />}
|
||||
{...form.getInputProps('logo')}
|
||||
/>
|
||||
@@ -146,9 +174,28 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
hour12: true
|
||||
})}
|
||||
/>
|
||||
|
||||
{isEditMode && (
|
||||
<SlidePanelField
|
||||
key={form.key('end_time')}
|
||||
{...form.getInputProps('end_time')}
|
||||
Component={DateTimePicker}
|
||||
title="Select End Date"
|
||||
label="End Date (Optional)"
|
||||
formatValue={(date) => date ? new Date(date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true
|
||||
}) : 'Not set'}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</SlidePanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTournament;
|
||||
export default TournamentForm;
|
||||
12
src/features/tournaments/hooks/use-update-tournament.ts
Normal file
12
src/features/tournaments/hooks/use-update-tournament.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { updateTournament } from "@/features/tournaments/server";
|
||||
import { TournamentFormInput } from "@/features/tournaments/types";
|
||||
|
||||
const useUpdateTournament = (tournamentId: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: Partial<TournamentFormInput>) =>
|
||||
updateTournament({ data: { id: tournamentId, updates: data } }),
|
||||
});
|
||||
};
|
||||
|
||||
export default useUpdateTournament;
|
||||
Reference in New Issue
Block a user