Admin tabs

This commit is contained in:
Kalle
2026-05-30 15:19:31 +03:00
parent 4f65786188
commit 99a06ef804
20 changed files with 192 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
import clsx from "clsx";
import { Trash } from "lucide-react";
import { Trash, Trophy, Tv, UserCog, Users } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useFetcher } from "react-router";
@@ -7,6 +7,12 @@ import { Avatar } from "~/components/Avatar";
import { Divider } from "~/components/Divider";
import { LinkButton, SendouButton } from "~/components/elements/Button";
import { SendouDialog } from "~/components/elements/Dialog";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { UserSearch } from "~/components/elements/UserSearch";
import { FormMessage } from "~/components/FormMessage";
import { FormWithConfirm } from "~/components/FormWithConfirm";
@@ -20,6 +26,7 @@ import { useUser } from "~/features/auth/core/user";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server";
import { USER } from "~/features/user-page/user-page-constants";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import { databaseTimestampToDate } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { assertUnreachable } from "~/utils/types";
@@ -35,8 +42,10 @@ import adminStyles from "./to.$id.admin.module.css";
export { action } from "../actions/to.$id.admin.server";
type AdminTab = "teams" | "staff" | "stream" | "brackets";
export default function TournamentAdminPage() {
const { t } = useTranslation(["calendar"]);
const { t } = useTranslation(["tournament", "calendar"]);
const tournament = useTournament();
const [editingProgression, setEditingProgression] = React.useState(false);
@@ -47,6 +56,39 @@ export default function TournamentAdminPage() {
setEditingProgression(false);
}, [tournament]);
const showReopen = Boolean(
DANGEROUS_CAN_ACCESS_DEV_CONTROLS &&
tournament.ctx.isFinalized &&
tournament.isAdmin(user),
);
const showEditBrackets =
tournament.isAdmin(user) &&
tournament.hasStarted &&
!tournament.ctx.isFinalized;
const showStaffTab = tournament.isAdmin(user);
const showBracketsTab =
!tournament.isLeagueSignup || showEditBrackets || showReopen;
const isVisibleTab = (tab: string): tab is AdminTab => {
switch (tab) {
case "teams":
case "stream":
return true;
case "staff":
return showStaffTab;
case "brackets":
return showBracketsTab;
default:
return false;
}
};
const [tab, setTab] = useSearchParamState<AdminTab>({
defaultValue: "teams",
name: "tab",
revive: (value) => (isVisibleTab(value) ? value : null),
});
if (
!tournament.isOrganizer(user) ||
(tournament.ctx.isFinalized && !DANGEROUS_CAN_ACCESS_DEV_CONTROLS)
@@ -86,51 +128,77 @@ export default function TournamentAdminPage() {
) : null}
</div>
) : null}
{tournament.isAdmin(user) &&
tournament.hasStarted &&
!tournament.ctx.isFinalized ? (
<div className="stack horizontal justify-end">
<SendouButton
onPress={() => setEditingProgression(true)}
size="small"
variant="outlined"
data-testid="edit-event-info-button"
>
Edit brackets
</SendouButton>
{editingProgression ? (
<BracketProgressionEditDialog
close={() => setEditingProgression(false)}
/>
<SendouTabs
orientation="vertical"
horizontalBelow={720}
selectedKey={tab}
onSelectionChange={(key) => setTab(key as AdminTab)}
>
<SendouTabList>
<SendouTab id="teams" icon={<Users />}>
{t("tournament:admin.tab.teams")}
</SendouTab>
{showStaffTab ? (
<SendouTab id="staff" icon={<UserCog />}>
{t("tournament:admin.tab.staff")}
</SendouTab>
) : null}
</div>
) : null}
<Divider smallText>Team actions</Divider>
<TeamActions />
{tournament.isAdmin(user) ? (
<>
<Divider smallText>Staff</Divider>
<Staff />
</>
) : null}
<Divider smallText>Cast Twitch Accounts</Divider>
<CastTwitchAccounts />
<Divider smallText>Participant list download</Divider>
<DownloadParticipants />
{!tournament.isLeagueSignup ? (
<>
<Divider smallText>Bracket reset</Divider>
<BracketReset />
</>
) : null}
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS &&
tournament.ctx.isFinalized &&
tournament.isAdmin(user) ? (
<>
<Divider smallText>Reopen tournament (dev only)</Divider>
<ReopenTournament />
</>
) : null}
<SendouTab id="stream" icon={<Tv />}>
{t("tournament:admin.tab.stream")}
</SendouTab>
{showBracketsTab ? (
<SendouTab id="brackets" icon={<Trophy />}>
{t("tournament:admin.tab.brackets")}
</SendouTab>
) : null}
</SendouTabList>
<SendouTabPanel id="teams" className="stack lg">
<TeamActions />
<Divider smallText>Participant list download</Divider>
<DownloadParticipants />
</SendouTabPanel>
{showStaffTab ? (
<SendouTabPanel id="staff">
<Staff />
</SendouTabPanel>
) : null}
<SendouTabPanel id="stream">
<CastTwitchAccounts />
</SendouTabPanel>
{showBracketsTab ? (
<SendouTabPanel id="brackets" className="stack lg">
{showEditBrackets ? (
<div className="stack horizontal justify-end">
<SendouButton
onPress={() => setEditingProgression(true)}
size="small"
variant="outlined"
data-testid="edit-event-info-button"
>
Edit brackets
</SendouButton>
{editingProgression ? (
<BracketProgressionEditDialog
close={() => setEditingProgression(false)}
/>
) : null}
</div>
) : null}
{!tournament.isLeagueSignup ? (
<>
<Divider smallText>Bracket reset</Divider>
<BracketReset />
</>
) : null}
{showReopen ? (
<>
<Divider smallText>Reopen tournament (dev only)</Divider>
<ReopenTournament />
</>
) : null}
</SendouTabPanel>
) : null}
</SendouTabs>
</div>
);
}

