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}
/>
+