From cde74a04d5629156a99bf45b14415f2f89ccf6d3 Mon Sep 17 00:00:00 2001 From: yohlo Date: Tue, 16 Sep 2025 09:24:21 -0500 Subject: [PATCH] work on team enrollment --- ...262_deleted_player_stats_per_tournament.js | 167 ++++++++++++++++++ .../1757950535_updated_team_stats.js | 96 ++++++++++ .../1757950649_created_team_stats.js | 157 ++++++++++++++++ src/app/routeTree.gen.ts | 22 +++ src/app/routes/__root.tsx | 7 - src/app/routes/_authed.tsx | 7 +- src/app/routes/_authed/profile.$playerId.tsx | 4 +- src/app/routes/_authed/teams.$teamId.tsx | 5 +- .../_authed/tournaments/$tournamentId.tsx | 2 +- src/app/routes/api/teams/upload-logo.ts | 116 ++++++++++++ src/components/page.tsx | 4 +- .../sheet/slide-panel/slide-panel-field.tsx | 4 +- .../sheet/slide-panel/slide-panel.tsx | 13 +- src/components/swipeable-tabs.tsx | 28 ++- src/contexts/auth-context.tsx | 71 ++++---- src/features/core/components/back-button.tsx | 6 +- src/features/core/components/header.tsx | 13 +- src/features/core/components/layout.tsx | 2 +- src/features/core/components/navbar.tsx | 3 +- src/features/core/components/pullable.tsx | 5 - .../core/components/settings-button.tsx | 6 +- .../matches/components/match-card.tsx | 69 ++++---- .../matches/components/match-list.tsx | 16 +- .../players/components/profile/header.tsx | 1 - .../players/components/profile/index.tsx | 20 +-- src/features/players/queries.ts | 12 +- src/features/players/server.ts | 9 +- .../components/team-form/color-picker.tsx | 80 +++++++++ .../{team-form.tsx => team-form/index.tsx} | 157 +++++++++------- .../components/team-form/players-picker.tsx | 62 +++++++ .../components/team-form/song-picker.tsx | 56 ++++++ .../team-form/team-color-picker.tsx | 77 -------- .../teams/components/team-profile/index.tsx | 8 +- src/features/teams/queries.ts | 17 +- src/features/teams/server.ts | 10 +- .../tournaments/components/profile/index.tsx | 6 +- .../enroll-free-agent.tsx | 2 +- .../enroll-team/index.tsx | 26 ++- .../enroll-team/team-selection-view.tsx | 0 .../components/upcoming-tournament/header.tsx | 135 +++++++------- .../components/upcoming-tournament/index.tsx | 25 ++- src/lib/pocketbase/services/players.ts | 59 ++++++- src/lib/pocketbase/services/teams.ts | 17 +- src/lib/supertokens/config.ts | 2 +- src/shared/components/stats-overview.tsx | 97 +++++----- 45 files changed, 1244 insertions(+), 457 deletions(-) create mode 100644 pb_migrations/1757910262_deleted_player_stats_per_tournament.js create mode 100644 pb_migrations/1757950535_updated_team_stats.js create mode 100644 pb_migrations/1757950649_created_team_stats.js create mode 100644 src/app/routes/api/teams/upload-logo.ts create mode 100644 src/features/teams/components/team-form/color-picker.tsx rename src/features/teams/components/{team-form.tsx => team-form/index.tsx} (57%) create mode 100644 src/features/teams/components/team-form/players-picker.tsx create mode 100644 src/features/teams/components/team-form/song-picker.tsx delete mode 100644 src/features/teams/components/team-form/team-color-picker.tsx rename src/features/tournaments/components/{ => upcoming-tournament}/enroll-free-agent.tsx (94%) rename src/features/tournaments/components/{ => upcoming-tournament}/enroll-team/index.tsx (72%) rename src/features/tournaments/components/{ => upcoming-tournament}/enroll-team/team-selection-view.tsx (100%) diff --git a/pb_migrations/1757910262_deleted_player_stats_per_tournament.js b/pb_migrations/1757910262_deleted_player_stats_per_tournament.js new file mode 100644 index 0000000..3619146 --- /dev/null +++ b/pb_migrations/1757910262_deleted_player_stats_per_tournament.js @@ -0,0 +1,167 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_135889471"); + + return app.delete(collection); +}, (app) => { + const collection = new Collection({ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3208210256", + "max": 0, + "min": 0, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3072146508", + "hidden": false, + "id": "relation2582050271", + "maxSelect": 1, + "minSelect": 0, + "name": "player_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json4231605813", + "maxSize": 1, + "name": "player_name", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation694999214", + "maxSelect": 1, + "minSelect": 0, + "name": "team_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_XGbN", + "max": 0, + "min": 0, + "name": "team_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_340646327", + "hidden": false, + "id": "relation869376999", + "maxSelect": 1, + "minSelect": 0, + "name": "tournament_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_uud6", + "max": 0, + "min": 0, + "name": "tournament_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number103159226", + "max": null, + "min": null, + "name": "matches", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json2732118329", + "maxSize": 1, + "name": "wins", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json724428801", + "maxSize": 1, + "name": "losses", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3154249934", + "maxSize": 1, + "name": "total_cups_made", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3227208027", + "maxSize": 1, + "name": "total_cups_against", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "id": "pbc_135889471", + "indexes": [], + "listRule": null, + "name": "player_stats_per_tournament", + "system": false, + "type": "view", + "updateRule": null, + "viewQuery": "SELECT\n (p.id || '_' || t.id || '_' || tour.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n t.id as team_id,\n t.name as team_name,\n tour.id as tournament_id,\n tour.name as tournament_name,\n COUNT(m.id) as matches,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id, t.id, tour.id", + "viewRule": null + }); + + return app.save(collection); +}) diff --git a/pb_migrations/1757950535_updated_team_stats.js b/pb_migrations/1757950535_updated_team_stats.js new file mode 100644 index 0000000..ad9968f --- /dev/null +++ b/pb_migrations/1757950535_updated_team_stats.js @@ -0,0 +1,96 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_135889472") + + // update collection data + unmarshal({ + "name": "team_stats_per_tournament" + }, collection) + + // remove field + collection.fields.removeById("_clone_2Mic") + + // remove field + collection.fields.removeById("_clone_C8ev") + + // add field + collection.fields.addAt(2, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_QmWG", + "max": 0, + "min": 0, + "name": "team_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(4, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_1o7N", + "max": 0, + "min": 0, + "name": "tournament_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_135889472") + + // update collection data + unmarshal({ + "name": "team_stats" + }, collection) + + // add field + collection.fields.addAt(2, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_2Mic", + "max": 0, + "min": 0, + "name": "team_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // add field + collection.fields.addAt(4, new Field({ + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_C8ev", + "max": 0, + "min": 0, + "name": "tournament_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + })) + + // remove field + collection.fields.removeById("_clone_QmWG") + + // remove field + collection.fields.removeById("_clone_1o7N") + + return app.save(collection) +}) diff --git a/pb_migrations/1757950649_created_team_stats.js b/pb_migrations/1757950649_created_team_stats.js new file mode 100644 index 0000000..64d3464 --- /dev/null +++ b/pb_migrations/1757950649_created_team_stats.js @@ -0,0 +1,157 @@ +/// +migrate((app) => { + const collection = new Collection({ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3208210256", + "max": 0, + "min": 0, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation694999214", + "maxSelect": 1, + "minSelect": 0, + "name": "team_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "_clone_nYJn", + "max": 0, + "min": 0, + "name": "team_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number103159226", + "max": null, + "min": null, + "name": "matches", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json2732118329", + "maxSize": 1, + "name": "wins", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json724428801", + "maxSize": 1, + "name": "losses", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3041953980", + "maxSize": 1, + "name": "margin_of_victory", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json1531431708", + "maxSize": 1, + "name": "margin_of_loss", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json1062535948", + "maxSize": 1, + "name": "total_cups_won_by", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json4249694556", + "maxSize": 1, + "name": "total_cups_lost_by", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3154249934", + "maxSize": 1, + "name": "total_cups_made", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3227208027", + "maxSize": 1, + "name": "total_cups_against", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "id": "pbc_1582517110", + "indexes": [], + "listRule": null, + "name": "team_stats", + "system": false, + "type": "view", + "updateRule": null, + "viewQuery": "\n SELECT\n t.id as id,\n t.id as team_id,\n t.name as team_name,\n COUNT(m.id) as matches,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END) as margin_of_victory,\n AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END) as margin_of_loss,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_won_by,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_lost_by,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against\n FROM teams t\n JOIN matches m ON (m.home = t.id OR m.away = t.id)\n WHERE m.status = 'ended'\n GROUP BY t.id", + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1582517110"); + + return app.delete(collection); +}) diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts index 7608220..fe450ff 100644 --- a/src/app/routeTree.gen.ts +++ b/src/app/routeTree.gen.ts @@ -29,6 +29,7 @@ import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authe import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id' import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id' import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo' +import { ServerRoute as ApiTeamsUploadLogoServerRouteImport } from './routes/api/teams/upload-logo' import { ServerRoute as ApiSpotifyTokenServerRouteImport } from './routes/api/spotify/token' import { ServerRoute as ApiSpotifyResumeServerRouteImport } from './routes/api/spotify/resume' import { ServerRoute as ApiSpotifyPlaybackServerRouteImport } from './routes/api/spotify/playback' @@ -134,6 +135,12 @@ const ApiTournamentsUploadLogoServerRoute = path: '/api/tournaments/upload-logo', getParentRoute: () => rootServerRouteImport, } as any) +const ApiTeamsUploadLogoServerRoute = + ApiTeamsUploadLogoServerRouteImport.update({ + id: '/api/teams/upload-logo', + path: '/api/teams/upload-logo', + getParentRoute: () => rootServerRouteImport, + } as any) const ApiSpotifyTokenServerRoute = ApiSpotifyTokenServerRouteImport.update({ id: '/api/spotify/token', path: '/api/spotify/token', @@ -304,6 +311,7 @@ export interface FileServerRoutesByFullPath { '/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute '/api/spotify/resume': typeof ApiSpotifyResumeServerRoute '/api/spotify/token': typeof ApiSpotifyTokenServerRoute + '/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute '/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute } @@ -315,6 +323,7 @@ export interface FileServerRoutesByTo { '/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute '/api/spotify/resume': typeof ApiSpotifyResumeServerRoute '/api/spotify/token': typeof ApiSpotifyTokenServerRoute + '/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute '/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute } @@ -327,6 +336,7 @@ export interface FileServerRoutesById { '/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute '/api/spotify/resume': typeof ApiSpotifyResumeServerRoute '/api/spotify/token': typeof ApiSpotifyTokenServerRoute + '/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute '/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute '/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute } @@ -340,6 +350,7 @@ export interface FileServerRouteTypes { | '/api/spotify/playback' | '/api/spotify/resume' | '/api/spotify/token' + | '/api/teams/upload-logo' | '/api/tournaments/upload-logo' | '/api/files/$collection/$recordId/$file' fileServerRoutesByTo: FileServerRoutesByTo @@ -351,6 +362,7 @@ export interface FileServerRouteTypes { | '/api/spotify/playback' | '/api/spotify/resume' | '/api/spotify/token' + | '/api/teams/upload-logo' | '/api/tournaments/upload-logo' | '/api/files/$collection/$recordId/$file' id: @@ -362,6 +374,7 @@ export interface FileServerRouteTypes { | '/api/spotify/playback' | '/api/spotify/resume' | '/api/spotify/token' + | '/api/teams/upload-logo' | '/api/tournaments/upload-logo' | '/api/files/$collection/$recordId/$file' fileServerRoutesById: FileServerRoutesById @@ -374,6 +387,7 @@ export interface RootServerRouteChildren { ApiSpotifyPlaybackServerRoute: typeof ApiSpotifyPlaybackServerRoute ApiSpotifyResumeServerRoute: typeof ApiSpotifyResumeServerRoute ApiSpotifyTokenServerRoute: typeof ApiSpotifyTokenServerRoute + ApiTeamsUploadLogoServerRoute: typeof ApiTeamsUploadLogoServerRoute ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute } @@ -510,6 +524,13 @@ declare module '@tanstack/react-start/server' { preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport parentRoute: typeof rootServerRouteImport } + '/api/teams/upload-logo': { + id: '/api/teams/upload-logo' + path: '/api/teams/upload-logo' + fullPath: '/api/teams/upload-logo' + preLoaderRoute: typeof ApiTeamsUploadLogoServerRouteImport + parentRoute: typeof rootServerRouteImport + } '/api/spotify/token': { id: '/api/spotify/token' path: '/api/spotify/token' @@ -631,6 +652,7 @@ const rootServerRouteChildren: RootServerRouteChildren = { ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute, ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute, ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute, + ApiTeamsUploadLogoServerRoute: ApiTeamsUploadLogoServerRoute, ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute, ApiFilesCollectionRecordIdFileServerRoute: ApiFilesCollectionRecordIdFileServerRoute, diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 83ff2d1..0459715 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -20,7 +20,6 @@ import { playerQueries } from "@/features/players/queries"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; import FullScreenLoader from "@/components/full-screen-loader"; -import { scan } from "react-scan"; export const Route = createRootRouteWithContext<{ queryClient: QueryClient; @@ -106,12 +105,6 @@ function RootComponent() { // todo: analytics -> process.env data-website-id function RootDocument({ children }: { children: React.ReactNode }) { - React.useEffect(() => { - scan({ - enabled: true, - }); - }, []); - return ( { @@ -27,7 +26,9 @@ export const Route = createFileRoute("/_authed")({ }, pendingComponent: () => ( - + + + ), }); diff --git a/src/app/routes/_authed/profile.$playerId.tsx b/src/app/routes/_authed/profile.$playerId.tsx index b9b7319..f108e88 100644 --- a/src/app/routes/_authed/profile.$playerId.tsx +++ b/src/app/routes/_authed/profile.$playerId.tsx @@ -1,5 +1,5 @@ import Profile from "@/features/players/components/profile"; -import { playerQueries } from "@/features/players/queries"; +import { playerKeys, playerQueries } from "@/features/players/queries"; import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; import { createFileRoute } from "@tanstack/react-router"; import { z } from "zod"; @@ -31,7 +31,7 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({ context?.auth.user.id === params.playerId ? "/settings" : undefined, }, withPadding: false, - refresh: [playerQueries.details(params.playerId).queryKey], + refresh: [playerKeys.details(params.playerId), playerKeys.matches(params.playerId), playerKeys.stats(params.playerId)], }), component: () => { const { playerId } = Route.useParams(); diff --git a/src/app/routes/_authed/teams.$teamId.tsx b/src/app/routes/_authed/teams.$teamId.tsx index ff5cd4b..e4e796c 100644 --- a/src/app/routes/_authed/teams.$teamId.tsx +++ b/src/app/routes/_authed/teams.$teamId.tsx @@ -1,5 +1,5 @@ import TeamProfile from "@/features/teams/components/team-profile"; -import { teamQueries } from "@/features/teams/queries"; +import { teamKeys, teamQueries } from "@/features/teams/queries"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; import { redirect, createFileRoute } from "@tanstack/react-router"; @@ -20,7 +20,8 @@ export const Route = createFileRoute("/_authed/teams/$teamId")({ collapsed: true, withBackButton: true, }, - refresh: [teamQueries.details(params.teamId).queryKey], + refresh: [teamKeys.details(params.teamId), teamKeys.matches(params.teamId), teamKeys.stats(params.teamId)], + withPadding: false }), component: () => { const { teamId } = Route.useParams(); diff --git a/src/app/routes/_authed/tournaments/$tournamentId.tsx b/src/app/routes/_authed/tournaments/$tournamentId.tsx index c67eeff..6c3bb3e 100644 --- a/src/app/routes/_authed/tournaments/$tournamentId.tsx +++ b/src/app/routes/_authed/tournaments/$tournamentId.tsx @@ -20,7 +20,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({ withBackButton: true, settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined }, - refresh: tournamentQueries.details(params.tournamentId).queryKey, + refresh: [tournamentQueries.details(params.tournamentId).queryKey], withPadding: false }), component: RouteComponent, diff --git a/src/app/routes/api/teams/upload-logo.ts b/src/app/routes/api/teams/upload-logo.ts new file mode 100644 index 0000000..cc4be4d --- /dev/null +++ b/src/app/routes/api/teams/upload-logo.ts @@ -0,0 +1,116 @@ +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({ + teamId: z.string().min(1, 'Team ID is required'), +}); + +export const ServerRoute = createServerFileRoute('/api/teams/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 }); + + const formData = await request.formData(); + const teamId = formData.get('teamId') as string; + const logoFile = formData.get('logo') as File; + + const validationResult = uploadSchema.safeParse({ teamId }); + 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 team = await pbAdmin.getTeam(teamId); + if (!team) { + return new Response(JSON.stringify({ + error: 'Team not found' + }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } + + if (!team.players.map(p => p.id).includes(context.userId) && !isAdmin) + return new Response('Unauthorized', { status: 403 }); + + logger.info('Uploading team logo', { + teamId, + fileName: logoFile.name, + fileSize: logoFile.size, + userId + }); + + const pbFormData = new FormData(); + pbFormData.append('logo', logoFile); + + const updatedTeam= await pbAdmin.updateTeam(teamId, pbFormData as any); + + logger.info('Team logo uploaded successfully', { + teamId, + logo: updatedTeam.logo + }); + + return new Response(JSON.stringify({ + success: true, + team: updatedTeam, + message: 'Logo uploaded successfully' + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error: any) { + logger.error('Error uploading team 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' } + }); + } + } + }); \ No newline at end of file diff --git a/src/components/page.tsx b/src/components/page.tsx index 34639c5..b1f037c 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -22,10 +22,10 @@ const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => { {...props} > {header.collapsed && header.withBackButton && ( - + )} {header.collapsed && header.settingsLink && ( - + )} {children} diff --git a/src/components/sheet/slide-panel/slide-panel-field.tsx b/src/components/sheet/slide-panel/slide-panel-field.tsx index 0677a81..176d926 100644 --- a/src/components/sheet/slide-panel/slide-panel-field.tsx +++ b/src/components/sheet/slide-panel/slide-panel-field.tsx @@ -1,6 +1,6 @@ import { Box, Text, UnstyledButton, Flex, Stack } from "@mantine/core"; import { CaretRightIcon } from "@phosphor-icons/react"; -import { ComponentType, useContext } from "react"; +import React, { ComponentType, useContext } from "react"; import { SlidePanelContext } from "./slide-panel-context"; interface SlidePanelFieldProps { @@ -11,7 +11,7 @@ interface SlidePanelFieldProps { title: string; label?: string; placeholder?: string; - formatValue?: (value: any) => string; + formatValue?: (value: any) => string | React.ReactNode; componentProps?: Record; withAsterisk?: boolean; error?: string; diff --git a/src/components/sheet/slide-panel/slide-panel.tsx b/src/components/sheet/slide-panel/slide-panel.tsx index 2f7749a..c2f1f06 100644 --- a/src/components/sheet/slide-panel/slide-panel.tsx +++ b/src/components/sheet/slide-panel/slide-panel.tsx @@ -167,14 +167,11 @@ const SlidePanel = ({ bg="var(--mantine-color-dimmed)" my="xs" /> - - - - + )} diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx index 023c84e..e5c705b 100644 --- a/src/components/swipeable-tabs.tsx +++ b/src/components/swipeable-tabs.tsx @@ -86,7 +86,7 @@ function SwipeableTabs({ } }, [search?.tab]); - useEffect(() => { + const updateHeight = useCallback(() => { const activeSlideRef = slideRefs.current[activeTab]; if (activeSlideRef) { const height = activeSlideRef.scrollHeight; @@ -94,6 +94,32 @@ function SwipeableTabs({ } }, [activeTab]); + useEffect(() => { + updateHeight(); + }, [activeTab, updateHeight]); + + // Update height when content changes (after render) + useEffect(() => { + const timeoutId = setTimeout(updateHeight, 0); + return () => clearTimeout(timeoutId); + }); + + // Use ResizeObserver to watch for content size changes + useEffect(() => { + const activeSlideRef = slideRefs.current[activeTab]; + if (!activeSlideRef) return; + + const resizeObserver = new ResizeObserver(() => { + updateHeight(); + }); + + resizeObserver.observe(activeSlideRef); + + return () => { + resizeObserver.disconnect(); + }; + }, [activeTab, updateHeight]); + const setControlRef = useCallback( (index: number) => (node: HTMLSpanElement | null) => { controlsRefs.current[index] = node; diff --git a/src/contexts/auth-context.tsx b/src/contexts/auth-context.tsx index a8d2703..bf1a7df 100644 --- a/src/contexts/auth-context.tsx +++ b/src/contexts/auth-context.tsx @@ -1,10 +1,15 @@ -import { createContext, PropsWithChildren, useCallback, useContext, useMemo } from "react"; +import { + createContext, + PropsWithChildren, + useCallback, + useContext, + useMemo, +} from "react"; import { MantineColor, MantineColorScheme } from "@mantine/core"; import { useQueryClient } from "@tanstack/react-query"; import { Player } from "@/features/players/types"; import { playerKeys, playerQueries, useMe } from "@/features/players/queries"; - interface AuthData { user: Player | undefined; metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme }; @@ -13,9 +18,9 @@ interface AuthData { export const defaultAuthData: AuthData = { user: undefined, - metadata: { accentColor: 'blue', colorScheme: 'auto' }, + metadata: { accentColor: "blue", colorScheme: "auto" }, roles: [], -} +}; export interface AuthContextType extends AuthData { set: ({ user, metadata, roles }: Partial) => void; @@ -27,39 +32,45 @@ const AuthContext = createContext({ }); export const AuthProvider: React.FC = ({ children }) => { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); const { data } = useMe(); - - const set = useCallback((updates: Partial) => { - queryClient.setQueryData(playerKeys.auth, (oldData: AuthData | undefined) => { - const currentData = oldData || defaultAuthData; - return { - ...currentData, - ...updates, - metadata: updates.metadata - ? { ...currentData.metadata, ...updates.metadata } - : currentData.metadata - }; - }); - }, [queryClient]); - return ( - - {children} - - ) + const set = useCallback( + (updates: Partial) => { + queryClient.setQueryData( + playerKeys.auth, + (oldData: AuthData | undefined) => { + const currentData = oldData || defaultAuthData; + return { + ...currentData, + ...updates, + metadata: updates.metadata + ? { ...currentData.metadata, ...updates.metadata } + : currentData.metadata, + }; + } + ); + }, + [queryClient] + ); + + const value = useMemo( + () => ({ + user: data?.user || defaultAuthData.user, + metadata: data?.metadata || defaultAuthData.metadata, + roles: data?.roles || defaultAuthData.roles, + set, + }), + [data, defaultAuthData] + ); + + return {children}; }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } return context; }; diff --git a/src/features/core/components/back-button.tsx b/src/features/core/components/back-button.tsx index 6b09ad6..863d543 100644 --- a/src/features/core/components/back-button.tsx +++ b/src/features/core/components/back-button.tsx @@ -2,11 +2,7 @@ import { Box } from "@mantine/core" import { ArrowLeftIcon } from "@phosphor-icons/react" import { useRouter } from "@tanstack/react-router" -interface BackButtonProps { - offsetY: number; -} - -const BackButton = ({ offsetY }: BackButtonProps) => { +const BackButton = () => { const router = useRouter() return ( diff --git a/src/features/core/components/header.tsx b/src/features/core/components/header.tsx index 9cf8167..2c75931 100644 --- a/src/features/core/components/header.tsx +++ b/src/features/core/components/header.tsx @@ -1,16 +1,9 @@ import { Title, AppShell, Flex } from "@mantine/core"; import { HeaderConfig } from "../types/header-config"; -import BackButton from "./back-button"; -import { useMemo } from "react"; -import SettingsButton from "./settings-button"; -interface HeaderProps extends HeaderConfig { - scrollPosition: { x: number, y: number }; -} -const Header = ({ withBackButton, settingsLink, collapsed, title, scrollPosition }: HeaderProps) => { - const offsetY = useMemo(() => { - return collapsed ? scrollPosition.y : 0; - }, [collapsed, scrollPosition.y]); +interface HeaderProps extends HeaderConfig {} + +const Header = ({ collapsed, title }: HeaderProps) => { return ( diff --git a/src/features/core/components/layout.tsx b/src/features/core/components/layout.tsx index fec97ae..3d1cfcd 100644 --- a/src/features/core/components/layout.tsx +++ b/src/features/core/components/layout.tsx @@ -33,7 +33,7 @@ const Layout: React.FC = ({ children }) => { mah='100dvh' style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }} > -
+
{ const { user, roles } = useAuth() @@ -35,4 +36,4 @@ const Navbar = () => { } -export default Navbar; +export default memo(Navbar); diff --git a/src/features/core/components/pullable.tsx b/src/features/core/components/pullable.tsx index b38e951..e8ec509 100644 --- a/src/features/core/components/pullable.tsx +++ b/src/features/core/components/pullable.tsx @@ -14,7 +14,6 @@ interface PullableProps extends PropsWithChildren { /** * Pullable is a component that allows the user to pull down to refresh the page - * TODO: Need to make the router config nicer */ const Pullable: React.FC = ({ children, scrollPosition, onScrollPositionChange }) => { const height = useAppShellHeight(); @@ -110,10 +109,6 @@ const Pullable: React.FC = ({ children, scrollPosition, onScrollP pt={(scrolling || scrollY > 40) || !isRefreshing ? 0 : 40 - scrollY} > - { /* TODO: Remove this debug button */} - - - {children} diff --git a/src/features/core/components/settings-button.tsx b/src/features/core/components/settings-button.tsx index 08f4431..b4b6176 100644 --- a/src/features/core/components/settings-button.tsx +++ b/src/features/core/components/settings-button.tsx @@ -1,13 +1,13 @@ import { Box } from "@mantine/core" import { GearIcon } from "@phosphor-icons/react" import { useNavigate } from "@tanstack/react-router" +import { memo } from "react"; interface SettingButtonProps { - offsetY: number; to: string; } -const SettingsButton = ({ offsetY, to }: SettingButtonProps) => { +const SettingsButton = ({ to }: SettingButtonProps) => { const navigate = useNavigate(); return ( @@ -23,4 +23,4 @@ const SettingsButton = ({ offsetY, to }: SettingButtonProps) => { ); } -export default SettingsButton; +export default memo(SettingsButton, (prev, next) => prev.to !== next.to); diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx index acc3f17..4e41628 100644 --- a/src/features/matches/components/match-card.tsx +++ b/src/features/matches/components/match-card.tsx @@ -7,6 +7,7 @@ import { Indicator, Box, Badge, + Skeleton, } from "@mantine/core"; import { TrophyIcon, CrownIcon } from "@phosphor-icons/react"; import { useNavigate } from "@tanstack/react-router"; @@ -54,7 +55,7 @@ const MatchCard = ({ match }: MatchCardProps) => { - - Round {match.round} + Round {match.round + 1} {match.is_losers_bracket && " (Losers)"} @@ -81,7 +82,7 @@ const MatchCard = ({ match }: MatchCardProps) => { )} { {match.home?.name!} + + {match.home_cups} + + - - - {match.home_cups} - - - - - - - {match.away_cups} - - {match.ot_count > 0 && ( - - {match.ot_count}OT - - )} - - - - - {match.away?.name} - + + { style={{ position: "absolute", top: -10, - right: -4, - transform: "rotate(25deg)", + left: -4, + transform: "rotate(-25deg)", color: "gold", }} > @@ -144,7 +120,22 @@ const MatchCard = ({ match }: MatchCardProps) => { )} + + {match.away?.name} + + + {match.away_cups} + diff --git a/src/features/matches/components/match-list.tsx b/src/features/matches/components/match-list.tsx index fdc00ec..c81cb0a 100644 --- a/src/features/matches/components/match-list.tsx +++ b/src/features/matches/components/match-list.tsx @@ -1,5 +1,4 @@ -import { Stack, Text, ThemeIcon, Box } from "@mantine/core"; -import { TrophyIcon } from "@phosphor-icons/react"; +import { Stack } from "@mantine/core"; import { motion, AnimatePresence } from "framer-motion"; import { Match } from "../types"; import MatchCard from "./match-card"; @@ -14,20 +13,11 @@ const MatchList = ({ matches }: MatchListProps) => { ) || []; if (!filteredMatches.length) { - return ( - - - - - - No matches found - - - ); + return undefined; } return ( - + {filteredMatches.map((match, index) => ( { - const sheet = useSheet(); const { user: authUser } = useAuth(); diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index 2284843..44e38e7 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -1,12 +1,10 @@ import { Box } from "@mantine/core"; import Header from "./header"; -import { Player, PlayerStats } from "@/features/players/types"; import SwipeableTabs from "@/components/swipeable-tabs"; import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries"; import TeamList from "@/features/teams/components/team-list"; import StatsOverview from "@/shared/components/stats-overview"; import MatchList from "@/features/matches/components/match-list"; -import { BaseStats } from "@/shared/types/stats"; interface ProfileProps { id: string; @@ -17,26 +15,14 @@ const Profile = ({ id }: ProfileProps) => { const { data: matches } = usePlayerMatches(id); const { data: stats, isLoading: statsLoading } = usePlayerStats(id); - // Aggregate player stats from multiple tournaments into a single BaseStats object - const aggregatedStats: BaseStats | null = stats && stats.length > 0 ? { - id: `player_${id}_aggregate`, - matches: stats.reduce((acc, stat) => acc + stat.matches, 0), - wins: stats.reduce((acc, stat) => acc + stat.wins, 0), - losses: stats.reduce((acc, stat) => acc + stat.losses, 0), - total_cups_made: stats.reduce((acc, stat) => acc + stat.total_cups_made, 0), - total_cups_against: stats.reduce((acc, stat) => acc + stat.total_cups_against, 0), - margin_of_victory: stats.filter(s => s.margin_of_victory > 0).reduce((acc, stat, _, arr) => acc + stat.margin_of_victory / arr.length, 0), - margin_of_loss: stats.filter(s => s.margin_of_loss > 0).reduce((acc, stat, _, arr) => acc + stat.margin_of_loss / arr.length, 0), - } : null; - const tabs = [ { label: "Overview", - content: , + content: , }, { label: "Matches", - content: , + content: , }, { label: "Teams", @@ -47,7 +33,7 @@ const Profile = ({ id }: ProfileProps) => { return ( <>
- + diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts index a17819c..ee4170f 100644 --- a/src/features/players/queries.ts +++ b/src/features/players/queries.ts @@ -1,11 +1,12 @@ import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks"; -import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches } from "./server"; +import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers } from "./server"; export const playerKeys = { auth: ['auth'], list: ['players', 'list'], details: (id: string) => ['players', 'details', id], unassociated: ['players','unassociated'], + unenrolled: (tournamentId: string) => ['players', 'unenrolled', tournamentId], stats: (id: string) => ['players', 'stats', id], allStats: ['players', 'stats', 'all'], matches: (id: string) => ['players', 'matches', id], @@ -28,6 +29,10 @@ export const playerQueries = { queryKey: playerKeys.unassociated, queryFn: async () => await getUnassociatedPlayers() }), + unenrolled: (tournamentId: string) => ({ + queryKey: playerKeys.unenrolled(tournamentId), + queryFn: async () => await getUnenrolledPlayers({ data: tournamentId }) + }), stats: (id: string) => ({ queryKey: playerKeys.stats(id), queryFn: async () => await getPlayerStats({ data: id }) @@ -81,4 +86,7 @@ export const useAllPlayerStats = () => useServerSuspenseQuery(playerQueries.allStats()); export const usePlayerMatches = (id: string) => - useServerSuspenseQuery(playerQueries.matches(id)); \ No newline at end of file + useServerSuspenseQuery(playerQueries.matches(id)); + +export const useUnenrolledPlayers = (tournamentId: string) => + useServerSuspenseQuery(playerQueries.unenrolled(tournamentId)); \ No newline at end of file diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 785beca..4728925 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -125,7 +125,7 @@ export const getPlayerStats = createServerFn() .validator(z.string()) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => - toServerResult(async () => await pbAdmin.getPlayerStats(data)) + toServerResult(async () => await pbAdmin.getPlayerStats(data)) ); export const getAllPlayerStats = createServerFn() @@ -139,4 +139,11 @@ export const getPlayerMatches = createServerFn() .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => toServerResult(async () => await pbAdmin.getPlayerMatches(data)) + ); + +export const getUnenrolledPlayers = createServerFn() + .validator(z.string()) + .middleware([superTokensFunctionMiddleware]) + .handler(async ({ data: tournamentId }) => + toServerResult(async () => await pbAdmin.getUnenrolledPlayers(tournamentId)) ); \ No newline at end of file diff --git a/src/features/teams/components/team-form/color-picker.tsx b/src/features/teams/components/team-form/color-picker.tsx new file mode 100644 index 0000000..2e243ba --- /dev/null +++ b/src/features/teams/components/team-form/color-picker.tsx @@ -0,0 +1,80 @@ +import { ColorPicker, TextInput, Stack, Group, ColorSwatch, Text } from '@mantine/core'; +import React, { useState, useCallback, useMemo } from 'react'; + +const presetColors = [ + '#FF0000', '#00FF00', '#0000FF', '#FFFF00', + '#FF00FF', '#00FFFF', '#FFA500', '#800080', + '#008000', '#000080', '#800000', '#808000' +]; + +interface TeamColorPickerProps { + value: string; + onChange: (value: string) => void; + label?: string; +} + +const TeamColorPicker: React.FC = ({ + value, + onChange, + label = "Select Color" +}) => { + const [customHex, setCustomHex] = useState(value || ''); + + const isValidHex = useMemo(() => { + const hexRegex = /^#[0-9A-F]{6}$/i; + return hexRegex.test(customHex); + }, [customHex]); + + const handleColorChange = useCallback((color: string) => { + setCustomHex(color); + onChange(color); + }, [onChange]); + + const handleHexInputChange = useCallback((event: React.ChangeEvent) => { + const hex = event.currentTarget.value; + setCustomHex(hex); + + if (/^#[0-9A-F]{6}$/i.test(hex)) { + onChange(hex); + } + }, [onChange]); + + return ( + + {label} + + + + + + + {isValidHex && ( + + )} + + + ); +}; + +export default TeamColorPicker; \ No newline at end of file diff --git a/src/features/teams/components/team-form.tsx b/src/features/teams/components/team-form/index.tsx similarity index 57% rename from src/features/teams/components/team-form.tsx rename to src/features/teams/components/team-form/index.tsx index 205a050..03cbbba 100644 --- a/src/features/teams/components/team-form.tsx +++ b/src/features/teams/components/team-form/index.tsx @@ -1,81 +1,88 @@ -import { FileInput, Stack, TextInput, Textarea } from "@mantine/core"; +import { Badge, FileInput, Group, Stack, Text, TextInput } from "@mantine/core"; import { useForm, UseFormInput } from "@mantine/form"; import { LinkIcon } from "@phosphor-icons/react"; import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel"; -import { TournamentInput } from "@/features/tournaments/types"; import { isNotEmpty } from "@mantine/form"; -import useCreateTournament from "../hooks/use-create-team"; -import useUpdateTournament from "../hooks/use-update-tournament"; +import useCreateTeam from "../../hooks/use-create-team"; +import useUpdateTeam from "../../hooks/use-update-team"; import toast from "@/lib/sonner"; -import { logger } from ".."; +import { logger } from "../.."; import { useQueryClient } from "@tanstack/react-query"; import { tournamentKeys } from "@/features/tournaments/queries"; import { useCallback } from "react"; -import { DateTimePicker } from "@/components/date-time-picker"; +import { TeamInput } from "../../types"; +import { teamKeys } from "../../queries"; +import SongPicker from "./song-picker"; +import TeamColorPicker from "./color-picker"; +import PlayersPicker from "./players-picker"; -interface TournamentFormProps { +interface TeamFormProps { close: () => void; - initialValues?: Partial; + initialValues?: Partial; + teamId?: string; tournamentId?: string; } -const TournamentForm = ({ +const TeamForm = ({ close, initialValues, + teamId, tournamentId, -}: TournamentFormProps) => { - const isEditMode = !!tournamentId; +}: TeamFormProps) => { + const isEditMode = !!teamId; - const config: UseFormInput = { + const config: UseFormInput = { 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 || "", + primary_color: initialValues?.primary_color, + accent_color: initialValues?.accent_color, + song_id: initialValues?.song_id, + song_name: initialValues?.song_name, + song_artist: initialValues?.song_artist, + song_album: initialValues?.song_album, + song_year: initialValues?.song_year, + song_start: initialValues?.song_start, + song_end: initialValues?.song_end, + song_image_url: initialValues?.song_image_url, logo: undefined, + players: initialValues?.players || [] }, 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"), + players: (value: string[]) => value.length > 1 && value[1] !== '' ? undefined : "Players are required" }, }; const form = useForm(config); const queryClient = useQueryClient(); - const { mutate: createTournament, isPending: createPending } = - useCreateTournament(); - const { mutate: updateTournament, isPending: updatePending } = - useUpdateTournament(tournamentId || ""); + const { mutate: createTournament, isPending: createPending } = useCreateTeam(); + const { mutate: updateTournament, isPending: updatePending } = useUpdateTeam(teamId!); const isPending = createPending || updatePending; const handleSubmit = useCallback( - async (values: TournamentInput) => { - const { logo, ...tournamentData } = values; + async (values: TeamInput) => { + const { logo, ...teamData } = values; const mutation = isEditMode ? updateTournament : createTournament; const successMessage = isEditMode - ? "Tournament updated successfully!" - : "Tournament created successfully!"; + ? "Team updated successfully!" + : "Team created successfully!"; const errorMessage = isEditMode - ? "Failed to update tournament" - : "Failed to create tournament"; + ? "Failed to update team" + : "Failed to create team"; - mutation(tournamentData, { - onSuccess: async (tournament) => { - if (logo && tournament) { + mutation(teamData, { + onSuccess: async (team) => { + if (logo && team) { try { const formData = new FormData(); - formData.append("tournamentId", tournament.id); + formData.append("teamId", team.id); formData.append("logo", logo); - const response = await fetch("/api/tournaments/upload-logo", { + const response = await fetch("/api/teams/upload-logo", { method: "POST", body: formData, }); @@ -87,22 +94,22 @@ const TournamentForm = ({ const result = await response.json(); - queryClient.invalidateQueries({ queryKey: tournamentKeys.list }); + queryClient.invalidateQueries({ queryKey: teamKeys.list }); queryClient.invalidateQueries({ - queryKey: tournamentKeys.details(result.tournament!.id), + queryKey: teamKeys.details(result.team!.id), }); queryClient.setQueryData( - tournamentKeys.details(result.tournament!.id), - result.tournament + tournamentKeys.details(result.team!.id), + result.team ); toast.success(successMessage); } catch (error: any) { const logoErrorMessage = isEditMode - ? `Tournament updated but logo upload failed: ${error.message}` - : `Tournament created but logo upload failed: ${error.message}`; + ? `Team updated but logo upload failed: ${error.message}` + : `Team created but logo upload failed: ${error.message}`; toast.error(logoErrorMessage); - logger.error("Tournament logo upload error", error); + logger.error("Team logo upload error", error); } } else { toast.success(successMessage); @@ -112,7 +119,7 @@ const TournamentForm = ({ onError: (error: any) => { toast.error(`${errorMessage}: ${error.message}`); logger.error( - `Tournament ${isEditMode ? "update" : "create"} error`, + `Team ${isEditMode ? "update" : "create"} error`, error ); }, @@ -123,9 +130,9 @@ const TournamentForm = ({ return ( console.log(values))} onCancel={close} - submitText={isEditMode ? "Update Tournament" : "Create Tournament"} + submitText={isEditMode ? "Update Team" : "Create Team"} cancelText="Cancel" loading={isPending} > @@ -136,19 +143,6 @@ const TournamentForm = ({ key={form.key("name")} {...form.getInputProps("name")} /> - - -