diff --git a/app/components/tournament/AdminTeamControls.tsx b/app/components/tournament/AdminTeamControls.tsx
new file mode 100644
index 000000000..ff9eef284
--- /dev/null
+++ b/app/components/tournament/AdminTeamControls.tsx
@@ -0,0 +1,191 @@
+import {
+ closestCenter,
+ DndContext,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import classNames from "classnames";
+import * as React from "react";
+import { useFetcher, useMatches } from "remix";
+import { Alert } from "~/components/Alert";
+import { Button } from "~/components/Button";
+import { MyForm } from "~/components/MyForm";
+import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants";
+import { checkInHasStarted } from "~/core/tournament/utils";
+import type { FindTournamentByNameForUrlI } from "~/services/tournament";
+import type { Unpacked } from "~/utils";
+
+export function AdminTeamControls() {
+ const [, parentRoute] = useMatches();
+ const { teams, checkInStartTime } =
+ parentRoute.data as FindTournamentByNameForUrlI;
+ const [teamOrder, setTeamOrder] = React.useState(teams.map((t) => t.id));
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
+ const teamsSorted = teams.sort(
+ (a, b) => teamOrder.indexOf(a.id) - teamOrder.indexOf(b.id)
+ );
+
+ return (
+ <>
+
+ Drag teams to adjust their seeding
+
+
+ >
+ );
+}
+
+function SortableRow({
+ team,
+ seed,
+}: {
+ team: Unpacked;
+ seed: number;
+}) {
+ const [, parentRoute] = useMatches();
+ const { checkInStartTime } = parentRoute.data as FindTournamentByNameForUrlI;
+
+ const { attributes, listeners, setNodeRef, transform, transition } =
+ useSortable({ id: team.id });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
+ return (
+
+ {seed}
+ {team.name}
+
+ {!checkInHasStarted(checkInStartTime) ? (
+ <>
+ {new Date(team.createdAt).toLocaleString("en-US", {
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })}
+ >
+ ) : team.checkedInTime ? (
+
+ ) : team.members.length >= TOURNAMENT_TEAM_ROSTER_MIN_SIZE ? (
+
+ ) : null}
+
+ = TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
+ tournament__admin__problem:
+ team.members.length < TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
+ })}
+ >
+ {team.members.length}
+
+
+ );
+}
+
+function CheckOutButton({ teamId }: { teamId: string }) {
+ const fetcher = useFetcher();
+ return (
+
+
+
+ );
+}
+
+function CheckInButton({ teamId }: { teamId: string }) {
+ const fetcher = useFetcher();
+ return (
+
+
+
+ );
+}
diff --git a/app/routes/to/$organization.$tournament/admin.tsx b/app/routes/to/$organization.$tournament/admin.tsx
index b1f2eed8b..c80b43728 100644
--- a/app/routes/to/$organization.$tournament/admin.tsx
+++ b/app/routes/to/$organization.$tournament/admin.tsx
@@ -1,118 +1,20 @@
-import { useFetcher, useMatches } from "remix";
import type { LinksFunction } from "remix";
import { Button } from "~/components/Button";
-import type { FindTournamentByNameForUrlI } from "~/services/tournament";
+import { AdminTeamControls } from "~/components/tournament/AdminTeamControls";
import styles from "~/styles/tournament-admin.css";
-import * as React from "react";
-import classNames from "classnames";
-import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants";
-import { Alert } from "~/components/Alert";
-import { MyForm } from "~/components/MyForm";
-import { checkInHasStarted } from "~/core/tournament/utils";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
export default function AdminPage() {
- const [, parentRoute] = useMatches();
- const { teams, checkInStartTime } =
- parentRoute.data as FindTournamentByNameForUrlI;
-
return (
<>
-
- Drag teams to adjust their seeding
-
-
-
Seed
-
Name
-
- {checkInHasStarted(checkInStartTime) ? "" : "Registered at"}
-
-
- Roster size
-
- {/* TODO: order by seed */}
- {teams.map((team, i) => (
-
- {i + 1}
- {team.name}
-
- {!checkInHasStarted(checkInStartTime) ? (
- <>
- {new Date(team.createdAt).toLocaleString("en-US", {
- month: "numeric",
- day: "numeric",
- hour: "numeric",
- minute: "numeric",
- })}
- >
- ) : team.checkedInTime ? (
-
- ) : team.members.length >= TOURNAMENT_TEAM_ROSTER_MIN_SIZE ? (
-
- ) : null}
-
- = TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
- tournament__admin__problem:
- team.members.length < TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
- })}
- >
- {team.members.length}
-
-
- ))}
-
+
>
);
}
-
-function CheckOutButton({ teamId }: { teamId: string }) {
- const fetcher = useFetcher();
- return (
-
-
-
- );
-}
-
-function CheckInButton({ teamId }: { teamId: string }) {
- const fetcher = useFetcher();
- return (
-
-
-
- );
-}
diff --git a/app/styles/tournament-admin.css b/app/styles/tournament-admin.css
index 87596c39e..8ad6ebb72 100644
--- a/app/styles/tournament-admin.css
+++ b/app/styles/tournament-admin.css
@@ -11,16 +11,21 @@
}
/* TODO: overflow-x scroll */
-.tournament__admin__teams-container {
+.tournament__admin__teams-list-row {
display: grid;
width: 100%;
column-gap: var(--s-1);
font-size: var(--fonts-sm);
grid-template-columns: 1fr 3fr 3fr 1fr;
+ list-style: none;
margin-block-start: var(--s-4);
row-gap: var(--s-1-5);
}
+.tournament__admin__teams-list-row.sortable {
+ cursor: grab;
+}
+
.tournament__admin__teams-container__header {
font-weight: var(--bold);
}
diff --git a/package-lock.json b/package-lock.json
index b1559597c..193d4d518 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,8 @@
"name": "remix-app-template",
"hasInstallScript": true,
"dependencies": {
+ "@dnd-kit/core": "^4.0.3",
+ "@dnd-kit/sortable": "^5.1.0",
"@prisma/client": "^3.6.0",
"@remix-run/express": "^1.0.6",
"@remix-run/react": "^1.0.6",
@@ -195,6 +197,75 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz",
+ "integrity": "sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/accessibility/node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-4.0.3.tgz",
+ "integrity": "sha512-uT1uHZxKx3iEkupmLfknMIvbykMJSetoXXmra6sGGvtWy+OMKrWm3axH2c90+JC/q6qaeKs2znd3Qs8GLnCa5Q==",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.0.0",
+ "@dnd-kit/utilities": "^3.0.1",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core/node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-5.1.0.tgz",
+ "integrity": "sha512-CPyiUHbTrSYzhddfgdeoX0ERg/dEyVKIWx9+4O6uqpoppo84SXCBHVFiFBRVpQ9wtpsXs7prtUAnAUTcvFQTZg==",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.0.0",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^4.0.2",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable/node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.0.1.tgz",
+ "integrity": "sha512-lAfK9lS5H9I1B/t3iVkQa85Cn+nPRJh3ksPUxAnoxPDqvwcW4tfV0wP++O/dRCP30T76+YF+/XJo7hyOEkzHig==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities/node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ },
"node_modules/@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@@ -8218,6 +8289,69 @@
}
}
},
+ "@dnd-kit/accessibility": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz",
+ "integrity": "sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==",
+ "requires": {
+ "tslib": "^2.0.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ }
+ }
+ },
+ "@dnd-kit/core": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-4.0.3.tgz",
+ "integrity": "sha512-uT1uHZxKx3iEkupmLfknMIvbykMJSetoXXmra6sGGvtWy+OMKrWm3axH2c90+JC/q6qaeKs2znd3Qs8GLnCa5Q==",
+ "requires": {
+ "@dnd-kit/accessibility": "^3.0.0",
+ "@dnd-kit/utilities": "^3.0.1",
+ "tslib": "^2.0.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ }
+ }
+ },
+ "@dnd-kit/sortable": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-5.1.0.tgz",
+ "integrity": "sha512-CPyiUHbTrSYzhddfgdeoX0ERg/dEyVKIWx9+4O6uqpoppo84SXCBHVFiFBRVpQ9wtpsXs7prtUAnAUTcvFQTZg==",
+ "requires": {
+ "@dnd-kit/utilities": "^3.0.0",
+ "tslib": "^2.0.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ }
+ }
+ },
+ "@dnd-kit/utilities": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.0.1.tgz",
+ "integrity": "sha512-lAfK9lS5H9I1B/t3iVkQa85Cn+nPRJh3ksPUxAnoxPDqvwcW4tfV0wP++O/dRCP30T76+YF+/XJo7hyOEkzHig==",
+ "requires": {
+ "tslib": "^2.0.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+ }
+ }
+ },
"@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
diff --git a/package.json b/package.json
index b6db94754..6a5fae6a0 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
"cy:run": "npx cypress run"
},
"dependencies": {
+ "@dnd-kit/core": "^4.0.3",
+ "@dnd-kit/sortable": "^5.1.0",
"@prisma/client": "^3.6.0",
"@remix-run/express": "^1.0.6",
"@remix-run/react": "^1.0.6",