Edit map list stages functionality

This commit is contained in:
Kalle (Sendou)
2021-12-17 22:39:00 +02:00
parent 33155e2593
commit 7b196384cc
5 changed files with 211 additions and 11 deletions

View File

@@ -1,3 +1,5 @@
import { Mode } from ".prisma/client";
export const stages = [
"The Reef",
"Musselforge Fitness",
@@ -24,7 +26,14 @@ export const stages = [
"Skipper Pavilion",
];
export const modesShort = ["TW", "SZ", "TC", "RM", "CB"];
export const modesShort: Mode[] = ["TW", "SZ", "TC", "RM", "CB"];
export const modesShortToLong: Record<Mode, string> = {
TW: "Turf War",
SZ: "Splat Zones",
TC: "Tower Control",
RM: "Rainmaker",
CB: "Clam Blitz",
};
export const navItems = [
{

View File

@@ -1,4 +1,6 @@
import clone from "just-clone";
import * as React from "react";
import invariant from "tiny-invariant";
import { generateMapListForRounds } from "~/core/tournament/mapList";
import type {
UseTournamentRoundsAction,
@@ -10,16 +12,70 @@ export function useTournamentRounds(args: UseTournamentRoundsArgs) {
return React.useReducer(
(oldState: UseTournamentRoundsState, action: UseTournamentRoundsAction) => {
switch (action.type) {
case "REGENERATE_MAP_LIST":
case "START_EDITING_ROUND": {
const newState = clone(oldState);
newState[action.data.side] = newState[action.data.side].map(
(round, i) =>
i === action.data.index
? { ...round, editing: true, newMapList: [...round.mapList] }
: round
);
return newState;
}
case "CANCEL_EDITING_ROUND": {
const newState = clone(oldState);
newState[action.data.side] = newState[action.data.side].map(
(round, i) =>
i === action.data.index ? { ...round, editing: false } : round
);
return newState;
}
case "SAVE_ROUND": {
const newState = clone(oldState);
newState[action.data.side] = newState[action.data.side].map(
(round, i) => {
if (i !== action.data.index) return round;
invariant(round.newMapList, "round.newMapList is undefined");
return { ...round, editing: false, mapList: round.newMapList };
}
);
return newState;
}
case "EDIT_STAGE": {
const newState = clone(oldState);
const roundToEdit = newState[action.data.side].find(
(_, i) => i === action.data.index
);
invariant(
roundToEdit?.editing,
"roundToEdit is undefined or doesn't have editing attribute"
);
let newMapList = roundToEdit.newMapList;
if (!newMapList) {
newMapList = roundToEdit.mapList;
}
newMapList[action.data.stageNumber - 1] = action.data.newStage;
return newState;
}
case "REGENERATE_MAP_LIST": {
return regenMapList(oldState);
case "SET_ROUND_BEST_OF":
const newState = { ...oldState };
}
case "SET_ROUND_BEST_OF": {
const newState = clone(oldState);
newState[action.data.side][action.data.index].bestOf =
action.data.newBestOf;
return regenMapList(newState);
default:
}
default: {
return oldState;
}
}
},
args.initialState

View File

@@ -11,6 +11,8 @@ export type UseTournamentRoundsState = EliminationBracket<
bestOf: BestOf;
name: string;
mapList: Stage[];
newMapList?: Stage[];
editing?: boolean;
}[]
>;
@@ -21,6 +23,27 @@ export interface UseTournamentRoundsArgs {
export type UseTournamentRoundsAction =
| MyReducerAction<"REGENERATE_MAP_LIST">
| MyReducerAction<
"START_EDITING_ROUND",
{ side: EliminationBracketSide; index: number }
>
| MyReducerAction<
"EDIT_STAGE",
{
side: EliminationBracketSide;
index: number;
stageNumber: number;
newStage: Stage;
}
>
| MyReducerAction<
"SAVE_ROUND",
{ side: EliminationBracketSide; index: number }
>
| MyReducerAction<
"CANCEL_EDITING_ROUND",
{ side: EliminationBracketSide; index: number }
>
| MyReducerAction<
"SET_ROUND_BEST_OF",
{ newBestOf: BestOf; side: EliminationBracketSide; index: number }

View File

@@ -1,8 +1,10 @@
import type { Mode, Stage } from ".prisma/client";
import classNames from "classnames";
import type { LinksFunction, LoaderFunction } from "remix";
import { json, useLoaderData } from "remix";
import invariant from "tiny-invariant";
import { Button } from "~/components/Button";
import { modesShort, modesShortToLong } from "~/constants";
import { eliminationBracket } from "~/core/tournament/algorithms";
import {
EliminationBracketSide,
@@ -17,6 +19,8 @@ import type {
import { findTournamentByNameForUrl } from "~/services/tournament";
import startBracketTabStylesUrl from "~/styles/tournament-start.css";
// TODO: some warning if trying to continue and some round card is editing
// TODO: add round numbers
// 4) can change any invidual map via dropdown (no regen)
export const links: LinksFunction = () => {
@@ -66,14 +70,14 @@ export default function StartBracketTab() {
<div className="tournament__start__round-collections-container">
<RoundsCollection
side="winners"
rounds={rounds.winners}
dispatch={dispatch}
rounds={rounds.winners}
/>
{args.initialState.losers.length > 0 && (
<RoundsCollection
side="losers"
rounds={rounds.losers}
dispatch={dispatch}
rounds={rounds.losers}
/>
)}
</div>
@@ -102,13 +106,15 @@ function ActionButtons({
function RoundsCollection({
side,
rounds,
dispatch,
rounds,
}: {
side: EliminationBracketSide;
rounds: UseTournamentRoundsState["winners"];
dispatch: React.Dispatch<UseTournamentRoundsAction>;
rounds: UseTournamentRoundsState["winners"];
}) {
const { mapPool } = useLoaderData<UseTournamentRoundsArgs>();
return (
<div>
<h2 className="tournament__start__title">{side}</h2>
@@ -141,9 +147,54 @@ function RoundsCollection({
))}
</div>
<ol className="tournament__start__rounds-list">
{round.mapList.map((stage) => {
{round.mapList.map((stage, stageI) => {
if (round.editing) {
return (
<li className="tournament__start__map-row" key={stage.id}>
<select
value={round.newMapList?.[stageI].id ?? stage.id}
onChange={(e) => {
const newStage = mapPool.find(
(stage) => stage.id === Number(e.target.value)
);
invariant(newStage, "newStage not found");
dispatch({
type: "EDIT_STAGE",
data: {
index: roundIndex,
newStage,
side,
stageNumber: stageI + 1,
},
});
}}
>
{modesShort.flatMap((mode) => {
const stages = stagesForSelect()[mode];
if (!stages) return [];
return (
<optgroup
key={mode}
label={modesShortToLong[mode]}
>
{stages.map((stage) => (
<option key={stage.id} value={stage.id}>
{stage.mode} {stage.name}
</option>
))}
</optgroup>
);
})}
</select>
</li>
);
}
return (
<li className="tournament__start__map-row" key={stage.id}>
<li
className="tournament__start__map-row"
key={"" + stageI + stage.id}
>
<img
src={`/img/modes/${stage.mode}.webp`}
className="tournament__start__mode-image"
@@ -155,10 +206,64 @@ function RoundsCollection({
);
})}
</ol>
<div className="tournament__start__round-card-buttons-container">
{round.editing ? (
<>
<Button
variant="minimal"
tiny
onClick={() =>
dispatch({
type: "SAVE_ROUND",
data: { side, index: roundIndex },
})
}
>
Save
</Button>
<Button
variant="minimal-destructive"
tiny
onClick={() =>
dispatch({
type: "CANCEL_EDITING_ROUND",
data: { side, index: roundIndex },
})
}
>
Cancel
</Button>
</>
) : (
<Button
variant="minimal"
tiny
onClick={() =>
dispatch({
type: "START_EDITING_ROUND",
data: { side, index: roundIndex },
})
}
>
Edit maps
</Button>
)}
</div>
</section>
);
})}
</div>
</div>
);
function stagesForSelect() {
return [...mapPool]
.sort((a, b) => a.id - b.id)
.reduce((acc: Partial<Record<Mode, Stage[]>>, cur) => {
const stages = acc[cur.mode] ?? [];
stages.push(cur);
acc[cur.mode] = stages;
return acc;
}, {});
}
}

View File

@@ -67,6 +67,13 @@
gap: var(--s-2);
}
.tournament__start__round-card-buttons-container {
display: flex;
justify-content: center;
gap: var(--s-4);
padding-block-start: var(--s-4);
}
.tournament__start__mode-image {
width: attr(width);
}