UNPKG

modaq

Version:

Quiz Bowl Reader using TypeScript, React, and MobX

713 lines 29.7 kB
"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