diff --git a/src/app/routes/_authed/admin/tournaments/$id.tsx b/src/app/routes/_authed/admin/tournaments/$id.tsx
index 3b360cb..be017f8 100644
--- a/src/app/routes/_authed/admin/tournaments/$id.tsx
+++ b/src/app/routes/_authed/admin/tournaments/$id.tsx
@@ -1,21 +1,28 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, redirect } 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'
+import { List } from '@mantine/core'
+import ListButton from '@/components/list-button'
+import { HardDrivesIcon, PencilLineIcon, UsersThreeIcon } from '@phosphor-icons/react'
+import { useSheet } from '@/hooks/use-sheet'
+import Sheet from '@/components/sheet/sheet'
+import TournamentForm from '@/features/tournaments/components/tournament-form'
export const Route = createFileRoute('/_authed/admin/tournaments/$id')({
beforeLoad: async ({ context, params }) => {
const { queryClient } = context;
- await queryClient.ensureQueryData(tournamentQueries.details(params.id))
+ const tournament = await queryClient.ensureQueryData(tournamentQueries.details(params.id))
+ if (!tournament) throw redirect({ to: '/admin/tournaments' });
+ return {
+ tournament
+ }
},
- loader: () => ({
+ loader: ({ context }) => ({
header: {
withBackButton: true,
- title: 'Edit Tournament',
+ title: `Manage ${context.tournament.name}`,
},
+ withPadding: false
}),
component: RouteComponent,
})
@@ -25,9 +32,30 @@ function RouteComponent() {
const { data: tournament } = useQuery(tournamentQueries.details(id))
if (!tournament) throw new Error("Tournament not found.")
+ const { isOpen: editTournamentOpened, open: openEditTournament, close: closeEditTournament } = useSheet();
+
return (
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ >
)
}
\ No newline at end of file
diff --git a/src/app/routes/_authed/tournaments/index.tsx b/src/app/routes/_authed/tournaments/index.tsx
index 42379d3..8b9c58e 100644
--- a/src/app/routes/_authed/tournaments/index.tsx
+++ b/src/app/routes/_authed/tournaments/index.tsx
@@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'
import { useAuth } from '@/contexts/auth-context'
import { useSheet } from '@/hooks/use-sheet'
import Sheet from '@/components/sheet/sheet'
-import CreateTournament from '@/features/admin/components/create-tournament'
+import TournamentForm from '@/features/tournaments/components/tournament-form'
import { PlusIcon } from '@phosphor-icons/react'
import Button from '@/components/button'
@@ -38,7 +38,7 @@ function RouteComponent() {
<>
} variant='subtle' onClick={sheet.open}>Create Tournament
-
+
>
) : null
diff --git a/src/app/routes/api/events.$.ts b/src/app/routes/api/events.$.ts
index 2584ed4..b67c086 100644
--- a/src/app/routes/api/events.$.ts
+++ b/src/app/routes/api/events.$.ts
@@ -5,7 +5,7 @@ import { superTokensRequestMiddleware } from "@/utils/supertokens";
export const ServerRoute = createServerFileRoute("/api/events/$").middleware([superTokensRequestMiddleware]).methods({
GET: ({ request, context }) => {
- logger.info('ServerEvents | New connection', (context as any)?.userAuthId);
+ logger.info('ServerEvents | New connection', context?.userAuthId);
const stream = new ReadableStream({
start(controller) {
@@ -41,7 +41,7 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
serverEvents.off("test", handleEvent);
clearInterval(pingInterval);
try {
- logger.info('ServerEvents | Closing connection', (context as any)?.userAuthId);
+ logger.info('ServerEvents | Closing connection', context?.userAuthId);
controller.close();
} catch (e) {
logger.error('ServerEvents | Error closing controller', e);
diff --git a/src/components/date-input.tsx b/src/components/date-input.tsx
deleted file mode 100644
index 519a289..0000000
--- a/src/components/date-input.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-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 (
- <>
- }
- style={{ cursor: 'pointer' }}
- />
-
-
-
-
-
-
-
-
-
- >
- );
-};
\ No newline at end of file
diff --git a/src/features/admin/components/date-time-picker.tsx b/src/components/date-time-picker.tsx
similarity index 100%
rename from src/features/admin/components/date-time-picker.tsx
rename to src/components/date-time-picker.tsx
diff --git a/src/components/list-button.tsx b/src/components/list-button.tsx
index 91278c6..aaca803 100644
--- a/src/components/list-button.tsx
+++ b/src/components/list-button.tsx
@@ -4,17 +4,17 @@ import { CaretRightIcon, Icon } from "@phosphor-icons/react";
interface ListButtonProps {
label: string;
Icon: Icon;
- handleClick: () => void;
+ onClick: () => void;
}
-const ListButton = ({ label, handleClick, Icon }: ListButtonProps) => {
+const ListButton = ({ label, onClick, Icon }: ListButtonProps) => {
return (
<>
diff --git a/src/components/list-link.tsx b/src/components/list-link.tsx
index a8798d8..003d96f 100644
--- a/src/components/list-link.tsx
+++ b/src/components/list-link.tsx
@@ -5,7 +5,7 @@ import { Link, useNavigate } from "@tanstack/react-router";
interface ListLinkProps {
label: string;
to: string;
- Icon: Icon;
+ Icon?: Icon;
}
const ListLink = ({ label, to, Icon }: ListLinkProps) => {
@@ -19,7 +19,7 @@ const ListLink = ({ label, to, Icon }: ListLinkProps) => {
component={'button'}
onClick={() => navigate({ to })}
label={{label}}
- leftSection={}
+ leftSection={Icon && }
rightSection={}
/>
diff --git a/src/contexts/auth-context.tsx b/src/contexts/auth-context.tsx
index 471d0c4..393a99d 100644
--- a/src/contexts/auth-context.tsx
+++ b/src/contexts/auth-context.tsx
@@ -2,6 +2,7 @@ import { createContext, PropsWithChildren, useCallback, useContext, useMemo } fr
import { MantineColor, MantineColorScheme } from "@mantine/core";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchMe } from "@/features/players/server";
+import { Player } from "@/features/players/types";
const queryKey = ['auth'];
export const authQueryConfig = {
@@ -10,7 +11,7 @@ export const authQueryConfig = {
}
interface AuthData {
- user: any;
+ user: Player | undefined;
metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme };
roles: string[];
}
diff --git a/src/features/admin/components/admin-page.tsx b/src/features/admin/components/admin-page.tsx
index b7d2897..a57fca4 100644
--- a/src/features/admin/components/admin-page.tsx
+++ b/src/features/admin/components/admin-page.tsx
@@ -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 (
Admin
+ window.location.replace(import.meta.env.VITE_POCKETBASE_URL! + "/_/")} />
);
diff --git a/src/features/admin/components/edit-tournament.tsx b/src/features/admin/components/edit-tournament.tsx
deleted file mode 100644
index 86d2200..0000000
--- a/src/features/admin/components/edit-tournament.tsx
+++ /dev/null
@@ -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 = {
- 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) =>
- 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 (
-
-
- Edit Tournament
-
-
-
-
- );
-};
-
-export default EditTournament;
\ No newline at end of file
diff --git a/src/features/admin/components/manage-tournaments.tsx b/src/features/admin/components/manage-tournaments.tsx
index cc46a85..6220076 100644
--- a/src/features/admin/components/manage-tournaments.tsx
+++ b/src/features/admin/components/manage-tournaments.tsx
@@ -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 (
-
{tournaments.map(t => (
-
+
))}
);
diff --git a/src/features/admin/index.ts b/src/features/admin/index.ts
index da205b1..f82c2b1 100644
--- a/src/features/admin/index.ts
+++ b/src/features/admin/index.ts
@@ -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';
\ No newline at end of file
diff --git a/src/features/players/server.ts b/src/features/players/server.ts
index 0356aae..ccce0d6 100644
--- a/src/features/players/server.ts
+++ b/src/features/players/server.ts
@@ -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 {
diff --git a/src/features/admin/components/create-tournament.tsx b/src/features/tournaments/components/tournament-form.tsx
similarity index 60%
rename from src/features/admin/components/create-tournament.tsx
rename to src/features/tournaments/components/tournament-form.tsx
index 115bfcc..fba9cb9 100644
--- a/src/features/admin/components/create-tournament.tsx
+++ b/src/features/tournaments/components/tournament-form.tsx
@@ -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;
+ tournamentId?: string;
+}
+
+const TournamentForm = ({ close, initialValues, tournamentId }: TournamentFormProps) => {
+ const isEditMode = !!tournamentId;
const config: UseFormInput = {
- 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 }) => {
@@ -97,16 +124,17 @@ const CreateTournament = ({ close }: { close: () => void }) => {
{...form.getInputProps('location')}
/>
-
}
{...form.getInputProps('logo')}
/>
@@ -146,9 +174,28 @@ const CreateTournament = ({ close }: { close: () => void }) => {
hour12: true
})}
/>
+
+ {isEditMode && (
+ date ? new Date(date).toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: true
+ }) : 'Not set'}
+ />
+ )}
);
};
-export default CreateTournament;
\ No newline at end of file
+export default TournamentForm;
\ No newline at end of file
diff --git a/src/features/admin/hooks/use-create-tournament.ts b/src/features/tournaments/hooks/use-create-tournament.ts
similarity index 100%
rename from src/features/admin/hooks/use-create-tournament.ts
rename to src/features/tournaments/hooks/use-create-tournament.ts
diff --git a/src/features/tournaments/hooks/use-update-tournament.ts b/src/features/tournaments/hooks/use-update-tournament.ts
new file mode 100644
index 0000000..5bd202d
--- /dev/null
+++ b/src/features/tournaments/hooks/use-update-tournament.ts
@@ -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) =>
+ updateTournament({ data: { id: tournamentId, updates: data } }),
+ });
+};
+
+export default useUpdateTournament;
\ No newline at end of file
diff --git a/src/utils/supertokens.ts b/src/utils/supertokens.ts
index c5e9d99..620b130 100644
--- a/src/utils/supertokens.ts
+++ b/src/utils/supertokens.ts
@@ -114,7 +114,7 @@ export const setUserMetadata = createServerFn({ method: 'POST' })
}).partial())
.middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => {
- const { userAuthId, metadata } = context as any;
+ const { userAuthId, metadata } = context;
if (!userAuthId) return;
await UserMetadata.updateUserMetadata(userAuthId, {
@@ -135,7 +135,7 @@ export const updateUserColorScheme = createServerFn({ method: 'POST' })
.validator((data: string) => data)
.middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => {
- const { userAuthId, metadata } = context as any;
+ const { userAuthId, metadata } = context;
if (!userAuthId) return;
await UserMetadata.updateUserMetadata(userAuthId, {
@@ -154,7 +154,7 @@ export const updateUserAccentColor = createServerFn({ method: 'POST' })
.validator((data: string) => data)
.middleware([superTokensFunctionMiddleware])
.handler(async ({ context, data }) => {
- const { userAuthId, metadata } = context as any;
+ const { userAuthId, metadata } = context;
if (!userAuthId) return;
await UserMetadata.updateUserMetadata(userAuthId, {
diff --git a/src/utils/test-event.ts b/src/utils/test-event.ts
index 6891d97..b37ce24 100644
--- a/src/utils/test-event.ts
+++ b/src/utils/test-event.ts
@@ -7,6 +7,6 @@ export const testEvent = createServerFn()
.handler(async ({ context }) => {
serverEvents.emit('test', {
type: 'test',
- userId: (context as any).userAuthId,
+ userId: context.userAuthId,
});
});
diff --git a/tsconfig.json b/tsconfig.json
index fd5ec2f..9064ffe 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
- "strict": false,
+ "strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"jsx": "react-jsx",