Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions pages/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import {
import { ResetPlayersModal } from "../src/ResetPlayersModal";
import { PlayerNameEdit } from "../src/PlayerNameEdit";
import { disambiguateNames, renameWithDisambiguation } from "../src/playerNames";
import { GroupsEditor } from "../src/GroupsEditor";
import {
assignNewPlayersToStandard,
defaultGroupsState,
ensurePairInHighestGroup,
GroupsState,
levelFixedPairsForGroups,
normalizeGroupsState,
sanitizePlayerGroups,
} from "../src/groups";
import { v4 as uuidv4 } from "uuid";

type NamePair = [string, string];
Expand Down Expand Up @@ -86,6 +96,9 @@ function NewGame() {
const [courtNames, setCourtNames] = useState<string[]>([]);
const [fixedPairs, setFixedPairs] = useState<NamePair[]>([]);
const [linkingPlayer, setLinkingPlayer] = useState<string | null>(null);
const [groupsState, setGroupsState] = useState<GroupsState>(
defaultGroupsState()
);

const applySetupDisambiguation = (
roster: SetupPlayer[],
Expand Down Expand Up @@ -114,7 +127,16 @@ function NewGame() {
setPlayers((current) => {
const before = current;
const added = names.map((name) => ({ id: uuidv4(), name }));
return applySetupDisambiguation([...current, ...added], before);
const next = applySetupDisambiguation([...current, ...added], before);
if (groupsState.enabled) {
setGroupsState((gs) =>
assignNewPlayersToStandard(
gs,
added.map((p) => p.id)
)
);
}
return next;
});
setPlayerInput("");
playerInputRef.current?.focus();
Expand Down Expand Up @@ -147,7 +169,16 @@ function NewGame() {
.filter((pair): pair is NamePair => pair !== null);
setFixedPairs(namePairs);
}
}, [state.players, state.courts, state.courtNames, state.fixedPairs, playersById]);

setGroupsState(normalizeGroupsState(state.groups));
}, [
state.players,
state.courts,
state.courtNames,
state.fixedPairs,
state.groups,
playersById,
]);

const handleNewGame = async () => {
const names = players.map((p) => p.name);
Expand All @@ -168,11 +199,23 @@ function NewGame() {
setFormStatus("validating");
return;
}
const playerIds = players.map((p) => p.id);
const nameToId = Object.fromEntries(players.map((p) => [p.name, p.id]));
const pairTeams = fixedPairs
.map(([a, b]) => [nameToId[a], nameToId[b]] as [string, string])
.filter(([a, b]) => a && b);
let setupGroups = groupsState;
if (setupGroups.enabled) {
setupGroups = sanitizePlayerGroups(setupGroups, playerIds);
setupGroups = levelFixedPairsForGroups(pairTeams, setupGroups);
}

await newGame(dispatch, state, worker, {
names,
courts: courtCount,
courtNames: customizeCourtNames ? courtNames : [],
fixedPairs,
groups: setupGroups,
});
router.push("/rounds");
};
Expand Down Expand Up @@ -200,6 +243,13 @@ function NewGame() {
}

setFixedPairs(addPair(fixedPairs, linkingPlayer, name));
if (groupsState.enabled) {
const idA = players.find((p) => p.name === linkingPlayer)?.id;
const idB = players.find((p) => p.name === name)?.id;
if (idA && idB) {
setGroupsState((gs) => ensurePairInHighestGroup(idA, idB, gs));
}
}
setLinkingPlayer(null);
};

