tournament logo upload via api

This commit is contained in:
yohlo
2025-08-24 13:50:48 -05:00
parent 466a3365f0
commit 936ab0ce72
13 changed files with 417 additions and 23 deletions

View File

@@ -0,0 +1,48 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_340646327")
// remove field
collection.fields.removeById("text156371623")
// add field
collection.fields.addAt(9, new Field({
"hidden": false,
"id": "file3834550803",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [],
"name": "logo",
"presentable": false,
"protected": false,
"required": false,
"system": false,
"thumbs": [],
"type": "file"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_340646327")
// add field
collection.fields.addAt(9, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text156371623",
"max": 0,
"min": 0,
"name": "logo_url",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// remove field
collection.fields.removeById("file3834550803")
return app.save(collection)
})

View File

@@ -0,0 +1,45 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1568971955")
// remove field
collection.fields.removeById("url156371623")
// add field
collection.fields.addAt(14, new Field({
"hidden": false,
"id": "file3834550803",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [],
"name": "logo",
"presentable": false,
"protected": false,
"required": false,
"system": false,
"thumbs": [],
"type": "file"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1568971955")
// add field
collection.fields.addAt(2, new Field({
"exceptDomains": null,
"hidden": false,
"id": "url156371623",
"name": "logo_url",
"onlyDomains": null,
"presentable": false,
"required": false,
"system": false,
"type": "url"
}))
// remove field
collection.fields.removeById("file3834550803")
return app.save(collection)
})

View File

@@ -24,8 +24,10 @@ import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$t
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId' import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview' import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
import { 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 ApiEventsSplatServerRouteImport } from './routes/api/events.$' import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$' import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
const rootServerRouteImport = createServerRootRoute() const rootServerRouteImport = createServerRootRoute()
@@ -94,6 +96,12 @@ const ApiTestServerRoute = ApiTestServerRouteImport.update({
path: '/api/test', path: '/api/test',
getParentRoute: () => rootServerRouteImport, getParentRoute: () => rootServerRouteImport,
} as any) } as any)
const ApiTournamentsUploadLogoServerRoute =
ApiTournamentsUploadLogoServerRouteImport.update({
id: '/api/tournaments/upload-logo',
path: '/api/tournaments/upload-logo',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({ const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
id: '/api/events/$', id: '/api/events/$',
path: '/api/events/$', path: '/api/events/$',
@@ -104,6 +112,12 @@ const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
path: '/api/auth/$', path: '/api/auth/$',
getParentRoute: () => rootServerRouteImport, getParentRoute: () => rootServerRouteImport,
} as any) } as any)
const ApiFilesCollectionRecordIdFileServerRoute =
ApiFilesCollectionRecordIdFileServerRouteImport.update({
id: '/api/files/$collection/$recordId/$file',
path: '/api/files/$collection/$recordId/$file',
getParentRoute: () => rootServerRouteImport,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/login': typeof LoginRoute '/login': typeof LoginRoute
@@ -196,30 +210,54 @@ export interface FileServerRoutesByFullPath {
'/api/test': typeof ApiTestServerRoute '/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute '/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute '/api/events/$': typeof ApiEventsSplatServerRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
} }
export interface FileServerRoutesByTo { export interface FileServerRoutesByTo {
'/api/test': typeof ApiTestServerRoute '/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute '/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute '/api/events/$': typeof ApiEventsSplatServerRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
} }
export interface FileServerRoutesById { export interface FileServerRoutesById {
__root__: typeof rootServerRouteImport __root__: typeof rootServerRouteImport
'/api/test': typeof ApiTestServerRoute '/api/test': typeof ApiTestServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute '/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/events/$': typeof ApiEventsSplatServerRoute '/api/events/$': typeof ApiEventsSplatServerRoute
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
} }
export interface FileServerRouteTypes { export interface FileServerRouteTypes {
fileServerRoutesByFullPath: FileServerRoutesByFullPath fileServerRoutesByFullPath: FileServerRoutesByFullPath
fullPaths: '/api/test' | '/api/auth/$' | '/api/events/$' fullPaths:
| '/api/test'
| '/api/auth/$'
| '/api/events/$'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
fileServerRoutesByTo: FileServerRoutesByTo fileServerRoutesByTo: FileServerRoutesByTo
to: '/api/test' | '/api/auth/$' | '/api/events/$' to:
id: '__root__' | '/api/test' | '/api/auth/$' | '/api/events/$' | '/api/test'
| '/api/auth/$'
| '/api/events/$'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
id:
| '__root__'
| '/api/test'
| '/api/auth/$'
| '/api/events/$'
| '/api/tournaments/upload-logo'
| '/api/files/$collection/$recordId/$file'
fileServerRoutesById: FileServerRoutesById fileServerRoutesById: FileServerRoutesById
} }
export interface RootServerRouteChildren { export interface RootServerRouteChildren {
ApiTestServerRoute: typeof ApiTestServerRoute ApiTestServerRoute: typeof ApiTestServerRoute
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -319,6 +357,13 @@ declare module '@tanstack/react-start/server' {
preLoaderRoute: typeof ApiTestServerRouteImport preLoaderRoute: typeof ApiTestServerRouteImport
parentRoute: typeof rootServerRouteImport parentRoute: typeof rootServerRouteImport
} }
'/api/tournaments/upload-logo': {
id: '/api/tournaments/upload-logo'
path: '/api/tournaments/upload-logo'
fullPath: '/api/tournaments/upload-logo'
preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/events/$': { '/api/events/$': {
id: '/api/events/$' id: '/api/events/$'
path: '/api/events/$' path: '/api/events/$'
@@ -333,6 +378,13 @@ declare module '@tanstack/react-start/server' {
preLoaderRoute: typeof ApiAuthSplatServerRouteImport preLoaderRoute: typeof ApiAuthSplatServerRouteImport
parentRoute: typeof rootServerRouteImport parentRoute: typeof rootServerRouteImport
} }
'/api/files/$collection/$recordId/$file': {
id: '/api/files/$collection/$recordId/$file'
path: '/api/files/$collection/$recordId/$file'
fullPath: '/api/files/$collection/$recordId/$file'
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileServerRouteImport
parentRoute: typeof rootServerRouteImport
}
} }
} }
@@ -385,6 +437,9 @@ const rootServerRouteChildren: RootServerRouteChildren = {
ApiTestServerRoute: ApiTestServerRoute, ApiTestServerRoute: ApiTestServerRoute,
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute, ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute, ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
ApiFilesCollectionRecordIdFileServerRoute:
ApiFilesCollectionRecordIdFileServerRoute,
} }
export const serverRouteTree = rootServerRouteImport export const serverRouteTree = rootServerRouteImport
._addFileChildren(rootServerRouteChildren) ._addFileChildren(rootServerRouteChildren)

