From ae934e77f412213f9e807460d24fc6545bbfa9dd Mon Sep 17 00:00:00 2001 From: yohlo Date: Mon, 22 Sep 2025 17:24:45 -0500 Subject: [PATCH] manage team data --- src/app/routeTree.gen.ts | 54 ++++-- .../tournaments/{$id.tsx => $id/index.tsx} | 2 +- .../_authed/admin/tournaments/$id/teams.tsx | 32 ++++ .../teams/components/manage-teams.tsx | 165 ++++++++++++++++++ src/features/teams/components/team-list.tsx | 13 +- .../components/edit-enrolled-teams.tsx | 1 + .../components/manage-tournament.tsx | 6 + 7 files changed, 253 insertions(+), 20 deletions(-) rename src/app/routes/_authed/admin/tournaments/{$id.tsx => $id/index.tsx} (98%) create mode 100644 src/app/routes/_authed/admin/tournaments/$id/teams.tsx create mode 100644 src/features/teams/components/manage-teams.tsx diff --git a/src/app/routeTree.gen.ts b/src/app/routeTree.gen.ts index 1c86190..c9c8758 100644 --- a/src/app/routeTree.gen.ts +++ b/src/app/routeTree.gen.ts @@ -27,8 +27,9 @@ import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/prof import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview' import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index' import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket' -import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id' +import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index' import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id' +import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams' 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' @@ -125,10 +126,10 @@ const AuthedTournamentsIdBracketRoute = path: '/tournaments/$id/bracket', getParentRoute: () => AuthedRoute, } as any) -const AuthedAdminTournamentsIdRoute = - AuthedAdminTournamentsIdRouteImport.update({ - id: '/tournaments/$id', - path: '/tournaments/$id', +const AuthedAdminTournamentsIdIndexRoute = + AuthedAdminTournamentsIdIndexRouteImport.update({ + id: '/tournaments/$id/', + path: '/tournaments/$id/', getParentRoute: () => AuthedAdminRoute, } as any) const AuthedAdminTournamentsRunIdRoute = @@ -137,6 +138,12 @@ const AuthedAdminTournamentsRunIdRoute = path: '/tournaments/run/$id', getParentRoute: () => AuthedAdminRoute, } as any) +const AuthedAdminTournamentsIdTeamsRoute = + AuthedAdminTournamentsIdTeamsRouteImport.update({ + id: '/tournaments/$id/teams', + path: '/tournaments/$id/teams', + getParentRoute: () => AuthedAdminRoute, + } as any) const ApiTournamentsUploadLogoServerRoute = ApiTournamentsUploadLogoServerRouteImport.update({ id: '/api/tournaments/upload-logo', @@ -212,10 +219,11 @@ export interface FileRoutesByFullPath { '/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/admin/': typeof AuthedAdminIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute - '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute + '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRoutesByTo { '/login': typeof LoginRoute @@ -230,10 +238,11 @@ export interface FileRoutesByTo { '/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/admin': typeof AuthedAdminIndexRoute '/tournaments': typeof AuthedTournamentsIndexRoute - '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute + '/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -251,10 +260,11 @@ export interface FileRoutesById { '/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute '/_authed/admin/': typeof AuthedAdminIndexRoute '/_authed/tournaments/': typeof AuthedTournamentsIndexRoute - '/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute '/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute '/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute + '/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute '/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute + '/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -272,10 +282,11 @@ export interface FileRouteTypes { | '/tournaments/$tournamentId' | '/admin/' | '/tournaments' - | '/admin/tournaments/$id' | '/tournaments/$id/bracket' | '/admin/tournaments' + | '/admin/tournaments/$id/teams' | '/admin/tournaments/run/$id' + | '/admin/tournaments/$id' fileRoutesByTo: FileRoutesByTo to: | '/login' @@ -290,10 +301,11 @@ export interface FileRouteTypes { | '/tournaments/$tournamentId' | '/admin' | '/tournaments' - | '/admin/tournaments/$id' | '/tournaments/$id/bracket' | '/admin/tournaments' + | '/admin/tournaments/$id/teams' | '/admin/tournaments/run/$id' + | '/admin/tournaments/$id' id: | '__root__' | '/_authed' @@ -310,10 +322,11 @@ export interface FileRouteTypes { | '/_authed/tournaments/$tournamentId' | '/_authed/admin/' | '/_authed/tournaments/' - | '/_authed/admin/tournaments/$id' | '/_authed/tournaments/$id/bracket' | '/_authed/admin/tournaments/' + | '/_authed/admin/tournaments/$id/teams' | '/_authed/admin/tournaments/run/$id' + | '/_authed/admin/tournaments/$id/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -532,11 +545,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedTournamentsIdBracketRouteImport parentRoute: typeof AuthedRoute } - '/_authed/admin/tournaments/$id': { - id: '/_authed/admin/tournaments/$id' + '/_authed/admin/tournaments/$id/': { + id: '/_authed/admin/tournaments/$id/' path: '/tournaments/$id' fullPath: '/admin/tournaments/$id' - preLoaderRoute: typeof AuthedAdminTournamentsIdRouteImport + preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport parentRoute: typeof AuthedAdminRoute } '/_authed/admin/tournaments/run/$id': { @@ -546,6 +559,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedAdminTournamentsRunIdRouteImport parentRoute: typeof AuthedAdminRoute } + '/_authed/admin/tournaments/$id/teams': { + id: '/_authed/admin/tournaments/$id/teams' + path: '/tournaments/$id/teams' + fullPath: '/admin/tournaments/$id/teams' + preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport + parentRoute: typeof AuthedAdminRoute + } } } declare module '@tanstack/react-start/server' { @@ -633,17 +653,19 @@ declare module '@tanstack/react-start/server' { interface AuthedAdminRouteChildren { AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute - AuthedAdminTournamentsIdRoute: typeof AuthedAdminTournamentsIdRoute AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute + AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute + AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute } const AuthedAdminRouteChildren: AuthedAdminRouteChildren = { AuthedAdminPreviewRoute: AuthedAdminPreviewRoute, AuthedAdminIndexRoute: AuthedAdminIndexRoute, - AuthedAdminTournamentsIdRoute: AuthedAdminTournamentsIdRoute, AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute, + AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute, AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute, + AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute, } const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren( diff --git a/src/app/routes/_authed/admin/tournaments/$id.tsx b/src/app/routes/_authed/admin/tournaments/$id/index.tsx similarity index 98% rename from src/app/routes/_authed/admin/tournaments/$id.tsx rename to src/app/routes/_authed/admin/tournaments/$id/index.tsx index 2870035..6857a14 100644 --- a/src/app/routes/_authed/admin/tournaments/$id.tsx +++ b/src/app/routes/_authed/admin/tournaments/$id/index.tsx @@ -3,7 +3,7 @@ import { tournamentQueries } from "@/features/tournaments/queries"; import ManageTournament from "@/features/tournaments/components/manage-tournament"; import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; -export const Route = createFileRoute("/_authed/admin/tournaments/$id")({ +export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({ beforeLoad: async ({ context, params }) => { const { queryClient } = context; const tournament = await ensureServerQueryData( diff --git a/src/app/routes/_authed/admin/tournaments/$id/teams.tsx b/src/app/routes/_authed/admin/tournaments/$id/teams.tsx new file mode 100644 index 0000000..ed034e1 --- /dev/null +++ b/src/app/routes/_authed/admin/tournaments/$id/teams.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { tournamentQueries } from "@/features/tournaments/queries"; +import ManageTeams from "@/features/teams/components/manage-teams"; +import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure"; + +export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({ + beforeLoad: async ({ context, params }) => { + const { queryClient } = context; + const tournament = await ensureServerQueryData( + queryClient, + tournamentQueries.details(params.id) + ); + if (!tournament) throw redirect({ to: "/admin/tournaments" }); + return { + tournament, + }; + }, + loader: ({ context }) => ({ + header: { + withBackButton: true, + title: `Manage Teams - ${context.tournament.name}`, + }, + withPadding: false, + }), + component: RouteComponent, +}); + +function RouteComponent() { + const { id } = Route.useParams(); + const { tournament } = Route.useRouteContext(); + return ; +} \ No newline at end of file diff --git a/src/features/teams/components/manage-teams.tsx b/src/features/teams/components/manage-teams.tsx new file mode 100644 index 0000000..b0abb03 --- /dev/null +++ b/src/features/teams/components/manage-teams.tsx @@ -0,0 +1,165 @@ +import { useState, useMemo } from "react"; +import { + Text, + TextInput, + Stack, + Container, + Box, + ThemeIcon, + Title, +} from "@mantine/core"; +import { + MagnifyingGlassIcon, + UsersIcon, +} from "@phosphor-icons/react"; +import { Tournament } from "@/features/tournaments/types"; +import TeamList from "./team-list"; +import Sheet from "@/components/sheet/sheet"; +import TeamForm from "./team-form"; +import { useSheet } from "@/hooks/use-sheet"; +import { useTeam } from "../queries"; + +interface TeamEditSheetProps { + teamId: string; + isOpen: boolean; + onClose: () => void; +} + +const TeamEditSheet = ({ teamId, isOpen, onClose }: TeamEditSheetProps) => { + const { data: team } = useTeam(teamId); + + return ( + + {team && ( + p.id) : [], + logo: typeof team.logo === "string" ? undefined : team.logo, + }} + close={onClose} + /> + )} + + ); +}; + +interface ManageTeamsProps { + tournament: Tournament; +} + +const ManageTeams = ({ tournament }: ManageTeamsProps) => { + const [search, setSearch] = useState(""); + const [selectedTeamId, setSelectedTeamId] = useState(null); + + const { + isOpen: editTeamOpened, + open: openEditTeam, + close: closeEditTeam, + } = useSheet(); + + const teams = tournament.teams || []; + + const filteredTeams = useMemo(() => { + if (!search.trim()) return teams; + + const searchLower = search.toLowerCase(); + + return teams.filter((team) => { + if (team.name.toLowerCase().includes(searchLower)) { + return true; + } + + if (team.players) { + return team.players.some((player) => { + const firstName = player.first_name?.toLowerCase() || ""; + const lastName = player.last_name?.toLowerCase() || ""; + const fullName = `${firstName} ${lastName}`.toLowerCase(); + + return fullName.includes(searchLower) || + firstName.includes(searchLower) || + lastName.includes(searchLower); + }); + } + + return false; + }); + }, [teams, search]); + + const handleTeamClick = (teamId: string) => { + setSelectedTeamId(teamId); + openEditTeam(); + }; + + const handleCloseEditTeam = () => { + setSelectedTeamId(null); + closeEditTeam(); + }; + + if (!teams.length) { + return ( + + + + + + + No Teams Enrolled + + + This tournament has no enrolled teams yet. + + + + ); + } + + return ( + <> + + + setSearch(e.currentTarget.value)} + leftSection={} + size="md" + px="md" + /> + + + + {filteredTeams.length} of {teams.length} teams + + + + + + {filteredTeams.length === 0 && search && ( + + No teams found matching "{search}" + + )} + + + + {selectedTeamId && ( + + )} + + ); +}; + +export default ManageTeams; \ No newline at end of file diff --git a/src/features/teams/components/team-list.tsx b/src/features/teams/components/team-list.tsx index 57ce65f..250a818 100644 --- a/src/features/teams/components/team-list.tsx +++ b/src/features/teams/components/team-list.tsx @@ -39,14 +39,21 @@ const TeamListItem = React.memo(({ team }: TeamListItemProps) => { interface TeamListProps { teams: TeamInfo[]; loading?: boolean; + onTeamClick?: (teamId: string) => void; } -const TeamList = ({ teams, loading = false }: TeamListProps) => { +const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => { const navigate = useNavigate(); const handleClick = useCallback( - (teamId: string) => navigate({ to: `/teams/${teamId}` }), - [navigate] + (teamId: string) => { + if (onTeamClick) { + onTeamClick(teamId); + } else { + navigate({ to: `/teams/${teamId}` }); + } + }, + [navigate, onTeamClick] ); if (loading) diff --git a/src/features/tournaments/components/edit-enrolled-teams.tsx b/src/features/tournaments/components/edit-enrolled-teams.tsx index 31b7a54..f2bb3f7 100644 --- a/src/features/tournaments/components/edit-enrolled-teams.tsx +++ b/src/features/tournaments/components/edit-enrolled-teams.tsx @@ -36,6 +36,7 @@ const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => { { Icon={UsersThreeIcon} onClick={openEditTeams} /> +