View File

@@ -376,6 +376,7 @@ test.describe("Tournament bracket", () => {
await submit(page);
}
await page.getByRole("tab", { name: "Brackets" }).click();
await page.getByTestId("edit-event-info-button").click();
for (let i = 0; i < 3; i++) {
await page.getByTestId("delete-bracket-button").last().click();
@@ -436,6 +437,7 @@ test.describe("Tournament bracket", () => {
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Brackets" }).click();
await page.getByTestId("edit-event-info-button").click();
await page.getByTestId("delete-bracket-button").last().click();
await page.getByTestId("placements-input").last().fill("3,4");
@@ -498,6 +500,7 @@ test.describe("Tournament bracket", () => {
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Brackets" }).click();
await page.getByTestId("edit-event-info-button").click();
await page.getByTestId("delete-bracket-button").last().click();
@@ -596,6 +599,7 @@ test.describe("Tournament bracket", () => {
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Brackets" }).click();
await page.getByTestId("edit-event-info-button").click();
await page.getByRole("button", { name: "Clear" }).click();
@@ -738,6 +742,7 @@ test.describe("Tournament bracket", () => {
await submit(page);
}
await page.getByRole("tab", { name: "Stream" }).click();
await page.getByLabel("Twitch accounts").fill("test");
await submit(page, "save-cast-twitch-accounts-button");
@@ -802,11 +807,13 @@ test.describe("Tournament bracket", () => {
await reportResult(page, { mapsToReport: 2 });
await page.getByTestId("admin-tab").click();
await page.getByRole("tab", { name: "Brackets" }).click();
await page
.getByLabel('Type bracket name ("Main bracket") to confirm')
.fill("Main bracket");
await submit(page, "reset-bracket-button");
await page.getByRole("tab", { name: "Teams" }).click();
await page.getByLabel("Action").selectOption("CHECK_IN");
await page.getByLabel("Team", { exact: true }).selectOption("1");
await submit(page);

View File

@@ -31,6 +31,7 @@ test.describe("Tournament staff", () => {
url: tournamentAdminPage(TOURNAMENT_ID),
});
await page.getByRole("tab", { name: "Staff" }).click();
await selectUser({
page,
userName: "N-ZAP",
@@ -65,6 +66,7 @@ test.describe("Tournament staff", () => {
url: tournamentAdminPage(TOURNAMENT_ID),
});
await page.getByRole("tab", { name: "Staff" }).click();
await selectUser({
page,
userName: "N-ZAP",
@@ -83,7 +85,7 @@ test.describe("Tournament staff", () => {
await isNotVisible(page.getByTestId("add-staff-button"));
await page.getByLabel("Action").selectOption("CHECK_IN");
await page.getByLabel("Team").selectOption("101");
await page.getByLabel("Team", { exact: true }).selectOption("101");
await submit(page);
await navigate({
@@ -118,6 +120,7 @@ test.describe("Tournament staff", () => {
url: tournamentAdminPage(TOURNAMENT_ID),
});
await page.getByRole("tab", { name: "Staff" }).click();
await selectUser({
page,
userName: "N-ZAP",

View File

@@ -31,6 +31,7 @@ test.describe("Tournament streams", () => {
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Stream" }).click();
await page
.getByLabel("Twitch accounts")
.fill("test_cast_stream,another_cast");
@@ -46,6 +47,7 @@ test.describe("Tournament streams", () => {
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Stream" }).click();
await expect(page.getByLabel("Twitch accounts")).toHaveValue(
"test_cast_stream,another_cast",
);
@@ -114,6 +116,7 @@ test.describe("Tournament streams", () => {
page,
url: tournamentAdminPage(tournamentId),
});
await page.getByRole("tab", { name: "Stream" }).click();
await page.getByLabel("Twitch accounts").fill("test_cast_stream");
await submit(page, "save-cast-twitch-accounts-button");

View File

@@ -76,6 +76,10 @@
"generator.error": "Ændringer, som du har lavet, blev ikke gemt, da turneringen nu er begyndt. ",
"teams.mapsPickedStatus": "Status for banevalg",
"admin.download": "Hent liste over deltagere",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Ændr holdkaptajn",
"admin.actions.CHANGE_TEAM_NAME": "Ændr holdnavn",
"admin.actions.CHECK_IN": "Tjek ind",

View File

@@ -76,6 +76,10 @@
"generator.error": "Die Änderungen wurden nicht gespeichert, weil das Turnier bereits begonnen hat",
"teams.mapsPickedStatus": "Status ausgewählter Arenen",
"admin.download": "Teilnehmerliste herunterladen",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Kapitän ändern",
"admin.actions.CHANGE_TEAM_NAME": "Teamnamen ändern",
"admin.actions.CHECK_IN": "Einchecken",

View File

@@ -76,6 +76,10 @@
"generator.error": "Changes you made weren't saved since the tournament started",
"teams.mapsPickedStatus": "Maps picked status",
"admin.download": "Download participants",
"admin.tab.teams": "Teams",
"admin.tab.staff": "Staff",
"admin.tab.stream": "Stream",
"admin.tab.brackets": "Brackets",
"admin.actions.CHANGE_TEAM_OWNER": "Change captain",
"admin.actions.CHANGE_TEAM_NAME": "Change team name",
"admin.actions.CHECK_IN": "Check in",

View File

@@ -77,6 +77,10 @@
"generator.error": "Los cambios no fueron guardados ya que el torneo comenzó",
"teams.mapsPickedStatus": "Estado de mapas elegidos",
"admin.download": "Descarga participantes",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Cambiar capitán",
"admin.actions.CHANGE_TEAM_NAME": "Cambiar nombre de equipo",
"admin.actions.CHECK_IN": "Check in",

View File

@@ -77,6 +77,10 @@
"generator.error": "Cambios no fueron guardados ya que el torneo comenzó",
"teams.mapsPickedStatus": "Estado de escenarios elegidos",
"admin.download": "Descarga participantes",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Cambiar capitán",
"admin.actions.CHANGE_TEAM_NAME": "Cambiar nombre de equipo",
"admin.actions.CHECK_IN": "Check in",

View File

@@ -77,6 +77,10 @@
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",
"teams.mapsPickedStatus": "Statut des stages sélectionnés",
"admin.download": "Télécharger les participants",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Changer le capitaine",
"admin.actions.CHANGE_TEAM_NAME": "",
"admin.actions.CHECK_IN": "S'enregister",

View File

@@ -77,6 +77,10 @@
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",
"teams.mapsPickedStatus": "Statut des stages sélectionnés",
"admin.download": "Télécharger les participants",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Changer le capitaine",
"admin.actions.CHANGE_TEAM_NAME": "Changer le nom de l'équipe ",
"admin.actions.CHECK_IN": "S'enregister",

View File

@@ -77,6 +77,10 @@
"generator.error": "שינויים שביצעת לא נשמרו מאז תחילת הטורניר",
"teams.mapsPickedStatus": "סטטוס בחירת המפות",
"admin.download": "הורדת משתתפים",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "שינוי קפטן",
"admin.actions.CHANGE_TEAM_NAME": "",
"admin.actions.CHECK_IN": "קבלה",

View File

@@ -77,6 +77,10 @@
"generator.error": "I cambiamenti che hai fatto non sono stati salvati visto che il torneo è già cominciato.",
"teams.mapsPickedStatus": "Stato scenari selezionati",
"admin.download": "Download partecipanti",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Cambia capitano",
"admin.actions.CHANGE_TEAM_NAME": "Cambia nome team",
"admin.actions.CHECK_IN": "Check-in",

View File

@@ -74,6 +74,10 @@
"generator.error": "トーナメントが開始済みなので変更は保存されませんでした。",
"teams.mapsPickedStatus": "マップの選択状況",
"admin.download": "参加者リストをダウンロード",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "キャプテンを変更",
"admin.actions.CHANGE_TEAM_NAME": "チーム名変更",
"admin.actions.CHECK_IN": "チェックイン",

View File

@@ -74,6 +74,10 @@
"generator.error": "",
"teams.mapsPickedStatus": "",
"admin.download": "",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "",
"admin.actions.CHANGE_TEAM_NAME": "",
"admin.actions.CHECK_IN": "",

View File

@@ -76,6 +76,10 @@
"generator.error": "",
"teams.mapsPickedStatus": "",
"admin.download": "",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "",
"admin.actions.CHANGE_TEAM_NAME": "",
"admin.actions.CHECK_IN": "",

View File

@@ -78,6 +78,10 @@
"generator.error": "Zmiany nie zostały zapisane z powodu rozpoczęcia turnieju",
"teams.mapsPickedStatus": "Status wybranych map",
"admin.download": "Zapisz uczestników",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "",
"admin.actions.CHANGE_TEAM_NAME": "",
"admin.actions.CHECK_IN": "",

View File

@@ -77,6 +77,10 @@
"generator.error": "Mudanças que você fez não foram salvas uma vez que o torneio já começou",
"teams.mapsPickedStatus": "Status dos mapas escolhidos",
"admin.download": "Download dos participantes",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Mudar capitão",
"admin.actions.CHANGE_TEAM_NAME": "Mudar nome do time",
"admin.actions.CHECK_IN": "Check-in",

View File

@@ -78,6 +78,10 @@
"generator.error": "Ваши изменения не были сохранены так как турнир начался",
"teams.mapsPickedStatus": "Статус выбранных карт",
"admin.download": "Скачать участников",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "Изменить капитана",
"admin.actions.CHANGE_TEAM_NAME": "Изменить имя команды",
"admin.actions.CHECK_IN": "Зарегистрировать",

View File

@@ -74,6 +74,10 @@
"generator.error": "由于比赛已经开始,您所更改的内容无法保存",
"teams.mapsPickedStatus": "地图的选择情况",
"admin.download": "下载参赛者",
"admin.tab.teams": "",
"admin.tab.staff": "",
"admin.tab.stream": "",
"admin.tab.brackets": "",
"admin.actions.CHANGE_TEAM_OWNER": "变更队长",
"admin.actions.CHANGE_TEAM_NAME": "变更队名",
"admin.actions.CHECK_IN": "登入",