modaq
Version:
Quiz Bowl Reader using TypeScript, React, and MobX
713 lines • 29.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameBar = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const React = __importStar(require("react"));
const mobx_react_lite_1 = require("mobx-react-lite");
const react_1 = require("@fluentui/react");
const BonusQuestionController = __importStar(require("./BonusQuestionController"));
const ReorderTeamsDialogController = __importStar(require("./dialogs/ReorderTeamsDialogController"));
const TossupQuestionController = __importStar(require("./TossupQuestionController"));
require("../state/GameState");
require("../state/UIState");
require("../state/Cycle");
require("../state/PacketState");
require("../state/TeamState");
require("../state/AppState");
require("../state/Events");
const StateContext_1 = require("../contexts/StateContext");
const StatusDisplayType_1 = require("../state/StatusDisplayType");
const overflowProps = { ariaLabel: "More" };
exports.GameBar = mobx_react_lite_1.observer(function GameBar() {
// This should pop up the new game handler
const appState = React.useContext(StateContext_1.StateContext);
const uiState = appState.uiState;
const game = appState.game;
const newGameHandler = React.useCallback(() => {
if (appState.game.hasUpdates) {
// Prompt the user
uiState.dialogState.showYesNoCancelMessageDialog("Export Game?", "The game has changes that haven't been exported. Would you like to export the game before starting a new one?", () => {
// Open the export dialog, depending on if they have sheets. We should ideally abstract this logic
// to another method
if (appState.uiState.customExportOptions != undefined) {
appState.handleCustomExport(StatusDisplayType_1.StatusDisplayType.MessageDialog, "NewGame");
}
else if (appState.uiState.sheetsState.sheetId != undefined) {
exportToSheets(appState);
}
else {
// Manual export
appState.uiState.dialogState.showExportToJsonDialog();
}
}, () => {
// User doesn't want any updates, so clear them
appState.game.markUpdateComplete();
uiState.createPendingNewGame();
uiState.dialogState.showNewGameDialog();
});
}
else {
uiState.createPendingNewGame();
uiState.dialogState.showNewGameDialog();
}
}, [appState, uiState]);
const importGameHandler = React.useCallback(() => {
uiState.createPendingNewGame();
uiState.dialogState.showImportGameDialog();
}, [uiState]);
const importFromQBJHandler = React.useCallback(() => {
uiState.dialogState.showImportFromQBJDialog();
}, [uiState]);
const protestBonusHandler = React.useCallback(() => {
// Issue: pending protest needs existing index. Need to update it to include the part number
const cycle = game.cycles[uiState.cycleIndex];
if ((cycle === null || cycle === void 0 ? void 0 : cycle.bonusAnswer) == undefined) {
return;
}
const bonusIndex = game.getBonusIndex(uiState.cycleIndex);
const bonus = game.packet.bonuses[bonusIndex];
if (bonus == undefined) {
// Something is wrong... the bonus is undefined, but this handler can be accessed?
throw new Error(`Impossible to add bonus protest for bonus question ${bonusIndex}`);
}
const protestableParts = cycle.getProtestableBonusPartIndexes(bonus.parts.length);
uiState.setPendingBonusProtest(cycle.bonusAnswer.receivingTeamName, bonusIndex, protestableParts[0]);
}, [game, uiState]);
const addPlayerHandler = React.useCallback(() => {
uiState.createPendingNewPlayer(game.teamNames[0]);
}, [uiState, game]);
const addQuestionsHandler = React.useCallback(() => {
uiState.dialogState.showAddQuestionsDialog();
}, [uiState]);
const openHelpHandler = React.useCallback(() => appState.uiState.dialogState.showHelpDialog(), [appState]);
const reorderPlayersHandler = React.useCallback(() => {
uiState.dialogState.showReorderPlayersDialog(game.players);
}, [uiState, game]);
const reorderTeamsHandler = React.useCallback(() => {
uiState.dialogState.showOKCancelMessageDialog("Reorder teams", "Swap the order of teams?", ReorderTeamsDialogController.submit);
}, [uiState]);
const renameTeamHandler = React.useCallback(() => {
if (game.players.length === 0) {
return;
}
uiState.dialogState.showRenameTeamDialog(game.players[0].teamName);
}, [uiState, game]);
const items = appState.uiState.hideNewGame
? []
: [
{
key: "newGame",
text: "New game",
iconProps: { iconName: "Add" },
split: true,
subMenuProps: {
items: [
{
key: "newGameSubMenuItem",
text: "New game...",
iconProps: { iconName: "Add" },
onClick: newGameHandler,
},
{
key: "importQBJ",
text: "Import from QBJ...",
iconProps: { iconName: "Download" },
onClick: importFromQBJHandler,
},
{
key: "importGame",
text: "Import raw game...",
iconProps: { iconName: "Download" },
onClick: importGameHandler,
},
],
},
onClick: newGameHandler,
},
];
const optionsSubMenuItems = getOptionsSubMenuItems(appState);
items.push({
key: "options",
text: "Options",
subMenuProps: {
items: optionsSubMenuItems,
},
});
const viewSubMenuItems = getViewSubMenuItems(appState);
items.push({
key: "view",
text: "View",
subMenuProps: {
items: viewSubMenuItems,
},
});
// TODO: Look into memoizing; React.useMemo with just props doesn't seem to recognize when the cycle changes.
const actionSubMenuItems = getActionSubMenuItems(appState, addPlayerHandler, protestBonusHandler, reorderPlayersHandler, reorderTeamsHandler, renameTeamHandler, addQuestionsHandler);
items.push({
key: "actions",
text: "Actions",
subMenuProps: {
items: actionSubMenuItems,
},
});
// If a custom export option is given, only show a button for that export
if (appState.uiState.customExportOptions == undefined) {
const exportSubMenuItems = getExportSubMenuItems(appState);
items.push({
key: "export",
text: "Export",
subMenuProps: {
items: exportSubMenuItems,
},
});
}
else {
// This should be a split button, with custom as the default item
const handleCustomExport = () => {
appState.handleCustomExport(StatusDisplayType_1.StatusDisplayType.MessageDialog, "Menu");
};
items.push({
key: "export",
text: appState.uiState.customExportOptions.label,
disabled: appState.game.cycles.length === 0,
split: true,
onClick: handleCustomExport,
subMenuProps: {
items: [
{
key: "exportSubMenuItem",
text: appState.uiState.customExportOptions.label,
disabled: appState.game.cycles.length === 0,
onClick: handleCustomExport,
},
{
key: "downloadJson",
text: "Backup to JSON...",
disabled: appState.game.cycles.length === 0,
onClick: () => {
appState.uiState.dialogState.showExportToJsonDialog();
},
},
],
},
});
}
items.push({
key: "help",
text: "Help...",
onClick: openHelpHandler,
});
return jsx_runtime_1.jsx(react_1.CommandBar, { items: items, overflowButtonProps: overflowProps }, void 0);
});
function exportToSheets(appState) {
return __awaiter(this, void 0, void 0, function* () {
appState.uiState.createPendingSheet();
return;
});
}
function getActionSubMenuItems(appState, addPlayerHandler, protestBonusHandler, reorderPlayersHandler, reorderTeamsHandler, renameTeamHandler, addQuestionsHandler) {
const items = [];
const uiState = appState.uiState;
const game = appState.game;
const playerManagementSection = getPlayerManagementSubMenuItems(appState, game, uiState, addPlayerHandler, reorderPlayersHandler, reorderTeamsHandler, renameTeamHandler);
items.push(playerManagementSection);
const protestsSection = getProtestSubMenuItems(appState, game, uiState, protestBonusHandler);
items.push(protestsSection);
const removeQuestionSection = {
key: "removeQuestionSection",
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "Remove Question",
items: [
{
key: "removeTossup",
text: "Throw out tossup",
onClick: () => TossupQuestionController.throwOutTossup(appState.game.cycles[appState.uiState.cycleIndex], appState.game.getTossupIndex(appState.uiState.cycleIndex) + 1),
disabled: appState.game.cycles.length === 0,
},
{
key: "removeBonus",
text: "Throw out bonus",
onClick: () => BonusQuestionController.throwOutBonus(appState.game.cycles[appState.uiState.cycleIndex], appState.game.getBonusIndex(appState.uiState.cycleIndex)),
disabled: appState.game.cycles.length === 0,
},
],
},
};
items.push(removeQuestionSection);
const packetSection = {
key: "packetSection",
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "Packet",
items: [
{
key: "addMoreQuestions",
text: "Add questions...",
onClick: addQuestionsHandler,
disabled: appState.game.cycles.length === 0,
},
],
},
};
items.push(packetSection);
return items;
}
function getExportSubMenuItems(appState) {
const items = [];
const disabled = appState.game.cycles.length === 0;
items.push({
key: "exportSheets",
text: "Export to Sheets...",
onClick: () => {
exportToSheets(appState);
},
disabled: disabled || appState.uiState.sheetsState.clientId == undefined,
});
items.push({
key: "downloadJson",
text: "Export to JSON...",
disabled,
onClick: () => {
appState.uiState.dialogState.showExportToJsonDialog();
},
});
return items;
}
function getOptionsSubMenuItems(appState) {
const items = [];
items.push({
key: "changeFormat",
text: "Change Format...",
onClick: () => {
appState.uiState.dialogState.showCustomizeGameFormatDialog(appState.game.gameFormat);
},
}, {
key: "font",
text: "Font...",
onClick: () => {
appState.uiState.showFontDialog();
},
});
return items;
}
function getViewSubMenuItems(appState) {
var _a;
let items = [
{
key: "showClock",
text: "Clock",
canCheck: true,
checked: !appState.uiState.isClockHidden,
onClick: () => appState.uiState.toggleClockVisibility(),
},
{
key: "showEventLog",
text: "Event Log",
canCheck: true,
checked: !appState.uiState.isEventLogHidden,
onClick: () => appState.uiState.toggleEventLogVisibility(),
},
{
key: "showPacketName",
text: "Packet Name",
canCheck: true,
checked: !appState.uiState.isPacketNameHidden,
onClick: () => appState.uiState.togglePacketNameVisibility(),
},
{
key: "showBonusAlways",
text: "Always show bonus",
canCheck: true,
checked: !appState.uiState.hideBonusOnDeadTossup,
onClick: () => appState.uiState.toggleHideBonusOnDeadTossup(),
},
];
if (((_a = appState.uiState.customExportOptions) === null || _a === void 0 ? void 0 : _a.customExportInterval) != undefined) {
items.push({
key: "showExportStatus",
text: "Export Status",
canCheck: true,
checked: !appState.uiState.isCustomExportStatusHidden,
onClick: () => appState.uiState.toggleCustomExportStatusVisibility(),
});
}
items = items.concat([
{
key: "viewDivider1",
itemType: react_1.ContextualMenuItemType.Divider,
},
{
key: "darkMode",
text: "Dark Mode",
canCheck: true,
checked: appState.uiState.useDarkMode,
onClick: () => {
appState.uiState.toggleDarkMode();
},
},
{
key: "showVerticalScore",
text: "Vertical Score",
canCheck: true,
checked: appState.uiState.isScoreVertical,
onClick: () => appState.uiState.toggleScoreVerticality(),
},
{
key: "highlightBonus",
text: "Highlight Bonus",
title: "Highlight the background of answered bonuses",
canCheck: true,
checked: !appState.uiState.noBonusHighlight,
onClick: () => appState.uiState.toggleBonusHighlight(),
},
]);
items = items.concat([
{
key: "viewDivider2",
itemType: react_1.ContextualMenuItemType.Divider,
},
{
key: "scoresheet",
text: "Scoresheet...",
disabled: appState.game.cycles.length === 0,
onClick: () => {
appState.uiState.dialogState.showScoresheetDialog();
},
},
]);
return items;
}
function getPlayerManagementSubMenuItems(appState, game, uiState, addPlayerHandler, reorderPlayersHandler, reorderTeamsHandler, renameTeamHandler) {
const teamNames = game.teamNames;
const playerActionsMenus = [];
for (const teamName of teamNames) {
const players = game.getPlayers(teamName);
const activePlayers = game.getActivePlayers(teamName, uiState.cycleIndex);
const subs = players.filter((player) => !activePlayers.has(player));
const activePlayerMenuItems = [];
for (const player of players) {
const subMenuItems = subs.map((p) => {
const subItemData = {
appState,
activePlayer: player,
player: p,
};
return {
key: `sub_${teamName}_${p.name}`,
text: p.name,
data: subItemData,
onClick: onSwapPlayerClick,
};
});
const subMenuSectionItem = {
key: `subs_${teamName}`,
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "Substitute",
items: subMenuItems,
},
};
const changeActivityItemData = {
appState,
activePlayer: player,
};
// For active players, remove. For inactive players, join
const isActivePlayer = activePlayers.has(player);
let changeActivityItem;
if (isActivePlayer) {
changeActivityItem = {
key: `remove_${teamName}_${player.name}`,
text: "Remove",
data: changeActivityItemData,
onClick: onRemovePlayerClick,
// TODO: should this be styled in a different color?
};
}
else {
changeActivityItem = {
key: `add_${teamName}_${player.name}`,
text: "Add",
data: changeActivityItemData,
onClick: onAddInactivePlayerClick,
// TODO: should this be styled in a different color?
};
}
const renameItem = {
key: `rename_${teamName}_${player.name}`,
text: "Rename",
data: changeActivityItemData,
onClick: onRenamePlayerClick,
// TODO: should this be styled in a different color?
};
const items = isActivePlayer
? [subMenuSectionItem, changeActivityItem, renameItem]
: [changeActivityItem, renameItem];
activePlayerMenuItems.push({
key: `active_${teamName}_${player.name}`,
text: player.name,
subMenuProps: {
items,
},
});
}
playerActionsMenus.push({
key: `active_${teamName}`,
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: teamName,
items: activePlayerMenuItems,
},
});
}
// TODO: This should be under a section for player management (add player, subs)
const playerActionsItem = {
key: "player",
text: "Player",
subMenuProps: {
items: playerActionsMenus,
},
disabled: playerActionsMenus.length === 0,
// This needs its own submenu, with all the starters, then all the possible subs
// We should disable this if there are no subs available
};
// Couple possible approaches
// - Submenu for Add Player, where it's Add New Player and then all the subs
// - Add all players to the sub/remove submenu, but the subs action item is just "join"/"rename"
// - You can get some weird behavior with joins/subs in the same event. May need to disable it if they have an
// existing action (sub vs join)
// - Should there be a color code for active players?
const gameMenuItemsDisabled = appState.game.cycles.length === 0;
const addPlayerItem = {
key: "addNewPlayer",
text: "Add player...",
onClick: addPlayerHandler,
disabled: gameMenuItemsDisabled,
};
const reorderPlayersItem = {
key: "reorderPlayers",
text: "Reorder players...",
onClick: reorderPlayersHandler,
disabled: gameMenuItemsDisabled,
};
const reorderTeamsItem = {
key: "reorderTeams",
text: "Reorder teams...",
onClick: reorderTeamsHandler,
disabled: gameMenuItemsDisabled,
};
const renameTeamItem = {
key: "renameTeam",
text: "Rename team...",
onClick: renameTeamHandler,
disabled: gameMenuItemsDisabled,
};
return {
key: "teamManagement",
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "Team Management",
items: [playerActionsItem, addPlayerItem, reorderPlayersItem, reorderTeamsItem, renameTeamItem],
},
};
}
function getProtestSubMenuItems(appState, game, uiState, protestBonusHandler) {
const cycle = uiState.cycleIndex < game.cycles.length ? game.cycles[uiState.cycleIndex] : undefined;
let protestTossupItems = undefined;
let protestBonusItem = undefined;
if ((cycle === null || cycle === void 0 ? void 0 : cycle.orderedBuzzes) != undefined) {
const tossupIndex = game.getTossupIndex(uiState.cycleIndex);
if (tossupIndex !== -1) {
const protestSubMenuItems = [];
for (const buzz of cycle.orderedBuzzes) {
const { name, teamName } = buzz.marker.player;
const tossupProtestItemData = {
appState,
buzz,
};
const protestExists = cycle.tossupProtests != undefined &&
cycle.tossupProtests.findIndex((protest) => protest.teamName === teamName) >= 0;
const protestSubMenuItem = {
key: `protest${teamName}_${name}`,
text: `${name} (${teamName})`,
canCheck: true,
checked: protestExists,
data: tossupProtestItemData,
onClick: onProtestTossupClick,
};
const protestSection = {
key: `protestSection${teamName}_${name}`,
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: teamName,
items: [protestSubMenuItem],
},
};
protestSubMenuItems.push(protestSection);
}
protestTossupItems = {
key: "protestTossup",
text: "Protest tossup...",
subMenuProps: {
items: protestSubMenuItems,
},
disabled: protestSubMenuItems.length === 0,
};
}
}
if ((cycle === null || cycle === void 0 ? void 0 : cycle.bonusAnswer) != undefined) {
const bonusIndex = game.getBonusIndex(uiState.cycleIndex);
if (bonusIndex !== -1) {
const bonus = game.packet.bonuses[bonusIndex];
if (cycle.getProtestableBonusPartIndexes(bonus.parts.length).length > 0) {
protestBonusItem = {
key: "protestBonus",
text: "Protest bonus...",
onClick: protestBonusHandler,
};
}
}
}
if (protestTossupItems == undefined) {
protestTossupItems = {
key: "protestTossup",
text: "Protest tossup...",
disabled: true,
};
}
if (protestBonusItem == undefined) {
protestBonusItem = {
key: "protestBonus",
text: "Protest bonus...",
disabled: true,
};
}
return {
key: "protestSection",
itemType: react_1.ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "Protests",
items: [protestTossupItems, protestBonusItem],
},
};
}
function onAddInactivePlayerClick(ev, item) {
if (item == undefined) {
return;
}
else if (!isSubMenuItemData(item.data)) {
return;
}
const appState = item.data.appState;
appState.game.addInactivePlayer(item.data.activePlayer, appState.uiState.cycleIndex);
}
function onProtestTossupClick(ev, item) {
if (item == undefined) {
return;
}
else if (!isTossupProtestMenuItemData(item.data)) {
return;
}
const { game, uiState } = item.data.appState;
const cycle = game.cycles[uiState.cycleIndex];
if ((cycle === null || cycle === void 0 ? void 0 : cycle.orderedBuzzes) == undefined) {
return;
}
const teamName = item.data.buzz.marker.player.teamName;
// If this item is checked, then clear the protest
if (item.checked === true) {
cycle.removeTossupProtest(teamName);
return;
}
const tossupIndex = game.getTossupIndex(uiState.cycleIndex);
const tossup = game.packet.tossups[tossupIndex];
if (tossup == undefined) {
// Something is wrong... the tossup is undefined, but this handler can be accessed?
throw new Error(`Impossible to add tossup protest for tossup question ${tossupIndex}`);
}
uiState.setPendingTossupProtest(teamName, tossupIndex, item.data.buzz.marker.position);
}
function onRemovePlayerClick(ev, item) {
if (item == undefined) {
return;
}
else if (!isSubMenuItemData(item.data)) {
return;
}
const appState = item.data.appState;
appState.uiState.dialogState.showOKCancelMessageDialog("Remove Player", `Are you sure you want to remove the player "${item.data.activePlayer.name}" from team "${item.data.activePlayer.teamName}"?`, () => appState.game.cycles[appState.uiState.cycleIndex].addPlayerLeaves(item.data.activePlayer));
}
function onRenamePlayerClick(ev, item) {
if (item == undefined) {
return;
}
else if (!isSubMenuItemData(item.data)) {
return;
}
const appState = item.data.appState;
appState.uiState.dialogState.showRenamePlayerDialog(item.data.activePlayer);
}
function onSwapPlayerClick(ev, item) {
if (item == undefined) {
return;
}
else if (!isSubMenuItemData(item.data) || item.data.player == undefined) {
return;
}
const { uiState, game } = item.data.appState;
const cycleIndex = uiState.cycleIndex;
const halftimeIndex = Math.floor(game.gameFormat.regulationTossupCount / 2);
// If a substitution doesn't happen in the beginning, after halftime, or during OT, ask the reader if a substitution
// was intended. Otherwise just do the substitution as normal
if (cycleIndex === 0 || cycleIndex === halftimeIndex || cycleIndex >= game.gameFormat.regulationTossupCount) {
game.cycles[uiState.cycleIndex].addSwapSubstitution(item.data.player, item.data.activePlayer);
return;
}
const additionalHint = Math.abs(cycleIndex - halftimeIndex) <= 1
? ` If you want to substitute a player at halftime, do it on question ${Math.floor(halftimeIndex + 1)}.`
: "";
item.data.appState.uiState.dialogState.showOKCancelMessageDialog("Substitute Player", "You are substituting players outside of a normal time (beginning of the game, after halftime, overtime). Are you sure you want to substitute now?" +
additionalHint, () => game.cycles[uiState.cycleIndex].addSwapSubstitution(item.data.player, item.data.activePlayer));
}
function isSubMenuItemData(data) {
return (data === null || data === void 0 ? void 0 : data.appState) !== undefined && data.activePlayer !== undefined;
}
function isTossupProtestMenuItemData(data) {
return (data === null || data === void 0 ? void 0 : data.appState) !== undefined && data.buzz !== undefined;
}
//# sourceMappingURL=GameBar.js.map