First commit to the new repository

:
This commit is contained in:
Daniel Ledda
2020-05-10 15:10:12 +02:00
commit d99d7cbfec
49 changed files with 10784 additions and 0 deletions

66
src/Components/Game.tsx Executable file
View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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&nbsp;&nbsp;A&nbsp;&nbsp;D&nbsp;&nbsp;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
View 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;

View 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;

View 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
View 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,
};
};