View File

@@ -0,0 +1,95 @@
import { createServerFileRoute } from "@tanstack/react-start/server";
import { logger } from "@/lib/logger";
export const ServerRoute = createServerFileRoute("/api/files/$collection/$recordId/$file").methods({
GET: async ({ params, request }) => {
try {
const { collection, recordId, file } = params;
const pocketbaseUrl = process.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`;
logger.info('File proxy', {
collection,
recordId,
file,
targetUrl: fileUrl
});
const response = await fetch(fileUrl, {
method: 'GET',
headers: {
...(request.headers.get('range') && { 'Range': request.headers.get('range')! }),
...(request.headers.get('if-none-match') && { 'If-None-Match': request.headers.get('if-none-match')! }),
...(request.headers.get('if-modified-since') && { 'If-Modified-Since': request.headers.get('if-modified-since')! }),
},
});
if (!response.ok) {
logger.error('PocketBase file request failed', {
status: response.status,
statusText: response.statusText,
url: fileUrl
});
if (response.status === 404) {
return new Response('File not found', { status: 404 });
}
return new Response(`PocketBase error: ${response.statusText}`, {
status: response.status
});
}
const body = response.body;
const responseHeaders = new Headers();
const headers = [
'content-type',
'content-length',
'content-disposition',
'etag',
'last-modified',
'cache-control',
'accept-ranges',
'content-range'
];
headers.forEach(header => {
const value = response.headers.get(header);
if (value) {
responseHeaders.set(header, value);
}
});
responseHeaders.set('Access-Control-Allow-Origin', '*');
responseHeaders.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
responseHeaders.set('Access-Control-Allow-Headers', 'Range, If-None-Match, If-Modified-Since');
logger.info('File proxy response', {
status: response.status,
contentType: response.headers.get('content-type'),
contentLength: response.headers.get('content-length')
});
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders
});
} catch (error) {
logger.error('File proxy error', error);
return new Response('Internal server error', { status: 500 });
}
},
OPTIONS: () => {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Max-Age': '86400',
}
});
}
});

View File

@@ -0,0 +1,115 @@
import { createServerFileRoute } from '@tanstack/react-start/server';
import { superTokensRequestMiddleware } from '@/utils/supertokens';
import { pbAdmin } from '@/lib/pocketbase/client';
import { logger } from '@/lib/logger';
import { z } from 'zod';
const uploadSchema = z.object({
tournamentId: z.string().min(1, 'Tournament ID is required'),
});
export const ServerRoute = createServerFileRoute('/api/tournaments/upload-logo')
.middleware([superTokensRequestMiddleware])
.methods({
POST: async ({ request, context }) => {
try {
const userId = context.userAuthId;
const isAdmin = context.roles.includes("Admin");
if (!userId) return new Response('Unauthenticated', { status: 401 });
if (!isAdmin) return new Response('Unauthorized', { status: 403 });
const formData = await request.formData();
const tournamentId = formData.get('tournamentId') as string;
const logoFile = formData.get('logo') as File;
const validationResult = uploadSchema.safeParse({ tournamentId });
if (!validationResult.success) {
return new Response(JSON.stringify({
error: 'Invalid input',
details: validationResult.error.issues
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
if (!logoFile || logoFile.size === 0) {
return new Response(JSON.stringify({
error: 'Logo file is required'
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(logoFile.type)) {
return new Response(JSON.stringify({
error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.'
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const maxSize = 10 * 1024 * 1024;
if (logoFile.size > maxSize) {
return new Response(JSON.stringify({
error: 'File too large. Maximum size is 10MB.'
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const tournament = await pbAdmin.getTournament(tournamentId);
if (!tournament) {
return new Response(JSON.stringify({
error: 'Tournament not found'
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
logger.info('Uploading tournament logo', {
tournamentId,
fileName: logoFile.name,
fileSize: logoFile.size,
userId
});
const pbFormData = new FormData();
pbFormData.append('logo', logoFile);
const updatedTournament = await pbAdmin.updateTournament(tournamentId, pbFormData as any);
logger.info('Tournament logo uploaded successfully', {
tournamentId,
logo: updatedTournament.logo
});
return new Response(JSON.stringify({
success: true,
tournament: updatedTournament,
message: 'Logo uploaded successfully'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
logger.error('Error uploading tournament logo:', error);
return new Response(JSON.stringify({
error: 'Failed to upload logo',
message: error.message || 'Unknown error occurred'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
});

View File

@@ -1,4 +1,4 @@
import { Stack, TextInput, Textarea } from "@mantine/core"; import { FileInput, Stack, TextInput, Textarea } from "@mantine/core";
import { useForm, UseFormInput } from "@mantine/form"; import { useForm, UseFormInput } from "@mantine/form";
import { LinkIcon } from "@phosphor-icons/react"; import { LinkIcon } from "@phosphor-icons/react";
import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel"; import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel";
@@ -6,6 +6,10 @@ import { TournamentFormInput } from "@/features/tournaments/types";
import { DateTimePicker } from "./date-time-picker"; import { DateTimePicker } from "./date-time-picker";
import { isNotEmpty } from "@mantine/form"; import { isNotEmpty } from "@mantine/form";
import useCreateTournament from "../hooks/use-create-tournament"; import useCreateTournament from "../hooks/use-create-tournament";
import toast from '@/lib/sonner';
import { logger } from "..";
import { useQueryClient } from "@tanstack/react-query";
import { tournamentQueries } from "@/features/tournaments/queries";
const CreateTournament = ({ close }: { close: () => void }) => { const CreateTournament = ({ close }: { close: () => void }) => {
@@ -14,7 +18,6 @@ const CreateTournament = ({ close }: { close: () => void }) => {
name: 'Test Tournament', name: 'Test Tournament',
location: 'Test Location', location: 'Test Location',
desc: 'Test Description', desc: 'Test Description',
logo_url: 'https://en.wikipedia.org/wiki/Trophy#/media/File:1934_Melbourne_Cup,_National_Museum_of_Australia.jpg',
start_time: '2025-01-01T00:00:00Z', start_time: '2025-01-01T00:00:00Z',
enroll_time: '2025-01-01T00:00:00Z', enroll_time: '2025-01-01T00:00:00Z',
}, },
@@ -28,12 +31,45 @@ const CreateTournament = ({ close }: { close: () => void }) => {
} }
const form = useForm(config); const form = useForm(config);
const queryClient = useQueryClient();
const { mutate: createTournament, isPending } = useCreateTournament(); const { mutate: createTournament, isPending } = useCreateTournament();
const handleSubmit = async (values: TournamentFormInput) => { const handleSubmit = async (values: TournamentFormInput) => {
createTournament(values, { const { logo, ...tournamentData } = values;
onSuccess: () => {
createTournament(tournamentData, {
onSuccess: async (tournament) => {
if (logo && tournament) {
try {
const formData = new FormData();
formData.append('tournamentId', tournament.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.invalidateQueries({ queryKey: tournamentQueries.list().queryKey });
queryClient.setQueryData(
tournamentQueries.details(result.tournament!.id).queryKey,
result.tournament
);
toast.success('Tournament created successfully!');
} catch (error: any) {
toast.error(`Tournament created but logo upload failed: ${error.message}`);
logger.error('Tournament logo upload error', error);
}
}
close(); close();
} }
}); });
@@ -67,12 +103,12 @@ const CreateTournament = ({ close }: { close: () => void }) => {
{...form.getInputProps('desc')} {...form.getInputProps('desc')}
/> />
<TextInput <FileInput
key={form.key('logo_url')} key={form.key('logo')}
accept="image/*" accept="image/png,image/jpeg,image/gif,image/jpg"
label="Logo" label="Logo"
leftSection={<LinkIcon size={16} />} leftSection={<LinkIcon size={16} />}
{...form.getInputProps('logo_url')} {...form.getInputProps('logo')}
/> />
<SlidePanelField <SlidePanelField

View File

@@ -18,7 +18,6 @@ const useCreateTournament = () => {
toast.error('There was an issue creating your tournament. Please try again later.'); toast.error('There was an issue creating your tournament. Please try again later.');
logger.error('Error creating tournament', data); logger.error('Error creating tournament', data);
} else { } else {
toast.success('Tournament created successfully!');
logger.info('Tournament created successfully', data); logger.info('Tournament created successfully', data);
navigate({ to: '/tournaments' }); navigate({ to: '/tournaments' });
} }

View File

@@ -13,7 +13,7 @@ const Profile = ({ player }: ProfileProps) => {
const tabs = [ const tabs = [
{ {
label: "Overview", label: "Overview",
content: <Text p="md">Panel 1 content</Text> content: <Text p="md">Stats/Badges will go here</Text>
}, },
{ {
label: "Teams", label: "Teams",

View File

@@ -4,7 +4,7 @@ import { z } from 'zod';
export interface Team { export interface Team {
id: string; id: string;
name: string; name: string;
logo_url: string; logo: string;
primary_color: string; primary_color: string;
accent_color: string; accent_color: string;
song_id: string; song_id: string;
@@ -22,7 +22,7 @@ export interface Team {
export const teamInputSchema = z.object({ export const teamInputSchema = z.object({
name: z.string().min(1, "Team name is required").max(100, "Name too long"), name: z.string().min(1, "Team name is required").max(100, "Name too long"),
logo_url: z.url("Invalid logo URL").optional(), logo: z.file("Invalid logo").optional(),
primary_color: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be valid hex color (#FF0000)").optional(), primary_color: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be valid hex color (#FF0000)").optional(),
accent_color: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be valid hex color (#FF0000)").optional(), accent_color: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be valid hex color (#FF0000)").optional(),
song_id: z.string().max(255).optional(), song_id: z.string().max(255).optional(),

View File

@@ -1,7 +1,7 @@
import { Badge, Card, Text, Image, Stack, Flex } from "@mantine/core" import { Badge, Card, Text, Image, Stack, Flex } from "@mantine/core"
import { Tournament } from "@/features/tournaments/types" import { Tournament } from "@/features/tournaments/types"
import { useMemo } from "react" import { useMemo } from "react"
import { CaretRightIcon } from "@phosphor-icons/react" import { CaretRightIcon, TrophyIcon } from "@phosphor-icons/react"
import { useNavigate } from "@tanstack/react-router" import { useNavigate } from "@tanstack/react-router"
interface TournamentCardProps { interface TournamentCardProps {
@@ -26,11 +26,12 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
<Stack> <Stack>
<Flex align='center' gap='md'> <Flex align='center' gap='md'>
<Image <Image
src={tournament.logo_url} src={tournament.logo ? `/api/files/tournaments/${tournament.id}/${tournament.logo}` : undefined}
maw={100} maw={100}
mah={100} mah={100}
fit='contain' fit='contain'
alt={tournament.name} alt={tournament.name}
fallbackSrc={"TODO"}
/> />
<Stack ta='center' mx='auto' gap='0'> <Stack ta='center' mx='auto' gap='0'>
<Text size='lg' fw={800}>{tournament.name} <CaretRightIcon size={12} weight='bold' /></Text> <Text size='lg' fw={800}>{tournament.name} <CaretRightIcon size={12} weight='bold' /></Text>

View File

@@ -7,7 +7,7 @@ export interface Tournament {
location?: string; location?: string;
desc?: string; desc?: string;
rules?: string; rules?: string;
logo_url?: string; logo?: string;
enroll_time?: string; enroll_time?: string;
start_time: string; start_time: string;
end_time?: string; end_time?: string;
@@ -22,7 +22,7 @@ export const tournamentFormSchema = z.object({
location: z.string().optional(), location: z.string().optional(),
desc: z.string().optional(), desc: z.string().optional(),
rules: z.string().optional(), rules: z.string().optional(),
logo_url: z.string().optional(), logo: z.file().optional(),
enroll_time: z.string(), enroll_time: z.string(),
start_time: z.string(), start_time: z.string(),
end_time: z.string().optional(), end_time: z.string().optional(),
@@ -34,7 +34,7 @@ export const tournamentInputSchema = z.object({
location: z.string().optional(), location: z.string().optional(),
desc: z.string().optional(), desc: z.string().optional(),
rules: z.string().optional(), rules: z.string().optional(),
logo_url: z.string().optional(), logo: z.file().optional(),
enroll_time: z.string(), enroll_time: z.string(),
start_time: z.string(), start_time: z.string(),
end_time: z.string().optional(), end_time: z.string().optional(),

View File

@@ -24,7 +24,7 @@ export function createTournamentsService(pb: PocketBase) {
const result = await pb const result = await pb
.collection("tournaments") .collection("tournaments")
.getFullList<Tournament>({ .getFullList<Tournament>({
fields: "id,name,start_time,end_time,logo_url,created", fields: "id,name,start_time,end_time,logo,created",
sort: "-created", sort: "-created",
}); });

View File

@@ -36,7 +36,7 @@ export function transformTeam(record: any): Team {
return { return {
id: record.id, id: record.id,
name: record.name, name: record.name,
logo_url: record.logo_url, logo: record.logo,
primary_color: record.primary_color, primary_color: record.primary_color,
accent_color: record.accent_color, accent_color: record.accent_color,
song_id: record.song_id, song_id: record.song_id,
@@ -67,7 +67,7 @@ export function transformTournament(record: any): Tournament {
location: record.location, location: record.location,
desc: record.desc, desc: record.desc,
rules: record.rules, rules: record.rules,
logo_url: record.logo_url, logo: record.logo,
enroll_time: record.enroll_time, enroll_time: record.enroll_time,
start_time: record.start_time, start_time: record.start_time,
end_time: record.end_time, end_time: record.end_time,