tournament logo upload via api
This commit is contained in:
48
pb_migrations/1756058328_updated_tournaments.js
Normal file
48
pb_migrations/1756058328_updated_tournaments.js
Normal 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)
|
||||
})
|
||||
45
pb_migrations/1756058447_updated_teams.js
Normal file
45
pb_migrations/1756058447_updated_teams.js
Normal 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)
|
||||
})
|
||||
@@ -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 AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||
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 ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||
|
||||
const rootServerRouteImport = createServerRootRoute()
|
||||
|
||||
@@ -94,6 +96,12 @@ const ApiTestServerRoute = ApiTestServerRouteImport.update({
|
||||
path: '/api/test',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiTournamentsUploadLogoServerRoute =
|
||||
ApiTournamentsUploadLogoServerRouteImport.update({
|
||||
id: '/api/tournaments/upload-logo',
|
||||
path: '/api/tournaments/upload-logo',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
|
||||
id: '/api/events/$',
|
||||
path: '/api/events/$',
|
||||
@@ -104,6 +112,12 @@ const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
|
||||
path: '/api/auth/$',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} 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 {
|
||||
'/login': typeof LoginRoute
|
||||
@@ -196,30 +210,54 @@ export interface FileServerRoutesByFullPath {
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRoutesByTo {
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRoutesById {
|
||||
__root__: typeof rootServerRouteImport
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRouteTypes {
|
||||
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
|
||||
to: '/api/test' | '/api/auth/$' | '/api/events/$'
|
||||
id: '__root__' | '/api/test' | '/api/auth/$' | '/api/events/$'
|
||||
to:
|
||||
| '/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
|
||||
}
|
||||
export interface RootServerRouteChildren {
|
||||
ApiTestServerRoute: typeof ApiTestServerRoute
|
||||
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
|
||||
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
|
||||
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
|
||||
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -319,6 +357,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiTestServerRouteImport
|
||||
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/$': {
|
||||
id: '/api/events/$'
|
||||
path: '/api/events/$'
|
||||
@@ -333,6 +378,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
|
||||
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,
|
||||
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
|
||||
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
|
||||
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
|
||||
ApiFilesCollectionRecordIdFileServerRoute:
|
||||
ApiFilesCollectionRecordIdFileServerRoute,
|
||||
}
|
||||
export const serverRouteTree = rootServerRouteImport
|
||||
._addFileChildren(rootServerRouteChildren)
|
||||
|
||||
95
src/app/routes/api/files/$collection/$recordId/$file.ts
Normal file
95
src/app/routes/api/files/$collection/$recordId/$file.ts
Normal 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',
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
115
src/app/routes/api/tournaments/upload-logo.ts
Normal file
115
src/app/routes/api/tournaments/upload-logo.ts
Normal 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' }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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 { LinkIcon } from "@phosphor-icons/react";
|
||||
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 { isNotEmpty } from "@mantine/form";
|
||||
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 }) => {
|
||||
|
||||
@@ -14,7 +18,6 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
name: 'Test Tournament',
|
||||
location: 'Test Location',
|
||||
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',
|
||||
enroll_time: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
@@ -28,12 +31,45 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
}
|
||||
|
||||
const form = useForm(config);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createTournament, isPending } = useCreateTournament();
|
||||
|
||||
const handleSubmit = async (values: TournamentFormInput) => {
|
||||
createTournament(values, {
|
||||
onSuccess: () => {
|
||||
const { logo, ...tournamentData } = values;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -67,12 +103,12 @@ const CreateTournament = ({ close }: { close: () => void }) => {
|
||||
{...form.getInputProps('desc')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
key={form.key('logo_url')}
|
||||
accept="image/*"
|
||||
<FileInput
|
||||
key={form.key('logo')}
|
||||
accept="image/png,image/jpeg,image/gif,image/jpg"
|
||||
label="Logo"
|
||||
leftSection={<LinkIcon size={16} />}
|
||||
{...form.getInputProps('logo_url')}
|
||||
{...form.getInputProps('logo')}
|
||||
/>
|
||||
|
||||
<SlidePanelField
|
||||
|
||||
@@ -18,7 +18,6 @@ const useCreateTournament = () => {
|
||||
toast.error('There was an issue creating your tournament. Please try again later.');
|
||||
logger.error('Error creating tournament', data);
|
||||
} else {
|
||||
toast.success('Tournament created successfully!');
|
||||
logger.info('Tournament created successfully', data);
|
||||
navigate({ to: '/tournaments' });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const Profile = ({ player }: ProfileProps) => {
|
||||
const tabs = [
|
||||
{
|
||||
label: "Overview",
|
||||
content: <Text p="md">Panel 1 content</Text>
|
||||
content: <Text p="md">Stats/Badges will go here</Text>
|
||||
},
|
||||
{
|
||||
label: "Teams",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
logo_url: string;
|
||||
logo: string;
|
||||
primary_color: string;
|
||||
accent_color: string;
|
||||
song_id: string;
|
||||
@@ -22,7 +22,7 @@ export interface Team {
|
||||
|
||||
export const teamInputSchema = z.object({
|
||||
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(),
|
||||
accent_color: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be valid hex color (#FF0000)").optional(),
|
||||
song_id: z.string().max(255).optional(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Badge, Card, Text, Image, Stack, Flex } from "@mantine/core"
|
||||
import { Tournament } from "@/features/tournaments/types"
|
||||
import { useMemo } from "react"
|
||||
import { CaretRightIcon } from "@phosphor-icons/react"
|
||||
import { CaretRightIcon, TrophyIcon } from "@phosphor-icons/react"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
|
||||
interface TournamentCardProps {
|
||||
@@ -26,11 +26,12 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
|
||||
<Stack>
|
||||
<Flex align='center' gap='md'>
|
||||
<Image
|
||||
src={tournament.logo_url}
|
||||
src={tournament.logo ? `/api/files/tournaments/${tournament.id}/${tournament.logo}` : undefined}
|
||||
maw={100}
|
||||
mah={100}
|
||||
fit='contain'
|
||||
alt={tournament.name}
|
||||
fallbackSrc={"TODO"}
|
||||
/>
|
||||
<Stack ta='center' mx='auto' gap='0'>
|
||||
<Text size='lg' fw={800}>{tournament.name} <CaretRightIcon size={12} weight='bold' /></Text>
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface Tournament {
|
||||
location?: string;
|
||||
desc?: string;
|
||||
rules?: string;
|
||||
logo_url?: string;
|
||||
logo?: string;
|
||||
enroll_time?: string;
|
||||
start_time: string;
|
||||
end_time?: string;
|
||||
@@ -22,7 +22,7 @@ export const tournamentFormSchema = z.object({
|
||||
location: z.string().optional(),
|
||||
desc: z.string().optional(),
|
||||
rules: z.string().optional(),
|
||||
logo_url: z.string().optional(),
|
||||
logo: z.file().optional(),
|
||||
enroll_time: z.string(),
|
||||
start_time: z.string(),
|
||||
end_time: z.string().optional(),
|
||||
@@ -34,7 +34,7 @@ export const tournamentInputSchema = z.object({
|
||||
location: z.string().optional(),
|
||||
desc: z.string().optional(),
|
||||
rules: z.string().optional(),
|
||||
logo_url: z.string().optional(),
|
||||
logo: z.file().optional(),
|
||||
enroll_time: z.string(),
|
||||
start_time: z.string(),
|
||||
end_time: z.string().optional(),
|
||||
|
||||
@@ -24,7 +24,7 @@ export function createTournamentsService(pb: PocketBase) {
|
||||
const result = await pb
|
||||
.collection("tournaments")
|
||||
.getFullList<Tournament>({
|
||||
fields: "id,name,start_time,end_time,logo_url,created",
|
||||
fields: "id,name,start_time,end_time,logo,created",
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export function transformTeam(record: any): Team {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
logo_url: record.logo_url,
|
||||
logo: record.logo,
|
||||
primary_color: record.primary_color,
|
||||
accent_color: record.accent_color,
|
||||
song_id: record.song_id,
|
||||
@@ -67,7 +67,7 @@ export function transformTournament(record: any): Tournament {
|
||||
location: record.location,
|
||||
desc: record.desc,
|
||||
rules: record.rules,
|
||||
logo_url: record.logo_url,
|
||||
logo: record.logo,
|
||||
enroll_time: record.enroll_time,
|
||||
start_time: record.start_time,
|
||||
end_time: record.end_time,
|
||||
|
||||
Reference in New Issue
Block a user