Remove legacy tournament map list generator

This commit is contained in:
Kalle 2024-06-09 11:42:06 +03:00
parent 890b73f7a1
commit 89a50ca24f
21 changed files with 12 additions and 481 deletions

View File

@ -451,7 +451,8 @@ export interface Tournament {
settings: ColumnType<TournamentSettings, string, string>;
id: GeneratedAlways<number>;
mapPickingStyle: TournamentMapPickingStyle;
showMapListGenerator: Generated<number | null>;
// TODO: remove in migration
// showMapListGenerator: Generated<number | null>;
castTwitchAccounts: ColumnType<string[] | null, string | null, string | null>;
castedMatchesInfo: ColumnType<
CastedMatchesInfo | null,

View File

@ -220,7 +220,6 @@ export interface Tournament {
id: number;
mapPickingStyle: TournamentMapPickingStyle;
format: TournamentFormat;
showMapListGenerator: number;
}
export interface TournamentTeam {

View File

@ -2268,7 +2268,6 @@ export const PADDLING_POOL_257 = () =>
thirdPlaceMatch: true,
isRanked: true,
},
showMapListGenerator: 0,
discordUrl: null,
castTwitchAccounts: ["dappleproductions"],
castedMatchesInfo: {
@ -8151,7 +8150,6 @@ export const PADDLING_POOL_255 = () =>
],
teamsPerGroup: 4,
},
showMapListGenerator: 0,
discordUrl: null,
castTwitchAccounts: ["dappleproductions"],
castedMatchesInfo: {
@ -14358,7 +14356,6 @@ export const IN_THE_ZONE_32 = () =>
},
],
},
showMapListGenerator: 0,
discordUrl: null,
castTwitchAccounts: ["dappleproductions", "kyochandxd"],
castedMatchesInfo: null,

View File

