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 (