mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-08 12:16:12 -05:00
Edit map list stages functionality
This commit is contained in:
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user