@ -51,7 +51,6 @@ export const testTournament = (
startTime: 1705858842,
isFinalized: 0,
name: "test",
showMapListGenerator: 0,
castTwitchAccounts: [],
staff: [],
tieBreakerMapPool: [],

View File

@ -26,7 +26,6 @@ export async function findById(id: number) {
"CalendarEvent.id as eventId",
"CalendarEvent.discordUrl",
"Tournament.settings",
"Tournament.showMapListGenerator",
"Tournament.castTwitchAccounts",
"Tournament.castedMatchesInfo",
"Tournament.mapPickingStyle",

View File

@ -12,7 +12,6 @@ select
"Tournament"."id",
"Tournament"."mapPickingStyle",
"Tournament"."settings",
"Tournament"."showMapListGenerator",
"CalendarEvent"."id" as "eventId",
"CalendarEvent"."name",
"CalendarEvent"."description",
@ -33,7 +32,7 @@ type FindByIdentifierRow = (Pick<
CalendarEvent,
"bracketUrl" | "name" | "description" | "authorId"
> &
Pick<Tournament, "id" | "mapPickingStyle" | "showMapListGenerator"> &
Pick<Tournament, "id" | "mapPickingStyle"> &
Pick<User, "discordId" | "username"> &
Pick<CalendarEventDate, "startTime">) & {
eventId: CalendarEvent["id"];

View File

@ -1,20 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/* sql */ `
update
"Tournament"
set
"showMapListGenerator" = @showMapListGenerator
where
"id" = @tournamentId;
`);
export function updateShowMapListGenerator({
tournamentId,
showMapListGenerator,
}: {
tournamentId: number;
showMapListGenerator: number;
}) {
stm.run({ tournamentId, showMapListGenerator });
}

View File

@ -1,25 +1,29 @@
import type { ActionFunction } from "@remix-run/node";
import { useFetcher, useSubmit } from "@remix-run/react";
import { useFetcher } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import invariant from "~/utils/invariant";
import { Avatar } from "~/components/Avatar";
import { Button, LinkButton } from "~/components/Button";
import { Divider } from "~/components/Divider";
import { FormMessage } from "~/components/FormMessage";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
import { Redirect } from "~/components/Redirect";
import { SubmitButton } from "~/components/SubmitButton";
import { Toggle } from "~/components/Toggle";
import { UserSearch } from "~/components/UserSearch";
import { TrashIcon } from "~/components/icons/Trash";
import { USER } from "~/constants";
import { useUser } from "~/features/auth/core/user";
import { requireUserId } from "~/features/auth/core/user.server";
import { userIsBanned } from "~/features/ban/core/banned.server";
import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server";
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
import { isAdmin } from "~/permissions";
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { databaseTimestampToDate } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import {
badRequestIfFalsy,
parseRequestFormData,
@ -36,17 +40,10 @@ import { changeTeamOwner } from "../queries/changeTeamOwner.server";
import { createTeam } from "../queries/createTeam.server";
import { deleteTeam } from "../queries/deleteTeam.server";
import { joinTeam, leaveTeam } from "../queries/joinLeaveTeam.server";
import { updateShowMapListGenerator } from "../queries/updateShowMapListGenerator.server";
import { adminActionSchema } from "../tournament-schemas.server";
import { tournamentIdFromParams } from "../tournament-utils";
import { useTournament } from "./to.$id";
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
import { Input } from "~/components/Input";
import { logger } from "~/utils/logger";
import { userIsBanned } from "~/features/ban/core/banned.server";
import { inGameNameIfNeeded } from "../tournament-utils.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { USER } from "~/constants";
import { useTournament } from "./to.$id";
export const action: ActionFunction = async ({ request, params }) => {
const user = await requireUserId(request);
@ -90,14 +87,6 @@ export const action: ActionFunction = async ({ request, params }) => {
break;
}
case "UPDATE_SHOW_MAP_LIST_GENERATOR": {
validateIsTournamentAdmin();
updateShowMapListGenerator({
tournamentId: tournament.ctx.id,
showMapListGenerator: Number(data.show),
});
break;
}
case "CHANGE_TEAM_OWNER": {
validateIsTournamentOrganizer();
const team = tournament.teamById(data.teamId);
@ -394,7 +383,6 @@ export default function TournamentAdminPage() {
<DownloadParticipants />
<Divider smallText>Bracket reset</Divider>
<BracketReset />
{isAdmin(user) ? <EnableMapList /> : null}
</div>
);
}
@ -786,30 +774,6 @@ function RemoveStaffButton({
);
}
function EnableMapList() {
const tournament = useTournament();
const submit = useSubmit();
const [eventStarted, setEventStarted] = React.useState(
Boolean(tournament.ctx.showMapListGenerator),
);
function handleToggle(toggled: boolean) {
setEventStarted(toggled);
const data = new FormData();
data.append("_action", "UPDATE_SHOW_MAP_LIST_GENERATOR");
data.append("show", toggled ? "on" : "off");
submit(data, { method: "post" });
}
return (
<div>
<label>Public map list generator tool</label>
<Toggle checked={eventStarted} setChecked={handleToggle} name="show" />
</div>
);
}
function DownloadParticipants() {
const tournament = useTournament();

View File

@ -1,387 +0,0 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { useActionData, useLoaderData } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Alert } from "~/components/Alert";
import type { MapPoolMap } from "~/db/types";
import { useUser } from "~/features/auth/core/user";
import { getUserId } from "~/features/auth/core/user.server";
import { MapPool } from "~/features/map-list-generator/core/map-pool";
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import type {
BracketType,
TournamentMapListMap,
TournamentMaplistInput,
TournamentMaplistSource,
} from "~/modules/tournament-map-list-generator";
import { createTournamentMapList } from "~/modules/tournament-map-list-generator";
import { type SendouRouteHandle } from "~/utils/remix";
import { tournamentPage } from "~/utils/urls";
import { findMapPoolsByTournamentId } from "../queries/findMapPoolsByTournamentId.server";
import { TOURNAMENT } from "../tournament-constants";
import { tournamentIdFromParams } from "../tournament-utils";
import { useTournament } from "./to.$id";
import "~/styles/maps.css";
export const handle: SendouRouteHandle = {
i18n: ["tournament"],
};
type TeamInState = {
id: number;
mapPool?: Pick<MapPoolMap, "mode" | "stageId">[] | null;
};
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const tournamentId = tournamentIdFromParams(params);
const user = await getUserId(request);
const tournament = await tournamentFromDB({ tournamentId, user });
const mapListGeneratorAvailable =
tournament.isAdmin(user) || tournament.ctx.showMapListGenerator;
if (!mapListGeneratorAvailable) {
throw redirect(tournamentPage(tournamentId));
}
return {
mapPools: findMapPoolsByTournamentId(tournamentId),
};
};
export default function TournamentMapsPage() {
const user = useUser();
const { t } = useTranslation(["tournament"]);
const actionData = useActionData<{ failed?: boolean }>();
const data = useLoaderData<typeof loader>();
const tournament = useTournament();
const [bestOf, setBestOf] = useSearchParamState<
(typeof TOURNAMENT)["AVAILABLE_BEST_OF"][number]
>({
name: "bo",
defaultValue: 3,
revive: reviveBestOf,
});
const [teamOneId, setTeamOneId] = useSearchParamState({
name: "team-one",
defaultValue:
tournament.ownedTeamByUser(user)?.id ?? tournament.ctx.teams[0]?.id,
revive: reviveTeam(tournament.ctx.teams.map((t) => t.id)),
});
const [teamTwoId, setTeamTwoId] = useSearchParamState({
name: "team-two",
defaultValue: tournament.ctx.teams[1]?.id,
revive: reviveTeam(
tournament.ctx.teams.map((t) => t.id),
teamOneId,
),
});
const [roundNumber, setRoundNumber] = useSearchParamState({
name: "round",
defaultValue: 1,
revive: reviveRound,
});
const [bracketType, setBracketType] = useSearchParamState<BracketType>({
name: "bracket",
defaultValue: "DE_WINNERS",
revive: reviveBracketType,
});
const teamOne = tournament.ctx.teams.find((t) => t.id === teamOneId) ?? {
id: -1,
mapPool: [],
};
const teamTwo = tournament.ctx.teams.find((t) => t.id === teamTwoId) ?? {
id: -1,
mapPool: [],
};
const teamOneMaps =
data.mapPools.find((mp) => mp.tournamentTeamId === teamOne.id)?.mapPool ??
[];
const teamTwoMaps =
data.mapPools.find((mp) => mp.tournamentTeamId === teamTwo.id)?.mapPool ??
[];
return (
<div className="stack md">
{actionData?.failed && (
<Alert variation="ERROR" tiny>
{t("tournament:generator.error")}
</Alert>
)}
<RoundSelect
roundNumber={roundNumber}
bracketType={bracketType}
handleChange={(roundNumber, bracketType) => {
setRoundNumber(roundNumber);
setBracketType(bracketType);
}}
/>
<div className="tournament__teams-container">
<TeamsSelect
number={1}
team={teamOne}
otherTeam={teamTwo}
setTeam={setTeamOneId}
/>
<TeamsSelect
number={2}
team={teamTwo}
otherTeam={teamOne}
setTeam={setTeamTwoId}
/>
</div>
<BestOfRadios bestOf={bestOf} setBestOf={setBestOf} />
<MapList
teams={[
{ ...teamOne, maps: new MapPool(teamOneMaps) },
{ ...teamTwo, maps: new MapPool(teamTwoMaps) },
]}
count={bestOf}
seed={`${bracketType}-${roundNumber}`}
modesIncluded={tournament.modesIncluded}
/>
</div>
);
}
const BRACKET_TYPES: Array<BracketType> = ["DE_WINNERS", "DE_LOSERS"];
const AMOUNT_OF_ROUNDS = 12;
function reviveBestOf(value: string) {
const parsed = Number(value);
return TOURNAMENT.AVAILABLE_BEST_OF.find((bo) => bo === parsed);
}
function reviveBracketType(value: string) {
return BRACKET_TYPES.find((bracketType) => bracketType === value);
}
function reviveRound(value: string) {
const parsed = Number(value);
return new Array(AMOUNT_OF_ROUNDS)
.fill(null)
.map((_, i) => i + 1)
.find((val) => val === parsed);
}
function reviveTeam(teamIds: number[], excludedTeamId?: number) {
return function (value: string) {
const parsed = Number(value);
return teamIds
.filter((id) => id !== excludedTeamId)
.find((id) => id === parsed);
};
}
function RoundSelect({
roundNumber,
bracketType,
handleChange,
}: {
roundNumber: number;
bracketType: string;
handleChange: (roundNumber: number, bracketType: BracketType) => void;
}) {
const { t } = useTranslation(["tournament"]);
return (
<div className="tournament__round-container tournament__select-container">
<label htmlFor="round">{t("tournament:round.label")}</label>
<select
id="round"
value={`${bracketType}-${roundNumber}`}
onChange={(e) => {
const [bracketType, roundNumber] = e.target.value.split("-") as [
BracketType,
string,
];
handleChange(Number(roundNumber), bracketType);
}}
>
{BRACKET_TYPES.flatMap((type) =>
new Array(AMOUNT_OF_ROUNDS).fill(null).map((_, i) => {
return (
<option key={`${type}-${i}`} value={`${type}-${i + 1}`}>
{t(`tournament:bracket.type.${type}`)} {i + 1}
</option>
);
}),
)}
</select>
</div>
);
}
function TeamsSelect({
number,
team,
otherTeam,
setTeam,
}: {
number: number;
team: { id: number };
otherTeam: TeamInState;
setTeam: (newTeamId: number) => void;
}) {
const { t } = useTranslation(["tournament"]);
const tournament = useTournament();
return (
<div className="tournament__select-container">
<label htmlFor="round">
{t("tournament:team.label")} {number}
</label>
<select
id="round"
className="tournament__team-select"
value={team.id}
onChange={(e) => {
setTeam(Number(e.target.value));
}}
>
<option value={-1}>({t("tournament:team.unlisted")})</option>
{tournament.ctx.teams
.filter((t) => t.id !== otherTeam.id)
.map((team) => (
<option key={team.id} value={team.id}>
{team.name}
</option>
))}
</select>
</div>
);
}
function BestOfRadios({
bestOf,
setBestOf,
}: {
bestOf: (typeof TOURNAMENT)["AVAILABLE_BEST_OF"][number];
setBestOf: (bestOf: (typeof TOURNAMENT)["AVAILABLE_BEST_OF"][number]) => void;
}) {
const { t } = useTranslation(["tournament"]);
return (
<div className="tournament__bo-radios-container">
{TOURNAMENT.AVAILABLE_BEST_OF.map((bestOfOption) => (
<div key={bestOfOption}>
<label htmlFor={String(bestOfOption)}>
{t("tournament:bestOf.label.short")}
{bestOfOption}
</label>
<input
id={String(bestOfOption)}
name="bestOf"
type="radio"
checked={bestOfOption === bestOf}
onChange={() => setBestOf(bestOfOption)}
/>
</div>
))}
</div>
);
}
function MapList(props: Omit<TournamentMaplistInput, "tiebreakerMaps">) {
const { t } = useTranslation(["game-misc"]);
const tournament = useTournament();
let mapList: Array<TournamentMapListMap>;
try {
mapList = createTournamentMapList({
...props,
tiebreakerMaps: new MapPool(tournament.ctx.tieBreakerMapPool),
});
} catch (e) {
console.error(
"Failed to create map list. Falling back to default maps.",
e,
);
mapList = createTournamentMapList({
...props,
teams: [
{
id: -1,
maps: new MapPool([]),
},
{
id: -2,
maps: new MapPool([]),
},
],
tiebreakerMaps: new MapPool(tournament.ctx.tieBreakerMapPool),
});
}
return (
<div className="tournament__map-list">
{mapList.map(({ stageId, mode, source }, i) => {
return (
<React.Fragment key={`${stageId}-${mode}`}>
<PickInfoText
source={source}
teamOneId={props.teams[0].id}
teamTwoId={props.teams[1].id}
/>
<div key={stageId} className="tournament__stage-listed">
{i + 1}) {mode} {t(`game-misc:STAGE_${stageId}`)}
</div>
</React.Fragment>
);
})}
</div>
);
}
function PickInfoText({
source,
teamOneId,
teamTwoId,
}: {
source: TournamentMaplistSource;
teamOneId: number;
teamTwoId: number;
}) {
const { t } = useTranslation(["tournament"]);
const text = () => {
if (source === teamOneId) {
return t("tournament:pickInfo.team", { number: 1 });
}
if (source === teamTwoId) {
return t("tournament:pickInfo.team", { number: 2 });
}
if (source === "TIEBREAKER") return t("tournament:pickInfo.tiebreaker");
if (source === "BOTH") return t("tournament:pickInfo.both");
if (source === "DEFAULT") return t("tournament:pickInfo.default");
if (source === "COUNTERPICK") return t("tournament:pickInfo.counterpick");
if (source === "TO") return "";
console.error(`Unknown source: ${String(source)}`);
return "";
};
const otherClassName = () => {
if (source === teamOneId) return "team-1";
if (source === teamTwoId) return "team-2";
return typeof source === "string" ? source.toLocaleLowerCase() : source;
};
return (
<div className={clsx("tournament__pick-info", otherClassName())}>
{text()}
</div>
);
}

View File

@ -211,9 +211,6 @@ export default function TournamentLayout() {
<SubNavLink to="brackets" data-testid="brackets-tab" prefetch="render">
{t("tournament:tabs.brackets")}
</SubNavLink>
{tournament.ctx.showMapListGenerator ? (
<SubNavLink to="maps">{t("tournament:tabs.maps")}</SubNavLink>
) : null}
<SubNavLink to="teams" end={false} prefetch="render">
{t("tournament:tabs.teams", { count: tournament.ctx.teams.length })}
</SubNavLink>

View File

@ -53,10 +53,6 @@ export const seedsActionSchema = z.object({
});
export const adminActionSchema = z.union([
z.object({
_action: _action("UPDATE_SHOW_MAP_LIST_GENERATOR"),
show: z.preprocess(checkboxValueToBoolean, z.boolean()),
}),
z.object({
_action: _action("CHANGE_TEAM_OWNER"),
teamId: id,

View File

@ -3,7 +3,6 @@
"tabs.teams": "Hold ({{count}})",
"tabs.admin": "Admin",
"tabs.register": "Register",
"tabs.maps": "Baner",
"tabs.brackets": "Grupper",
"tabs.seeds": "Seedninger",
"tabs.streams": "Streams ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "Teams ({{count}})",
"tabs.admin": "Admin",
"tabs.register": "Register",
"tabs.maps": "Maps",
"tabs.brackets": "Brackets",
"tabs.seeds": "Seeds",
"tabs.streams": "Streams ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "Equipos ({{count}})",
"tabs.admin": "Admin",
"tabs.register": "Registrar",
"tabs.maps": "Mapas",
"tabs.brackets": "Cuadros de Torneo",
"tabs.seeds": "Listas de Equipos",
"tabs.streams": "Streams ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "Equipos ({{count}})",
"tabs.admin": "Admin",
"tabs.register": "Registrar",
"tabs.maps": "Mapas",
"tabs.brackets": "Cuadros de Torneo",
"tabs.seeds": "Listas de Equipos",
"tabs.streams": "Streams ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "Équipes ({{count}})",
"tabs.admin": "Admin",
"tabs.register": "Registration",
"tabs.maps": "Stages",
"tabs.brackets": "Brackets",
"tabs.seeds": "Seeds",
"tabs.streams": "Diffusions ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "צוותים ({{count}})",
"tabs.admin": "מנהל",
"tabs.register": "הרשמה",
"tabs.maps": "מפות",
"tabs.brackets": "מערכים",
"tabs.seeds": "דירוג",
"tabs.streams": "שידורים חיים ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "チーム ({{count}})",
"tabs.admin": "管理",
"tabs.register": "登録",
"tabs.maps": "マップ一覧",
"tabs.brackets": "ブラケット",
"tabs.seeds": "シード",
"tabs.streams": "配信 ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "Times ({{count}})",
"tabs.admin": "Administrador",
"tabs.register": "Registrar",
"tabs.maps": "Mapas",
"tabs.brackets": "Brackets",
"tabs.seeds": "Sementes",
"tabs.streams": "Transmissões ({{count}})",

View File

@ -3,7 +3,6 @@
"tabs.teams": "参赛队伍 ({{count}})",
"tabs.admin": "管理",
"tabs.register": "报名",
"tabs.maps": "地图",
"tabs.brackets": "对战表",
"tabs.seeds": "种子",
"tabs.streams": "直播 ({{count}})",

View File

@ -154,10 +154,6 @@ export default defineConfig(() => {
"/to/:id/seeds",
"features/tournament/routes/to.$id.seeds.tsx",
);
route(
"/to/:id/maps",
"features/tournament/routes/to.$id.maps.tsx",
);
route(
"/to/:id/streams",
"features/tournament/routes/to.$id.streams.tsx",