First commit to the new repository
:
This commit is contained in:
66
src/Components/Game.tsx
Executable file
66
src/Components/Game.tsx
Executable file
@@ -0,0 +1,66 @@
|
||||
import React, {ReactNode} from "react";
|
||||
import "../static/css/game.css";
|
||||
import KadiBoard from "./KadiBoard";
|
||||
import GameSetup, {GameSettings} from "./GameSetup"
|
||||
import Settings from "../static/settings.json";
|
||||
import {SupportedLang} from "../static/enums";
|
||||
|
||||
interface GameState {
|
||||
currentSettings: GameSettings;
|
||||
settingUp: boolean;
|
||||
}
|
||||
|
||||
interface GameProps {}
|
||||
|
||||
class Game extends React.Component<GameProps, GameState> {
|
||||
state: GameState;
|
||||
|
||||
constructor(props: GameProps) {
|
||||
super(props);
|
||||
|
||||
const startupSettings: GameSettings = {
|
||||
playerIds: Settings.players,
|
||||
ruleset: Settings.ruleset,
|
||||
lang: Settings.lang as SupportedLang
|
||||
};
|
||||
|
||||
this.state = {
|
||||
currentSettings: startupSettings,
|
||||
settingUp: true,
|
||||
};
|
||||
}
|
||||
|
||||
onSetupComplete: (gameSettings: GameSettings) => void = (gameSettings) => {
|
||||
this.setState({
|
||||
currentSettings: gameSettings,
|
||||
settingUp: false
|
||||
});
|
||||
};
|
||||
|
||||
returnToSetup: () => void = () => {
|
||||
this.setState({settingUp: true});
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
{this.state.settingUp ?
|
||||
(
|
||||
<GameSetup
|
||||
onSetupComplete={this.onSetupComplete}
|
||||
settings={this.state.currentSettings}
|
||||
/>
|
||||
) :
|
||||
(
|
||||
<KadiBoard
|
||||
settings={this.state.currentSettings}
|
||||
returnToSetup={this.returnToSetup}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Game;
|
||||
252
src/Components/GameSetup.tsx
Executable file
252
src/Components/GameSetup.tsx
Executable file
@@ -0,0 +1,252 @@
|
||||
import React, {ChangeEvent, FocusEvent, KeyboardEvent, ReactNode} from "react";
|
||||
import {getSchemaListings, SchemaListing} from "../static/rulesets";
|
||||
import {LocaleContext, LanguageNames} from "../static/strings";
|
||||
import {SupportedLang} from "../static/enums";
|
||||
|
||||
class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
|
||||
private readonly availableRulesets: SchemaListing[];
|
||||
private changeLang: (lang: string) => void;
|
||||
state: GameSetupState;
|
||||
|
||||
constructor(props: GameSetupProps) {
|
||||
super(props);
|
||||
|
||||
this.availableRulesets = getSchemaListings();
|
||||
this.changeLang = () => {};
|
||||
this.state = {
|
||||
selectedLang: this.props.settings.lang,
|
||||
selectedRuleset: this.props.settings.ruleset,
|
||||
enteredPlayerIds: this.props.settings.playerIds,
|
||||
editingPlayerName: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.changeLang = this.context.changeLang;
|
||||
}
|
||||
|
||||
onLanguageChange: (lang: SupportedLang) => void = (lang) => {
|
||||
this.setState({ selectedLang: lang });
|
||||
this.changeLang(lang);
|
||||
};
|
||||
|
||||
onRulesetChange: (ruleset: string) => void = (ruleset) => {
|
||||
this.setState({ selectedRuleset: ruleset });
|
||||
};
|
||||
|
||||
removePlayer: (index: number) => void = (index) => {
|
||||
const newPlayers = this.state.enteredPlayerIds.slice();
|
||||
newPlayers.splice(index, 1);
|
||||
this.setState({enteredPlayerIds: newPlayers});
|
||||
};
|
||||
|
||||
addPlayer: (playerSubmission: string, keepEditing: boolean) => void = (playerSubmission, keepEditing) => {
|
||||
const newPlayers = this.state.enteredPlayerIds.slice();
|
||||
if (!newPlayers.find(enteredPlayer => enteredPlayer == playerSubmission)) {
|
||||
newPlayers.push(playerSubmission);
|
||||
}
|
||||
this.setState({enteredPlayerIds: newPlayers, editingPlayerName: keepEditing});
|
||||
};
|
||||
|
||||
submitSettings: () => void = () => {
|
||||
this.props.onSetupComplete({
|
||||
ruleset: this.state.selectedRuleset,
|
||||
playerIds: this.state.enteredPlayerIds,
|
||||
lang: this.state.selectedLang,
|
||||
});
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const Locale = this.context.strings;
|
||||
|
||||
const langOptions: ReactNode[] = [];
|
||||
for (const lang in SupportedLang) {
|
||||
let className = "option";
|
||||
if (this.state.selectedLang === lang) {
|
||||
className += " selected";
|
||||
}
|
||||
langOptions.push((
|
||||
<div
|
||||
key={lang + "lang_option"}
|
||||
className={className}
|
||||
onClick={() => this.onLanguageChange(lang as SupportedLang)}
|
||||
>
|
||||
{LanguageNames[lang as SupportedLang]}
|
||||
<span className={"selectorBox"}/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
const rulesetOptions: ReactNode[] = [];
|
||||
for (const rulesetListing of this.availableRulesets) {
|
||||
let className = "option";
|
||||
if (this.state.selectedRuleset === rulesetListing.id) {
|
||||
className += " selected";
|
||||
}
|
||||
rulesetOptions.push((
|
||||
<div
|
||||
key={rulesetListing.id + "ruleset_option"}
|
||||
className={className}
|
||||
onClick={() => this.onRulesetChange(rulesetListing.id)}
|
||||
>
|
||||
{rulesetListing.label}
|
||||
<span className={"selectorBox"}/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
const playerListing: ReactNode[] = [];
|
||||
for (let i = 0; i < this.state.enteredPlayerIds.length; i++) {
|
||||
const playerName = this.state.enteredPlayerIds[i];
|
||||
playerListing.push((
|
||||
<div
|
||||
key={playerName + "_list"}
|
||||
className={"option playerOption"}
|
||||
>
|
||||
{playerName}
|
||||
<span
|
||||
className={"trashButton"}
|
||||
onClick={() => this.removePlayer(i)}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
playerListing.push((
|
||||
<AddPlayerField
|
||||
playersListEmpty={playerListing.length === 0}
|
||||
submitNewPlayer={this.addPlayer}
|
||||
userEditing={this.state.editingPlayerName}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={"gameSetup"}>
|
||||
<div className={"setupFormContainer"}>
|
||||
<div className={"setupForm"}>
|
||||
<div className={"optionGroup"}>
|
||||
<div className={"optionGroupTitleContainer"}>
|
||||
<span className={"optionGroupTitle"}>
|
||||
{Locale.setupScreen.players}
|
||||
</span>
|
||||
</div>
|
||||
<div className={"playerList optionList"}>
|
||||
{playerListing}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"optionGroup"}>
|
||||
<div className={"optionGroupTitleContainer"}>
|
||||
<span className={"optionGroupTitle"}>
|
||||
{Locale.setupScreen.selectRuleset}
|
||||
</span>
|
||||
</div>
|
||||
<div className={"rulesetOptions optionList"}>
|
||||
{rulesetOptions}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"optionGroup"}>
|
||||
<div className={"optionGroupTitleContainer"}>
|
||||
<span className={"optionGroupTitle"}>
|
||||
{Locale.setupScreen.selectLanguage}
|
||||
</span>
|
||||
</div>
|
||||
<div className={"languageOptions optionList"}>
|
||||
{langOptions}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"playButtonContainer"}>
|
||||
<button
|
||||
className={"playButton"}
|
||||
onClick={this.submitSettings}
|
||||
disabled={this.state.enteredPlayerIds.length < 1}
|
||||
>
|
||||
{Locale.setupScreen.startGame}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
GameSetup.contextType = LocaleContext;
|
||||
|
||||
interface GameSetupProps {
|
||||
onSetupComplete: (settings: GameSettings) => void;
|
||||
settings: GameSettings;
|
||||
}
|
||||
|
||||
interface GameSetupState {
|
||||
selectedLang: SupportedLang;
|
||||
selectedRuleset: string;
|
||||
enteredPlayerIds: string[];
|
||||
editingPlayerName: boolean;
|
||||
}
|
||||
|
||||
export interface GameSettings {
|
||||
ruleset: string;
|
||||
playerIds: string[];
|
||||
lang: SupportedLang;
|
||||
}
|
||||
|
||||
const AddPlayerField: React.FunctionComponent<AddPlayerFieldProps> = ({playersListEmpty, submitNewPlayer, userEditing}) => {
|
||||
const Locale = React.useContext(LocaleContext).strings;
|
||||
|
||||
const [beingEdited, updateBeingEdited] = React.useState(false);
|
||||
const [currentEditValue, updateCurrentEditValue] = React.useState("");
|
||||
|
||||
const placeholderText = playersListEmpty ?
|
||||
Locale.setupScreen.noPlayersEntered :
|
||||
Locale.setupScreen.clickToAddPlayer;
|
||||
const displayText = beingEdited ? currentEditValue : placeholderText;
|
||||
|
||||
const handleFocus = () => {
|
||||
updateBeingEdited(true);
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
updateCurrentEditValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||
attemptPlayerSubmit(e.target.value, false);
|
||||
updateBeingEdited(false);
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
attemptPlayerSubmit(e.currentTarget.value, true);
|
||||
}
|
||||
};
|
||||
|
||||
const attemptPlayerSubmit = (input: string, keepEditing: boolean) => {
|
||||
if (input !== "") {
|
||||
submitNewPlayer(input, keepEditing);
|
||||
updateCurrentEditValue("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={"noplayer_list"}
|
||||
className={"option playerOption inputPlayerField" + (beingEdited ? "" : " faded")}
|
||||
>
|
||||
<input
|
||||
type={"text"}
|
||||
value={displayText}
|
||||
autoFocus={userEditing}
|
||||
onFocus={handleFocus}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddPlayerFieldProps {
|
||||
playersListEmpty: boolean;
|
||||
submitNewPlayer: (name: string, keepEditing: boolean) => void;
|
||||
userEditing: boolean;
|
||||
}
|
||||
|
||||
export default GameSetup;
|
||||
17
src/Components/GenericKadiRowContainer.tsx
Executable file
17
src/Components/GenericKadiRowContainer.tsx
Executable file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
interface GenericKadiRowContainerProps {
|
||||
cellCssClassName: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const GenericKadiRowContainer: React.FunctionComponent<GenericKadiRowContainerProps> = ({ cellCssClassName, label, children }) => {
|
||||
return (
|
||||
<tr className={"kadiRow " + cellCssClassName}>
|
||||
<td className="kadiCell rowLabelCell">{label}</td>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericKadiRowContainer;
|
||||
29
src/Components/KadiBlockBonusRow.tsx
Executable file
29
src/Components/KadiBlockBonusRow.tsx
Executable file
@@ -0,0 +1,29 @@
|
||||
import React, {ReactNode} from "react";
|
||||
import KadiCell from "./KadiCell";
|
||||
import {FieldType} from "../static/enums";
|
||||
import {CellScores} from "./KadiBoard";
|
||||
|
||||
interface KadiBlockBonusRowProps {
|
||||
blockId: string;
|
||||
bonusScore: number;
|
||||
scores: CellScores;
|
||||
}
|
||||
|
||||
const KadiBlockBonusRow: React.FunctionComponent<KadiBlockBonusRowProps> = ({ blockId, bonusScore, scores}) => {
|
||||
const cells: ReactNode[] = [];
|
||||
for (const playerId in scores) {
|
||||
cells.push((
|
||||
<KadiCell
|
||||
key={"cell_bonus_" + blockId + "_" + playerId}
|
||||
location={{ blockId, cellId: "bonus"}}
|
||||
fieldType={FieldType.bonus}
|
||||
playerId={playerId}
|
||||
value={scores[playerId] ? bonusScore : 0}
|
||||
onCellEdit={() => {}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return <>{cells}</>;
|
||||
};
|
||||
|
||||
export default KadiBlockBonusRow;
|
||||
83
src/Components/KadiBlockRenderer.tsx
Executable file
83
src/Components/KadiBlockRenderer.tsx
Executable file
@@ -0,0 +1,83 @@
|
||||
import {BlockDef} from "../static/rulesets";
|
||||
import {CellLocation} from "../Classes/PlayerScoreCard";
|
||||
import React, {ReactElement} from "react";
|
||||
import {formatUnicorn, LocaleContext} from "../static/strings";
|
||||
import {FieldType} from "../static/enums";
|
||||
import {BlockScores, CellEventResponse} from "./KadiBoard";
|
||||
import GenericKadiRowContainer from "./GenericKadiRowContainer";
|
||||
import KadiEditableRowCells from "./KadiEditableRowCells";
|
||||
import KadiBlockTotalRow from "./KadiBlockTotalRow";
|
||||
import KadiBlockSubtotalRow from "./KadiBlockSubtotalRow";
|
||||
import KadiBlockBonusRow from "./KadiBlockBonusRow";
|
||||
|
||||
interface BlockRendererProps {
|
||||
blockSchema: BlockDef;
|
||||
showResults: boolean;
|
||||
onCellEdit(res: CellEventResponse): void;
|
||||
scores: BlockScores;
|
||||
}
|
||||
|
||||
const KadiBlockRenderer: React.FunctionComponent<BlockRendererProps> = ({ blockSchema , showResults, scores, onCellEdit}) => {
|
||||
const rowsInBlock: ReactElement[] = [];
|
||||
const Locale = React.useContext(LocaleContext).strings;
|
||||
|
||||
for (const cell of blockSchema.cells) {
|
||||
rowsInBlock.push((
|
||||
<GenericKadiRowContainer
|
||||
key={"rowCont" + cell.id + blockSchema.id}
|
||||
label={cell.label}
|
||||
cellCssClassName={cell.fieldType}
|
||||
>
|
||||
<KadiEditableRowCells
|
||||
location={{blockId: blockSchema.id, cellId: cell.id}}
|
||||
fieldType={cell.fieldType}
|
||||
scores={scores[cell.id]}
|
||||
onCellEdit={onCellEdit}
|
||||
/>
|
||||
</GenericKadiRowContainer>
|
||||
));
|
||||
}
|
||||
if (blockSchema.hasBonus) {
|
||||
rowsInBlock.push(
|
||||
<GenericKadiRowContainer
|
||||
key={"rowContSubtotal" + blockSchema.id}
|
||||
label={Locale.rowLabels.subtotal}
|
||||
cellCssClassName={FieldType.subtotal + (showResults ? "" : " hideResults")}
|
||||
>
|
||||
<KadiBlockSubtotalRow
|
||||
blockId={blockSchema.id}
|
||||
scores={scores.subtotals}
|
||||
/>
|
||||
</GenericKadiRowContainer>
|
||||
);
|
||||
rowsInBlock.push(
|
||||
<GenericKadiRowContainer
|
||||
key={"rowContBonus" + blockSchema.id}
|
||||
label={Locale.rowLabels.bonus}
|
||||
cellCssClassName={FieldType.bonus + (showResults ? "" : " hideResults")}
|
||||
>
|
||||
<KadiBlockBonusRow
|
||||
blockId={blockSchema.id}
|
||||
bonusScore={blockSchema.bonusScore}
|
||||
scores={scores.bonuses}
|
||||
/>
|
||||
</GenericKadiRowContainer>
|
||||
);
|
||||
}
|
||||
rowsInBlock.push(
|
||||
<GenericKadiRowContainer
|
||||
key={"rowContTotal" + blockSchema.id}
|
||||
label={formatUnicorn(Locale.rowLabels.blockTotal, blockSchema.label)}
|
||||
cellCssClassName={FieldType.total + (showResults ? "" : " hideResults")}
|
||||
>
|
||||
<KadiBlockTotalRow
|
||||
blockId={blockSchema.id}
|
||||
scores={scores.totals}
|
||||
/>
|
||||
</GenericKadiRowContainer>
|
||||
);
|
||||
|
||||
return <>{rowsInBlock}</>;
|
||||
};
|
||||
|
||||
export default KadiBlockRenderer;
|
||||
28
src/Components/KadiBlockSubtotalRow.tsx
Executable file
28
src/Components/KadiBlockSubtotalRow.tsx
Executable file
@@ -0,0 +1,28 @@
|
||||
import React, {ReactNode} from "react";
|
||||
import KadiCell from "./KadiCell";
|
||||
import {FieldType} from "../static/enums";
|
||||
import {CellScores} from "./KadiBoard";
|
||||
|
||||
interface KadiBlockSubtotalRowProps {
|
||||
blockId: string;
|
||||
scores: CellScores;
|
||||
}
|
||||
|
||||
const KadiBlockSubtotalRow: React.FunctionComponent<KadiBlockSubtotalRowProps> = ({ blockId, scores}) => {
|
||||
const cells: ReactNode[] = [];
|
||||
for (const playerId in scores) {
|
||||
cells.push((
|
||||
<KadiCell
|
||||
key={"cell_subtotal_" + blockId + "_" + playerId}
|
||||
location={{ blockId, cellId: "subtotal"}}
|
||||
fieldType={FieldType.subtotal}
|
||||
playerId={playerId}
|
||||
value={scores[playerId]}
|
||||
onCellEdit={() => {}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return <>{cells}</>;
|
||||
};
|
||||
|
||||
export default KadiBlockSubtotalRow;
|
||||
28
src/Components/KadiBlockTotalRow.tsx
Executable file
28
src/Components/KadiBlockTotalRow.tsx
Executable file
@@ -0,0 +1,28 @@
|
||||
import React, {ReactNode} from "react";
|
||||
import KadiCell from "./KadiCell";
|
||||
import {FieldType} from "../static/enums";
|
||||
import {CellScores} from "./KadiBoard";
|
||||
|
||||
interface KadiBlockTotalRowProps {
|
||||
blockId: string;
|
||||
scores: CellScores;
|
||||
}
|
||||
|
||||
const KadiBlockTotalRow: React.FunctionComponent<KadiBlockTotalRowProps> = ({ blockId, scores }) => {
|
||||
const cells: ReactNode[] = [];
|
||||
for (const playerId in scores) {
|
||||
cells.push((
|
||||
<KadiCell
|
||||
key={"cell_total_" + blockId + "_" + playerId}
|
||||
location={{ blockId, cellId: "total"}}
|
||||
fieldType={FieldType.total}
|
||||
playerId={playerId}
|
||||
value={scores[playerId]}
|
||||
onCellEdit={() => {}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return <>{cells}</>;
|
||||
};
|
||||
|
||||
export default KadiBlockTotalRow;
|
||||
308
src/Components/KadiBoard.tsx
Executable file
308
src/Components/KadiBoard.tsx
Executable file
@@ -0,0 +1,308 @@
|
||||
import React, {ReactElement, ReactNode, useContext} from "react";
|
||||
import PlayerScoreCard, {CellLocation, PlayerScoreCardJSONRepresentation} from "../Classes/PlayerScoreCard";
|
||||
import {BlockDef, GameSchema, getGameSchemaById} from "../static/rulesets";
|
||||
import {formatUnicorn, LocaleContext} from "../static/strings";
|
||||
import {CellFlag} from "../static/enums";
|
||||
import {ScoreCellValue} from "../Classes/ScoreCell";
|
||||
import {CaretakerSet} from "../Classes/Caretaker";
|
||||
import {GameSettings} from "./GameSetup";
|
||||
import Settings from "../static/settings.json";
|
||||
import axios from "axios";
|
||||
import {Button, Container, Header, Icon, Image} from "semantic-ui-react";
|
||||
import logo from "../static/images/kadi.png";
|
||||
import KadiGrandTotalRow from "./KadiGrandTotalRow";
|
||||
import KadiBlockRenderer from "./KadiBlockRenderer";
|
||||
import {KadiCellDisplayValue} from "./KadiCell";
|
||||
|
||||
|
||||
export interface CellScores {
|
||||
[key: string]: KadiCellDisplayValue;
|
||||
}
|
||||
|
||||
export interface BlockScores {
|
||||
[key: string]: CellScores;
|
||||
bonuses: CellScores;
|
||||
subtotals: CellScores;
|
||||
totals: CellScores;
|
||||
}
|
||||
|
||||
export interface CellEventResponse {
|
||||
value: ScoreCellValue | CellFlag;
|
||||
playerId: string;
|
||||
location: CellLocation;
|
||||
}
|
||||
|
||||
export interface KadiBoardProps {
|
||||
settings: GameSettings;
|
||||
returnToSetup: () => void;
|
||||
}
|
||||
|
||||
interface KadiBoardState {
|
||||
scoreSheet: ScoreSheet;
|
||||
playerIds: string[];
|
||||
showResults: boolean;
|
||||
savingGame: boolean;
|
||||
}
|
||||
|
||||
interface ScoreSheet {
|
||||
[key: string]: PlayerScoreCard;
|
||||
}
|
||||
|
||||
class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
|
||||
private readonly gameSchema: GameSchema;
|
||||
private readonly caretaker: CaretakerSet;
|
||||
state: KadiBoardState;
|
||||
|
||||
constructor(props: KadiBoardProps) {
|
||||
super(props);
|
||||
|
||||
this.gameSchema = getGameSchemaById(this.props.settings.ruleset);
|
||||
|
||||
this.state = {
|
||||
scoreSheet: this.generateNewScoreSheet(this.props.settings.playerIds),
|
||||
playerIds: this.props.settings.playerIds,
|
||||
showResults: true,
|
||||
savingGame: false,
|
||||
};
|
||||
|
||||
this.caretaker = new CaretakerSet(
|
||||
Settings.maxHistoryLength,
|
||||
...this.state.playerIds.map(
|
||||
pid => this.state.scoreSheet[pid]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private generateNewScoreSheet(playerIds: string[]): ScoreSheet {
|
||||
const scoreSheet: ScoreSheet = {};
|
||||
for (const playerId of playerIds) {
|
||||
scoreSheet[playerId] = new PlayerScoreCard(playerId, this.gameSchema);
|
||||
}
|
||||
return scoreSheet;
|
||||
}
|
||||
|
||||
private onCellEdit = (response: CellEventResponse): void => {
|
||||
const newScoreSheet = this.state.scoreSheet;
|
||||
KadiBoard.updateScoreSheetFromCellResponse(newScoreSheet, response);
|
||||
this.setState({ scoreSheet: newScoreSheet });
|
||||
this.caretaker.save();
|
||||
};
|
||||
|
||||
private static updateScoreSheetFromCellResponse(scoreSheet: ScoreSheet, response: CellEventResponse): void {
|
||||
const playerScoreCard = scoreSheet[response.playerId];
|
||||
playerScoreCard.updateCellByLocationWithValue(response.location, response.value);
|
||||
}
|
||||
|
||||
toggleShowResults = () => {
|
||||
this.setState({ showResults: !this.state.showResults });
|
||||
};
|
||||
|
||||
private getCellDisplayValueByPlayerIdAndLocation(playerId: string, location: CellLocation): KadiCellDisplayValue {
|
||||
const playerSheet = this.state.scoreSheet[playerId];
|
||||
let cellValue: KadiCellDisplayValue = playerSheet.getCellScoreByLocation(location);
|
||||
cellValue = playerSheet.cellAtLocationIsStruck(location) ? CellFlag.strike : cellValue;
|
||||
return cellValue;
|
||||
};
|
||||
|
||||
private getBlockSubtotalByPlayerId(blockId: string, playerId: string): number {
|
||||
return this.state.scoreSheet[playerId].getBlockSubTotalById(blockId);
|
||||
}
|
||||
|
||||
private getBlockTotalByPlayerId(blockId: string, playerId: string): number {
|
||||
return this.state.scoreSheet[playerId].getBlockTotalById(blockId);
|
||||
}
|
||||
|
||||
private getTotalForPlayer(playerId: string): number {
|
||||
return this.state.scoreSheet[playerId].getTotal();
|
||||
}
|
||||
|
||||
private playerHasBonusForBlock(playerId: string, blockId: string): boolean {
|
||||
return this.state.scoreSheet[playerId].blockWithIdHasBonus(blockId);
|
||||
}
|
||||
|
||||
private undo(): void {
|
||||
this.caretaker.undo();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private redo(): void {
|
||||
this.caretaker.redo();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private getJSONRepresentationForBoard(): string {
|
||||
const JSONRepresentation: PlayerScoreCardJSONRepresentation[] = [];
|
||||
for (const playerId in this.state.scoreSheet) {
|
||||
JSONRepresentation.push(this.state.scoreSheet[playerId].getJSONRepresentation());
|
||||
}
|
||||
return JSON.stringify(JSONRepresentation);
|
||||
}
|
||||
|
||||
private canSave(): boolean {
|
||||
for (const playerId in this.state.scoreSheet) {
|
||||
if (!this.state.scoreSheet[playerId].filledOut()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private saveGame: () => void = async () => {
|
||||
this.setState({savingGame: true});
|
||||
axios.post(Settings.rootUrl + "/api/savegame",
|
||||
this.getJSONRepresentationForBoard(),
|
||||
{headers: {"Content-Type": "application/json"}}
|
||||
)
|
||||
.then(response => this.onGameSave(response.data))
|
||||
.catch(error => this.onSaveError(error))
|
||||
.finally(() => this.setState({ savingGame: false }));
|
||||
};
|
||||
|
||||
private onGameSave = (serverResponse: string) => {
|
||||
console.log("Response:", serverResponse);
|
||||
};
|
||||
|
||||
private onSaveError = (error: any) => {
|
||||
console.log("Error saving:", error);
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const Locale = this.context.strings;
|
||||
const rows: ReactElement[] = [];
|
||||
|
||||
for (const block of this.gameSchema.blocks) {
|
||||
const scores: BlockScores = {subtotals: {}, bonuses: {}, totals: {}};
|
||||
for (const cell of block.cells) {
|
||||
scores[cell.id] = {};
|
||||
}
|
||||
this.state.playerIds.forEach(pid => {
|
||||
scores.totals[pid] = this.getBlockTotalByPlayerId(block.id, pid);
|
||||
scores.bonuses[pid] = this.playerHasBonusForBlock(pid, block.id);
|
||||
scores.subtotals[pid] = this.getBlockSubtotalByPlayerId(block.id, pid);
|
||||
for (const cell of block.cells) {
|
||||
scores[cell.id][pid] = this.getCellDisplayValueByPlayerIdAndLocation(
|
||||
pid, { blockId: block.id, cellId: cell.id });
|
||||
}
|
||||
});
|
||||
rows.push(
|
||||
<KadiBlockRenderer
|
||||
key={"block" + block.id}
|
||||
blockSchema={block}
|
||||
showResults={this.state.showResults}
|
||||
onCellEdit={this.onCellEdit}
|
||||
scores={scores}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const grandTotals: CellScores = {};
|
||||
this.state.playerIds.forEach(pid =>
|
||||
grandTotals[pid] = this.getTotalForPlayer(pid)
|
||||
);
|
||||
rows.push(
|
||||
<KadiGrandTotalRow
|
||||
key={"grandTotalRow"}
|
||||
showResults={this.state.showResults}
|
||||
scores={grandTotals}
|
||||
toggleShowResults={this.toggleShowResults}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="game">
|
||||
<table className="kadiTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={this.state.playerIds.length + 1}>
|
||||
<Header inverted={true} >
|
||||
<Image spaced={true} size={"small"} src={logo} />
|
||||
<Header.Content>
|
||||
<span className={"brandname"}>K A D I</span>
|
||||
</Header.Content>
|
||||
</Header>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ColumnHeadersRow playerIds={this.state.playerIds} />
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<Container>
|
||||
<div className="buttonContainer">
|
||||
<Button.Group>
|
||||
<Button
|
||||
secondary={true}
|
||||
disabled={!this.caretaker.undosLeft()}
|
||||
onClick={() => this.undo()}
|
||||
>
|
||||
<Icon name={"undo"} />
|
||||
{Locale.buttons.undoButton}
|
||||
</Button>
|
||||
<Button
|
||||
secondary={true}
|
||||
disabled={!this.caretaker.redosLeft()}
|
||||
onClick={() => this.redo()}
|
||||
>
|
||||
<Icon name={"redo"} />
|
||||
{Locale.buttons.redoButton}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<div className="buttonContainer">
|
||||
<Button
|
||||
icon={true}
|
||||
labelPosition={"left"}
|
||||
secondary={true}
|
||||
onClick={() => this.props.returnToSetup()}
|
||||
>
|
||||
<Icon name={"arrow alternate circle left"}/>
|
||||
{Locale.buttons.returnToSetupButton}
|
||||
</Button>
|
||||
<Button
|
||||
icon={true}
|
||||
labelPosition={"left"}
|
||||
primary={true}
|
||||
disabled={!this.canSave()}
|
||||
onClick={() => this.saveGame()}
|
||||
loading={this.state.savingGame}
|
||||
>
|
||||
<Icon name={"save"} />
|
||||
{Locale.buttons.saveGameButton}
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KadiBoard.contextType = LocaleContext;
|
||||
|
||||
interface ColumnHeadersRowProps {
|
||||
playerIds: string[];
|
||||
}
|
||||
|
||||
const ColumnHeadersRow: React.FunctionComponent<ColumnHeadersRowProps> = ({ playerIds }) => {
|
||||
const Locale = useContext(LocaleContext).strings;
|
||||
|
||||
const columnHeaders: ReactNode[] = [(
|
||||
<td className="topLeftBlankCell" key={"blank_header"}>
|
||||
{Locale.headers.rowLabels}
|
||||
</td>
|
||||
)];
|
||||
for (const playerId of playerIds) {
|
||||
columnHeaders.push(
|
||||
<td className="playerNameCell" key={"header" + playerId}>
|
||||
{playerId}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr className="columnHeaderRow">
|
||||
{columnHeaders}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default KadiBoard;
|
||||
256
src/Components/KadiCell.tsx
Executable file
256
src/Components/KadiCell.tsx
Executable file
@@ -0,0 +1,256 @@
|
||||
import React, {ChangeEvent, FocusEvent, ReactNode, KeyboardEvent} from "react";
|
||||
import {CellFlag, FieldType} from "../static/enums";
|
||||
import {ScoreCellValue} from "../Classes/ScoreCell";
|
||||
import {CellEventResponse} from "./KadiBoard";
|
||||
import {CellLocation} from "../Classes/PlayerScoreCard";
|
||||
import {useLongPress} from "./useLongPress";
|
||||
|
||||
export type KadiCellDisplayValue = ScoreCellValue | CellFlag.strike;
|
||||
|
||||
export interface KadiCellProps {
|
||||
location: CellLocation;
|
||||
fieldType: FieldType;
|
||||
playerId: string;
|
||||
value: KadiCellDisplayValue;
|
||||
showResults?: boolean;
|
||||
onCellEdit: (response: CellEventResponse) => void;
|
||||
}
|
||||
|
||||
interface KadiCellState {}
|
||||
|
||||
class KadiCell extends React.Component<KadiCellProps, KadiCellState> {
|
||||
private readonly standardTimeoutTimeMs: number;
|
||||
constructor(props: KadiCellProps) {
|
||||
super(props);
|
||||
this.standardTimeoutTimeMs = 400;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(
|
||||
nextProps: Readonly<KadiCellProps>,
|
||||
nextState: Readonly<KadiCellState>,
|
||||
nextContext: any): boolean {
|
||||
return nextProps.value != this.props.value;
|
||||
}
|
||||
|
||||
updateCell = (value: ScoreCellValue): void => {
|
||||
const response: CellEventResponse = {
|
||||
value: value,
|
||||
playerId: this.props.playerId,
|
||||
location: this.props.location,
|
||||
};
|
||||
this.props.onCellEdit(response);
|
||||
};
|
||||
|
||||
strikeCell = (): void => {
|
||||
const response: CellEventResponse = {
|
||||
value: CellFlag.strike,
|
||||
playerId: this.props.playerId,
|
||||
location: this.props.location,
|
||||
};
|
||||
this.props.onCellEdit(response);
|
||||
};
|
||||
|
||||
unstrikeCell = (): void => {
|
||||
const response: CellEventResponse = {
|
||||
value: CellFlag.unstrike,
|
||||
playerId: this.props.playerId,
|
||||
location: this.props.location,
|
||||
};
|
||||
this.props.onCellEdit(response);
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const {
|
||||
fieldType,
|
||||
value,
|
||||
} = this.props;
|
||||
|
||||
const propsForEditableCell = {
|
||||
timeoutMs: this.standardTimeoutTimeMs,
|
||||
updateCell: this.updateCell,
|
||||
strikeCell: this.strikeCell,
|
||||
value: value as ScoreCellValue,
|
||||
};
|
||||
|
||||
if (value === CellFlag.strike) {
|
||||
return <StrikeKadiCell unstrikeCell={this.unstrikeCell} />;
|
||||
}
|
||||
else {
|
||||
switch (fieldType) {
|
||||
case FieldType.bonus:
|
||||
case FieldType.subtotal:
|
||||
case FieldType.total:
|
||||
case FieldType.globalTotal:
|
||||
return (
|
||||
<GenericResultsKadiCell
|
||||
classNameString={fieldType}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
case FieldType.bool:
|
||||
return <BoolKadiCell {...propsForEditableCell}/>;
|
||||
case FieldType.multiplier:
|
||||
return <MultipleKadiCell {...propsForEditableCell}/>;
|
||||
case FieldType.number:
|
||||
return <NumberKadiCell {...propsForEditableCell}/>;
|
||||
case FieldType.yahtzee:
|
||||
return <YahtzeeKadiCell {...propsForEditableCell}/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface StandardKadiCellProps {
|
||||
value: ScoreCellValue,
|
||||
}
|
||||
|
||||
interface StrikeKadiCellProps {
|
||||
unstrikeCell: () => void,
|
||||
}
|
||||
|
||||
interface ResultsKadiCellProps extends StandardKadiCellProps {
|
||||
}
|
||||
|
||||
interface UpdateableKadiCellProps extends StandardKadiCellProps {
|
||||
updateCell: (updateVal: ScoreCellValue) => void,
|
||||
}
|
||||
|
||||
interface LongPressStrikeKadiCellProps extends StandardKadiCellProps {
|
||||
timeoutMs: number,
|
||||
strikeCell: () => void,
|
||||
}
|
||||
|
||||
interface GenericResultsKadiCellProps extends ResultsKadiCellProps {
|
||||
classNameString: string;
|
||||
}
|
||||
|
||||
type EditableKadiCellProps = UpdateableKadiCellProps & LongPressStrikeKadiCellProps;
|
||||
|
||||
const NumberKadiCell: React.FunctionComponent<EditableKadiCellProps> = ({ strikeCell, updateCell, value , timeoutMs}) => {
|
||||
const [beingEdited, setBeingEdited] = React.useState(false);
|
||||
const [currentEditValue, setCurrentEditValue] = React.useState("");
|
||||
const strikeCellOnLongPress = useLongPress(strikeCell, timeoutMs);
|
||||
|
||||
const displayText: string = beingEdited ? currentEditValue : value.toString();
|
||||
|
||||
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
|
||||
setBeingEdited(true);
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentEditValue(e.target.value);
|
||||
if (e.target.value == "") {
|
||||
strikeCell();
|
||||
endInput();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||
submitInput(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
submitInput(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const submitInput = (input: string) => {
|
||||
updateCell(Number(input));
|
||||
endInput();
|
||||
};
|
||||
|
||||
const endInput = () => {
|
||||
setBeingEdited(false);
|
||||
setCurrentEditValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<td className={"kadiCell editable numberField"}>
|
||||
<div className={"numberField"}>
|
||||
<input
|
||||
type={"number"}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onInput={handleChange}
|
||||
onChange={handleChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
value={displayText}
|
||||
className={"numberField"}
|
||||
onAuxClick={strikeCell}
|
||||
{...strikeCellOnLongPress}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
const YahtzeeKadiCell: React.FunctionComponent<EditableKadiCellProps> = ({value, timeoutMs, strikeCell, updateCell}) => {
|
||||
const handleClick = (): void => updateCell(true);
|
||||
const strikeCellOnLongPress = useLongPress(strikeCell, timeoutMs);
|
||||
return (
|
||||
<td
|
||||
className={"kadiCell editable yahtzeeField"}
|
||||
onClick={handleClick}
|
||||
{...strikeCellOnLongPress}
|
||||
>
|
||||
<div className={"yahtzeeField"}>
|
||||
{value}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
const BoolKadiCell: React.FunctionComponent<EditableKadiCellProps> = ({value, timeoutMs, strikeCell, updateCell}) => {
|
||||
const handleClick = (): void => updateCell(true);
|
||||
const strikeCellOnLongPress = useLongPress(strikeCell, timeoutMs);
|
||||
return (
|
||||
<td
|
||||
className={"kadiCell editable boolField " + (value ? "checked" : "unchecked")}
|
||||
>
|
||||
<div
|
||||
className="clickableArea"
|
||||
onClick={handleClick}
|
||||
{...strikeCellOnLongPress}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
const MultipleKadiCell: React.FunctionComponent<EditableKadiCellProps> = ({value, timeoutMs, strikeCell, updateCell}) => {
|
||||
const handleClick = (): void => updateCell(true);
|
||||
const strikeCellOnLongPress = useLongPress(strikeCell, timeoutMs);
|
||||
return (
|
||||
<td
|
||||
className={"kadiCell editable multipleField"}
|
||||
onClick={handleClick}
|
||||
{...strikeCellOnLongPress}
|
||||
>
|
||||
<div className={"multipleField"}>
|
||||
{value}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
const GenericResultsKadiCell: React.FunctionComponent<GenericResultsKadiCellProps> = ({value, classNameString}) => {
|
||||
return (
|
||||
<td className={"kadiCell " + classNameString}>
|
||||
<div className={classNameString}>
|
||||
{value}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
const StrikeKadiCell: React.FunctionComponent<StrikeKadiCellProps> = ({unstrikeCell}) => {
|
||||
const updateCell = () => unstrikeCell();
|
||||
return (
|
||||
<td
|
||||
className={"kadiCell strikeCell"}
|
||||
onClick={updateCell}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default KadiCell;
|
||||
33
src/Components/KadiEditableRowCells.tsx
Executable file
33
src/Components/KadiEditableRowCells.tsx
Executable file
@@ -0,0 +1,33 @@
|
||||
import {CellLocation} from "../Classes/PlayerScoreCard";
|
||||
import {FieldType} from "../static/enums";
|
||||
import React, {ReactElement} from "react";
|
||||
import KadiCell from "./KadiCell";
|
||||
import {CellEventResponse, CellScores} from "./KadiBoard";
|
||||
|
||||
interface KadiEditableRowCellsProps {
|
||||
location: CellLocation;
|
||||
fieldType: FieldType;
|
||||
scores: CellScores;
|
||||
onCellEdit(res: CellEventResponse): void;
|
||||
}
|
||||
|
||||
const KadiEditableRowCells: React.FunctionComponent<KadiEditableRowCellsProps> = ({ location, fieldType, scores, onCellEdit }) => {
|
||||
const cells: ReactElement[] = [];
|
||||
|
||||
for (const playerId in scores) {
|
||||
cells.push((
|
||||
<KadiCell
|
||||
key={"cell" + location.cellId + location.blockId + playerId}
|
||||
location={location}
|
||||
fieldType={fieldType}
|
||||
playerId={playerId}
|
||||
value={scores[playerId]}
|
||||
onCellEdit={onCellEdit}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return <>{cells}</>;
|
||||
};
|
||||
|
||||
export default KadiEditableRowCells;
|
||||
47
src/Components/KadiGrandTotalRow.tsx
Executable file
47
src/Components/KadiGrandTotalRow.tsx
Executable file
@@ -0,0 +1,47 @@
|
||||
import React, {ReactNode} from "react";
|
||||
import {LocaleContext} from "../static/strings";
|
||||
import KadiCell from "./KadiCell";
|
||||
import {FieldType} from "../static/enums";
|
||||
import {Icon} from "semantic-ui-react";
|
||||
import {CellScores} from "./KadiBoard";
|
||||
|
||||
interface KadiGrandTotalRowProps {
|
||||
showResults: boolean;
|
||||
scores: CellScores;
|
||||
toggleShowResults(): void;
|
||||
}
|
||||
|
||||
const KadiGrandTotalRow: React.FunctionComponent<KadiGrandTotalRowProps> = ({ showResults, toggleShowResults, scores}) => {
|
||||
const cells: ReactNode[] = [];
|
||||
const Locale = React.useContext(LocaleContext).strings;
|
||||
|
||||
for (const playerId in scores) {
|
||||
cells.push((
|
||||
<KadiCell
|
||||
key={"cell_grandtotal_" + playerId}
|
||||
location={{blockId: "global", cellId: "grandTotal"}}
|
||||
fieldType={FieldType.globalTotal}
|
||||
playerId={playerId}
|
||||
value={scores[playerId]}
|
||||
onCellEdit={() => {}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<tr
|
||||
key={"rowContGrandTotal"}
|
||||
className={"kadiRow " + FieldType.globalTotal + (showResults ? "" : " hideResults")}
|
||||
>
|
||||
<td
|
||||
onClick={toggleShowResults}
|
||||
className="kadiCell rowLabelCell"
|
||||
>
|
||||
{Locale.rowLabels.globalTotal}
|
||||
<Icon className={"showResultsIcon"} name={showResults ? "hide" : "unhide"} />
|
||||
</td>
|
||||
{cells}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default KadiGrandTotalRow;
|
||||
45
src/Components/useLongPress.ts
Executable file
45
src/Components/useLongPress.ts
Executable file
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
|
||||
interface useLongPressReturnProps {
|
||||
onMouseDown: () => void,
|
||||
onTouchStart: () => void,
|
||||
onMouseUp: () => void,
|
||||
onMouseLeave: () => void,
|
||||
onTouchEnd: () => void,
|
||||
}
|
||||
|
||||
export const useLongPress: (onLongPress: () => void, timeoutMs: number) => useLongPressReturnProps = (
|
||||
onLongPress: () => void,
|
||||
timeoutMs: number
|
||||
) => {
|
||||
const [doingLongPress, updateDoingLongPress] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
let timerId: number = 0;
|
||||
if (doingLongPress) {
|
||||
timerId = window.setTimeout(onLongPress, timeoutMs);
|
||||
}
|
||||
else {
|
||||
window.clearTimeout(timerId);
|
||||
}
|
||||
return () => {
|
||||
window.clearTimeout(timerId);
|
||||
};
|
||||
}, [doingLongPress]);
|
||||
|
||||
const startLongPress = React.useCallback(() => {
|
||||
updateDoingLongPress(true);
|
||||
}, []);
|
||||
|
||||
const stopLongPress = React.useCallback(() => {
|
||||
updateDoingLongPress(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onMouseDown: startLongPress,
|
||||
onTouchStart: startLongPress,
|
||||
onMouseUp: stopLongPress,
|
||||
onMouseLeave: stopLongPress,
|
||||
onTouchEnd: stopLongPress,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user