Expand Down Expand Up @@ -553,6 +603,14 @@ function NewGame() {
</ol>
</>
)}
<Spacer y={3} />
<GroupsEditor
groupsState={groupsState}
onChange={setGroupsState}
players={players}
showEnableToggle
enableToggleLabel="Enable skill groups"
/>
<Spacer y={4} />
<Button
onPress={() => {
Expand Down
60 changes: 56 additions & 4 deletions pages/rounds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import {
Spacer,
Pagination,
CardBody,
Divider,
Select,
SelectItem,
Tooltip,
} from "@nextui-org/react";
import Head from "next/head";
import React, { useEffect, useState } from "react";
import { Edit, People } from "react-iconly";
import { Edit, People, User } from "react-iconly";
import { BadgeGroup } from "../src/BadgeGroup";
import { Court } from "../src/Court";
import { CourtsModal } from "../src/CourtsModal";
import { GroupsModal } from "../src/GroupsModal";
import { GroupPlayMode } from "../src/groups";
import { PlayerBadge } from "../src/PlayerBadge";
import { PlayersModal } from "../src/PlayersModal";
import { SitoutsModal } from "../src/SitoutsModal";
Expand All @@ -21,6 +24,8 @@ import {
editCourts,
editPlayers,
newRound,
setNextRoundPlayMode,
updateGroups,
useShufflerDispatch,
useShufflerState,
useShufflerWorker,
Expand All @@ -34,6 +39,7 @@ export default function Rounds() {
const [sitoutModal, setSitoutModal] = useState(false);
const [playersModal, setPlayersModal] = useState(false);
const [courtsModal, setCourtsModal] = useState(false);
const [groupsModal, setGroupsModal] = useState(false);

const [roundIndex, setRoundIndex] = useState(0);
const prevRoundCount = React.useRef(state.rounds.length);
Expand Down Expand Up @@ -63,6 +69,9 @@ export default function Rounds() {
const volunteers = state.volunteerSitoutsByRound[displayIndex];
const { sitOuts = [], matches = [] } = round || {};
const isHistoricalRound = displayIndex < state.rounds.length - 1;
const isLatestRound = displayIndex === state.rounds.length - 1;
const nextPlayMode =
state.nextRoundPlayMode ?? state.groups.playMode ?? "separate";

const playerName = (id: string) => {
if (isHistoricalRound && round?.playerNamesById?.[id]) {
Expand Down Expand Up @@ -116,15 +125,24 @@ export default function Rounds() {
<PlayersModal
open={playersModal}
onClose={() => setPlayersModal(false)}
onSubmit={async (newPlayers, fixedPairs, regenerate) => {
onSubmit={async (newPlayers, fixedPairs, groups, regenerate) => {
await editPlayers(dispatch, state, worker, {
newPlayers,
fixedPairs,
groups,
regenerate,
});
setPlayersModal(false);
}}
/>
<GroupsModal
open={groupsModal}
onClose={() => setGroupsModal(false)}
onSubmit={(groups) => {
updateGroups(dispatch, groups);
setGroupsModal(false);
}}
/>
<CourtsModal
open={courtsModal}
onClose={() => setCourtsModal(false)}
Expand Down Expand Up @@ -153,14 +171,24 @@ export default function Rounds() {
{state.players.length}
</Button>
<Button
aria-label={`${state.players.length} players`}
aria-label={`${state.courts} courts`}
startContent={<Court />}
className="-mt-2"
color="primary"
onPress={() => setCourtsModal(true)}
>
{state.courts}
</Button>
<Button
aria-label="Skill groups"
startContent={<User />}
className="-mt-2"
color={state.groups.enabled ? "secondary" : "default"}
variant={state.groups.enabled ? "solid" : "flat"}
onPress={() => setGroupsModal(true)}
>
Groups
</Button>
</>
) : (
<Button
Expand Down Expand Up @@ -271,13 +299,37 @@ export default function Rounds() {
/>
</div>
<Spacer y={1.5} />
{state.groups.enabled && isLatestRound ? (
<div className="flex justify-center mb-4 px-4">
<Select
label="Next round play mode"
aria-label="Play mode for the next round"
className="max-w-xs"
selectedKeys={[nextPlayMode]}
onSelectionChange={(keys) => {
const selected = Array.from(keys)[0] as
| GroupPlayMode
| undefined;
if (selected) setNextRoundPlayMode(dispatch, selected);
}}
>
<SelectItem key="separate" value="separate">
Separate groups
</SelectItem>
<SelectItem key="combined" value="combined">
Combined groups
</SelectItem>
</Select>
</div>
) : null}
<div className="flex justify-around">
<Button
size="lg"
isDisabled={state.generating}
onPress={async () => {
await newRound(dispatch, state, worker, {
volunteerSitouts: [],
playMode: nextPlayMode,
});
}}
className="bg-gradient-to-l from-blue-600 to-pink-600 text-white"
Expand Down
Loading