New guests can be added, returning players recognised, and games are saved properly, locking the board. Changed webpack config to compile typescript with babel.

This commit is contained in:
Daniel Ledda
2020-05-25 22:52:10 +02:00
parent f08105c39f
commit 5342b7a48e
14 changed files with 333 additions and 157 deletions

View File

@@ -1,3 +1,8 @@
{ {
"presets": ["@babel/env", "@babel/preset-react"] "presets": [
"@babel/typescript",
"@babel/react",
"@babel/env"
],
"plugins": ["@babel/proposal-class-properties"]
} }

68
package-lock.json generated
View File

@@ -147,6 +147,20 @@
"semver": "^5.5.0" "semver": "^5.5.0"
} }
}, },
"@babel/helper-create-class-features-plugin": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.6.tgz",
"integrity": "sha512-6N9IeuyHvMBRyjNYOMJHrhwtu4WJMrYf8hVbEHD3pbbbmNOk1kmXSQs7bA4dYDUaIx4ZEzdnvo6NwC3WHd/Qow==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.9.5",
"@babel/helper-member-expression-to-functions": "^7.8.3",
"@babel/helper-optimise-call-expression": "^7.8.3",
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/helper-replace-supers": "^7.9.6",
"@babel/helper-split-export-declaration": "^7.8.3"
}
},
"@babel/helper-create-regexp-features-plugin": { "@babel/helper-create-regexp-features-plugin": {
"version": "7.8.8", "version": "7.8.8",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz",
@@ -366,6 +380,16 @@
"@babel/plugin-syntax-async-generators": "^7.8.0" "@babel/plugin-syntax-async-generators": "^7.8.0"
} }
}, },
"@babel/plugin-proposal-class-properties": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz",
"integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==",
"dev": true,
"requires": {
"@babel/helper-create-class-features-plugin": "^7.8.3",
"@babel/helper-plugin-utils": "^7.8.3"
}
},
"@babel/plugin-proposal-dynamic-import": { "@babel/plugin-proposal-dynamic-import": {
"version": "7.8.3", "version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz",
@@ -537,6 +561,15 @@
"@babel/helper-plugin-utils": "^7.8.3" "@babel/helper-plugin-utils": "^7.8.3"
} }
}, },
"@babel/plugin-syntax-typescript": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.8.3.tgz",
"integrity": "sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.8.3"
}
},
"@babel/plugin-transform-arrow-functions": { "@babel/plugin-transform-arrow-functions": {
"version": "7.8.3", "version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz",
@@ -885,6 +918,17 @@
"@babel/helper-plugin-utils": "^7.8.3" "@babel/helper-plugin-utils": "^7.8.3"
} }
}, },
"@babel/plugin-transform-typescript": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.9.6.tgz",
"integrity": "sha512-8OvsRdvpt3Iesf2qsAn+YdlwAJD7zJ+vhFZmDCa4b8dTp7MmHtKk5FF2mCsGxjZwuwsy/yIIay/nLmxST1ctVQ==",
"dev": true,
"requires": {
"@babel/helper-create-class-features-plugin": "^7.9.6",
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/plugin-syntax-typescript": "^7.8.3"
}
},
"@babel/plugin-transform-unicode-regex": { "@babel/plugin-transform-unicode-regex": {
"version": "7.8.3", "version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz",
@@ -895,6 +939,15 @@
"@babel/helper-plugin-utils": "^7.8.3" "@babel/helper-plugin-utils": "^7.8.3"
} }
}, },
"@babel/polyfill": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.8.7.tgz",
"integrity": "sha512-LeSfP9bNZH2UOZgcGcZ0PIHUt1ZuHub1L3CVmEyqLxCeDLm4C5Gi8jRH8ZX2PNpDhQCo0z6y/+DIs2JlliXW8w==",
"requires": {
"core-js": "^2.6.5",
"regenerator-runtime": "^0.13.4"
}
},
"@babel/preset-env": { "@babel/preset-env": {
"version": "7.9.6", "version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.6.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.6.tgz",
@@ -990,6 +1043,16 @@
"@babel/plugin-transform-react-jsx-source": "^7.9.0" "@babel/plugin-transform-react-jsx-source": "^7.9.0"
} }
}, },
"@babel/preset-typescript": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.9.0.tgz",
"integrity": "sha512-S4cueFnGrIbvYJgwsVFKdvOmpiL0XGw9MFW9D0vgRys5g36PBhZRL8NX8Gr2akz8XRtzq6HuDXPD/1nniagNUg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/plugin-transform-typescript": "^7.9.0"
}
},
"@babel/runtime": { "@babel/runtime": {
"version": "7.9.6", "version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
@@ -2263,6 +2326,11 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true "dev": true
}, },
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
},
"core-js-compat": { "core-js-compat": {
"version": "3.6.5", "version": "3.6.5",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",

View File

@@ -2,11 +2,12 @@
"name": "kadi-board", "name": "kadi-board",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"homepage": "/kadi/game/", "homepage": "/kadi/game",
"serverRoot": "/kadi",
"license": "ISC", "license": "ISC",
"author": "Daniel Ledda", "author": "Daniel Ledda",
"scripts": { "scripts": {
"build-dev": "webpack --mode development && postbuild", "build-dev": "webpack --mode development && npm postbuild",
"build": "webpack --mode production", "build": "webpack --mode production",
"postbuild": "rsync -avu --delete dist/ ../kadi_backend/static/game", "postbuild": "rsync -avu --delete dist/ ../kadi_backend/static/game",
"start": "webpack-dev-server --mode development", "start": "webpack-dev-server --mode development",
@@ -15,17 +16,20 @@
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6", "@babel/core": "^7.9.6",
"@babel/polyfill": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.9.6", "@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0",
"@types/node": "^13.11.1", "@types/node": "^13.11.1",
"@types/react": "^16.9.34", "@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7", "@types/react-dom": "^16.9.7",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.5",
"font-loader": "^0.1.2",
"ignore-loader": "^0.1.2",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"font-loader": "^0.1.2",
"ignore-loader": "^0.1.2",
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.12.21",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"ts-loader": "^7.0.3", "ts-loader": "^7.0.3",

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="static/favicon.ico" /> <link rel="icon" href="static/game/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
@@ -17,6 +17,6 @@
<noscript> <noscript>
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<script src="static/bundle.js"></script> <script src="static/game/bundle.js"></script>
</body> </body>
</html> </html>

View File

@@ -3,11 +3,19 @@ import "../static/css/game.css";
import KadiBoard from "./KadiBoard"; import KadiBoard from "./KadiBoard";
import GameSetup, {GameSettings} from "./GameSetup" import GameSetup, {GameSettings} from "./GameSetup"
import Settings from "../static/settings.json"; import Settings from "../static/settings.json";
import {SupportedLang} from "../static/enums"; import {SERVER_BASE_NAME} from "../index";
import axios from "axios";
export interface Player {
id: string;
nick: string;
}
interface GameState { interface GameState {
currentSettings: GameSettings; currentSettings: GameSettings;
settingUp: boolean; settingUp: boolean;
loading: boolean;
availablePlayers: Player[]
} }
interface GameProps {} interface GameProps {}
@@ -19,16 +27,36 @@ class Game extends React.Component<GameProps, GameState> {
super(props); super(props);
const startupSettings: GameSettings = { const startupSettings: GameSettings = {
playerIds: Settings.players, players: [],
ruleset: Settings.ruleset, ruleset: Settings.ruleset,
}; };
this.state = { this.state = {
currentSettings: startupSettings, currentSettings: startupSettings,
settingUp: true, settingUp: true,
loading: true,
availablePlayers: [],
}; };
} }
componentDidMount(): void {
axios.get(SERVER_BASE_NAME + "/api/players")
.then(response => this.addNewPlayers([...response.data.guests, response.data.mainPlayer]))
.catch(error => this.handleError(error))
.finally(() => this.setState({ loading: false }));
console.log(this.state.availablePlayers);
}
addNewPlayers(players: Player[]) {
const availablePlayers: Player[] = this.state.availablePlayers;
availablePlayers.push(...players);
this.setState({availablePlayers});
}
handleError(error: any): void {
};
onSetupComplete: (gameSettings: GameSettings) => void = (gameSettings) => { onSetupComplete: (gameSettings: GameSettings) => void = (gameSettings) => {
this.setState({ this.setState({
currentSettings: gameSettings, currentSettings: gameSettings,
@@ -43,11 +71,12 @@ class Game extends React.Component<GameProps, GameState> {
render(): ReactNode { render(): ReactNode {
return ( return (
<> <>
{this.state.settingUp ? {!this.state.loading && (this.state.settingUp ?
( (
<GameSetup <GameSetup
onSetupComplete={this.onSetupComplete} onSetupComplete={this.onSetupComplete}
settings={this.state.currentSettings} settings={this.state.currentSettings}
availablePlayers={this.state.availablePlayers}
/> />
) : ) :
( (
@@ -56,7 +85,7 @@ class Game extends React.Component<GameProps, GameState> {
returnToSetup={this.returnToSetup} returnToSetup={this.returnToSetup}
/> />
) )
} )}
</> </>
); );
} }

View File

@@ -1,31 +1,30 @@
import React, {ChangeEvent, FocusEvent, KeyboardEvent, ReactNode} from "react"; import React, {ChangeEvent, FocusEvent, KeyboardEvent, ReactNode} from "react";
import {getSchemaListings, SchemaListing} from "../static/rulesets"; import {getSchemaListings, SchemaListing} from "../static/rulesets";
import {LanguageNames} from "../static/strings";
import LocaleContext from "../LocaleContext"; import LocaleContext from "../LocaleContext";
import {SupportedLang} from "../static/enums";
import {Button} from "semantic-ui-react"; import {Button} from "semantic-ui-react";
import {Player} from "./Game";
interface GameSetupProps { interface GameSetupProps {
onSetupComplete: (settings: GameSettings) => void; onSetupComplete: (settings: GameSettings) => void;
settings: GameSettings; settings: GameSettings;
availablePlayers: Player[];
} }
interface GameSetupState { interface GameSetupState {
selectedRuleset: string; selectedRuleset: string;
enteredPlayerIds: string[]; enteredPlayers: Player[];
editingPlayerName: boolean; editingPlayerName: boolean;
} }
export interface GameSettings { export interface GameSettings {
ruleset: string; ruleset: string;
playerIds: string[]; players: Player[];
} }
class GameSetup extends React.Component<GameSetupProps, GameSetupState> { class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
private readonly availableRulesets: SchemaListing[]; private readonly availableRulesets: SchemaListing[];
private changeLang: (lang: string) => void; private changeLang: (lang: string) => void;
state: GameSetupState; state: GameSetupState;
constructor(props: GameSetupProps) { constructor(props: GameSetupProps) {
super(props); super(props);
@@ -33,7 +32,7 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
this.changeLang = () => {}; this.changeLang = () => {};
this.state = { this.state = {
selectedRuleset: this.props.settings.ruleset, selectedRuleset: this.props.settings.ruleset,
enteredPlayerIds: this.props.settings.playerIds, enteredPlayers: this.props.settings.players,
editingPlayerName: false, editingPlayerName: false,
}; };
} }
@@ -43,23 +42,38 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
}; };
removePlayer: (index: number) => void = (index) => { removePlayer: (index: number) => void = (index) => {
const newPlayers = this.state.enteredPlayerIds.slice(); const newPlayers = this.state.enteredPlayers.slice();
newPlayers.splice(index, 1); newPlayers.splice(index, 1);
this.setState({enteredPlayerIds: newPlayers}); this.setState({enteredPlayers: newPlayers});
}; };
addPlayer: (playerSubmission: string, keepEditing: boolean) => void = (playerSubmission, keepEditing) => { addPlayer: (playerSubmission: string, keepEditing: boolean) => void = (playerSubmission, keepEditing) => {
const newPlayers = this.state.enteredPlayerIds.slice(); const newPlayers = this.state.enteredPlayers.slice();
if (!newPlayers.find(enteredPlayer => enteredPlayer == playerSubmission)) { if (!this.alreadyPlaying(playerSubmission)) {
newPlayers.push(playerSubmission); newPlayers.push({
id: this.playerNameToId(playerSubmission) ?? playerSubmission,
nick: playerSubmission
});
} }
this.setState({enteredPlayerIds: newPlayers, editingPlayerName: keepEditing}); this.setState({enteredPlayers: newPlayers, editingPlayerName: keepEditing});
}; };
alreadyPlaying(playerName: string): boolean {
return !!this.state.enteredPlayers.find(player => player.nick === playerName);
}
playerNameToId(playerName: string): string | undefined {
return this.props.availablePlayers.find(player => player.nick === playerName)?.id;
}
playerIsNew(player: Player) {
return player.id === player.nick;
}
submitSettings: () => void = () => { submitSettings: () => void = () => {
this.props.onSetupComplete({ this.props.onSetupComplete({
ruleset: this.state.selectedRuleset, ruleset: this.state.selectedRuleset,
playerIds: this.state.enteredPlayerIds, players: this.state.enteredPlayers,
}); });
}; };
@@ -85,19 +99,14 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
} }
const playerListing: ReactNode[] = []; const playerListing: ReactNode[] = [];
for (let i = 0; i < this.state.enteredPlayerIds.length; i++) { for (let i = 0; i < this.state.enteredPlayers.length; i++) {
const playerName = this.state.enteredPlayerIds[i];
playerListing.push(( playerListing.push((
<div <ActivePlayerListItem
key={playerName + "_list"} key={i}
className={"option playerOption"} playerIsNew={this.playerIsNew(this.state.enteredPlayers[i])}
> removePlayer={() => this.removePlayer(i)}
{playerName} playerName={this.state.enteredPlayers[i].nick}
<span
className={"trashButton"}
onClick={() => this.removePlayer(i)}
/> />
</div>
)); ));
} }
playerListing.push(( playerListing.push((
@@ -105,6 +114,7 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
playersListEmpty={playerListing.length === 0} playersListEmpty={playerListing.length === 0}
submitNewPlayer={this.addPlayer} submitNewPlayer={this.addPlayer}
userEditing={this.state.editingPlayerName} userEditing={this.state.editingPlayerName}
availablePlayers={this.props.availablePlayers}
/> />
)); ));
@@ -137,7 +147,7 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
size={"huge"} size={"huge"}
color={"blue"} color={"blue"}
onClick={this.submitSettings} onClick={this.submitSettings}
disabled={this.state.enteredPlayerIds.length < 1} disabled={this.state.enteredPlayers.length < 1}
> >
{Locale.setupScreen.startGame} {Locale.setupScreen.startGame}
</Button> </Button>
@@ -150,7 +160,15 @@ class GameSetup extends React.Component<GameSetupProps, GameSetupState> {
} }
GameSetup.contextType = LocaleContext; GameSetup.contextType = LocaleContext;
const AddPlayerField: React.FunctionComponent<AddPlayerFieldProps> = ({playersListEmpty, submitNewPlayer, userEditing}) => { interface AddPlayerFieldProps {
playersListEmpty: boolean;
submitNewPlayer: (name: string, keepEditing: boolean) => void;
userEditing: boolean;
availablePlayers: Player[];
}
const AddPlayerField: React.FunctionComponent<AddPlayerFieldProps> = (props) => {
const {playersListEmpty, submitNewPlayer, userEditing, availablePlayers} = props;
const Locale = React.useContext(LocaleContext).strings; const Locale = React.useContext(LocaleContext).strings;
const [beingEdited, updateBeingEdited] = React.useState(false); const [beingEdited, updateBeingEdited] = React.useState(false);
@@ -205,10 +223,35 @@ const AddPlayerField: React.FunctionComponent<AddPlayerFieldProps> = ({playersLi
); );
}; };
interface AddPlayerFieldProps { interface ActivePlayerListItemProps {
playersListEmpty: boolean; removePlayer: () => any;
submitNewPlayer: (name: string, keepEditing: boolean) => void; playerName: string;
userEditing: boolean; playerIsNew: boolean;
} }
const ActivePlayerListItem: React.FunctionComponent<ActivePlayerListItemProps> = (props) => {
const {removePlayer, playerName, playerIsNew} = props;
const Locale = React.useContext(LocaleContext).strings;
return (
<>
<div
className={"option playerOption"}
>
<div className={"playerText"}>
{playerName}
{playerIsNew &&
<span className={"newPlayerText"}>
{Locale.setupScreen.playerNew}
</span>
}
</div>
<div
className={"trashButton"}
onClick={removePlayer}
/>
</div>
</>
);
};
export default GameSetup; export default GameSetup;

View File

@@ -1,7 +1,6 @@
import React, {ReactElement, ReactNode, useContext} from "react"; import React, {ReactElement, ReactNode, useContext} from "react";
import PlayerScoreCard, {CellLocation, PlayerScoreCardJSONRepresentation} from "../Classes/PlayerScoreCard"; import PlayerScoreCard, {CellLocation, PlayerScoreCardJSONRepresentation} from "../Classes/PlayerScoreCard";
import {BlockDef, GameSchema, getGameSchemaById} from "../static/rulesets"; import {GameSchema, getGameSchemaById} from "../static/rulesets";
import {formatUnicorn} from "../static/strings";
import LocaleContext from "../LocaleContext"; import LocaleContext from "../LocaleContext";
import {CellFlag} from "../static/enums"; import {CellFlag} from "../static/enums";
import {ScoreCellValue} from "../Classes/ScoreCell"; import {ScoreCellValue} from "../Classes/ScoreCell";
@@ -14,6 +13,8 @@ import logo from "../static/images/kadi.png";
import KadiGrandTotalRow from "./KadiGrandTotalRow"; import KadiGrandTotalRow from "./KadiGrandTotalRow";
import KadiBlockRenderer from "./KadiBlockRenderer"; import KadiBlockRenderer from "./KadiBlockRenderer";
import {KadiCellDisplayValue} from "./KadiCell"; import {KadiCellDisplayValue} from "./KadiCell";
import {Player} from "./Game";
import {SERVER_BASE_NAME} from "../index";
export interface CellScores { export interface CellScores {
@@ -40,9 +41,11 @@ export interface KadiBoardProps {
interface KadiBoardState { interface KadiBoardState {
scoreSheet: ScoreSheet; scoreSheet: ScoreSheet;
playerIds: string[]; players: Player[];
showResults: boolean; showResults: boolean;
savingGame: boolean; savingGame: boolean;
locked: boolean;
saved: boolean;
} }
interface ScoreSheet { interface ScoreSheet {
@@ -60,33 +63,37 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
this.gameSchema = getGameSchemaById(this.props.settings.ruleset); this.gameSchema = getGameSchemaById(this.props.settings.ruleset);
this.state = { this.state = {
scoreSheet: this.generateNewScoreSheet(this.props.settings.playerIds), scoreSheet: this.generateNewScoreSheet(this.props.settings.players),
playerIds: this.props.settings.playerIds, players: this.props.settings.players,
showResults: true, showResults: true,
savingGame: false, savingGame: false,
locked: false,
saved: false,
}; };
this.caretaker = new CaretakerSet( this.caretaker = new CaretakerSet(
Settings.maxHistoryLength, Settings.maxHistoryLength,
...this.state.playerIds.map( ...this.state.players.map(
pid => this.state.scoreSheet[pid] player => this.state.scoreSheet[player.id]
) )
); );
} }
private generateNewScoreSheet(playerIds: string[]): ScoreSheet { private generateNewScoreSheet(players: Player[]): ScoreSheet {
const scoreSheet: ScoreSheet = {}; const scoreSheet: ScoreSheet = {};
for (const playerId of playerIds) { for (const player of players) {
scoreSheet[playerId] = new PlayerScoreCard(playerId, this.gameSchema); scoreSheet[player.id] = new PlayerScoreCard(player.id, this.gameSchema);
} }
return scoreSheet; return scoreSheet;
} }
private onCellEdit = (response: CellEventResponse): void => { private onCellEdit = (response: CellEventResponse): void => {
if (!this.state.locked) {
const newScoreSheet = this.state.scoreSheet; const newScoreSheet = this.state.scoreSheet;
KadiBoard.updateScoreSheetFromCellResponse(newScoreSheet, response); KadiBoard.updateScoreSheetFromCellResponse(newScoreSheet, response);
this.setState({ scoreSheet: newScoreSheet }); this.setState({ scoreSheet: newScoreSheet });
this.caretaker.save(); this.caretaker.save();
}
}; };
private static updateScoreSheetFromCellResponse(scoreSheet: ScoreSheet, response: CellEventResponse): void { private static updateScoreSheetFromCellResponse(scoreSheet: ScoreSheet, response: CellEventResponse): void {
@@ -137,12 +144,16 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
JSONScoreCards.push(this.state.scoreSheet[playerId].getJSONRepresentation()); JSONScoreCards.push(this.state.scoreSheet[playerId].getJSONRepresentation());
} }
return JSON.stringify({ return JSON.stringify({
gameType: this.gameSchema.id, //rulesetUsed: this.gameSchema.id,
players: this.state.players,
results: JSONScoreCards results: JSONScoreCards
}); });
} }
private canSave(): boolean { private canSave(): boolean {
if (this.state.saved) {
return false;
}
for (const playerId in this.state.scoreSheet) { for (const playerId in this.state.scoreSheet) {
if (!this.state.scoreSheet[playerId].filledOut()) { if (!this.state.scoreSheet[playerId].filledOut()) {
return false; return false;
@@ -152,18 +163,19 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
} }
private saveGame: () => void = async () => { private saveGame: () => void = async () => {
this.setState({savingGame: true}); this.setState({savingGame: true}, () => {
axios.post(Settings.rootUrl + "/api/savegame", axios.post(SERVER_BASE_NAME + "/api/games",
this.getJSONRepresentationForBoard(), this.getJSONRepresentationForBoard(),
{headers: {"Content-Type": "application/json"}} {headers: {"Content-Type": "application/json"}}
) )
.then(response => this.onGameSave(response.data)) .then(response => this.onGameSave(response.data))
.catch(error => this.onSaveError(error)) .catch(error => this.onSaveError(error))
.finally(() => this.setState({ savingGame: false })); .finally(() => this.setState({ savingGame: false }));
});
}; };
private onGameSave = (serverResponse: string) => { private onGameSave = (serverResponse: string) => {
console.log("Response:", serverResponse); this.setState({locked: true, saved: true});
}; };
private onSaveError = (error: any) => { private onSaveError = (error: any) => {
@@ -179,13 +191,13 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
for (const cell of block.cells) { for (const cell of block.cells) {
scores[cell.id] = {}; scores[cell.id] = {};
} }
this.state.playerIds.forEach(pid => { this.state.players.forEach(player => {
scores.totals[pid] = this.getBlockTotalByPlayerId(block.id, pid); scores.totals[player.id] = this.getBlockTotalByPlayerId(block.id, player.id);
scores.bonuses[pid] = this.playerHasBonusForBlock(pid, block.id); scores.bonuses[player.id] = this.playerHasBonusForBlock(player.id, block.id);
scores.subtotals[pid] = this.getBlockSubtotalByPlayerId(block.id, pid); scores.subtotals[player.id] = this.getBlockSubtotalByPlayerId(block.id, player.id);
for (const cell of block.cells) { for (const cell of block.cells) {
scores[cell.id][pid] = this.getCellDisplayValueByPlayerIdAndLocation( scores[cell.id][player.id] = this.getCellDisplayValueByPlayerIdAndLocation(
pid, { blockId: block.id, cellId: cell.id }); player.id, { blockId: block.id, cellId: cell.id });
} }
}); });
rows.push( rows.push(
@@ -200,8 +212,8 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
} }
const grandTotals: CellScores = {}; const grandTotals: CellScores = {};
this.state.playerIds.forEach(pid => this.state.players.forEach(player =>
grandTotals[pid] = this.getTotalForPlayer(pid) grandTotals[player.id] = this.getTotalForPlayer(player.id)
); );
rows.push( rows.push(
<KadiGrandTotalRow <KadiGrandTotalRow
@@ -217,7 +229,7 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
<table className="kadiTable"> <table className="kadiTable">
<thead> <thead>
<tr> <tr>
<th colSpan={this.state.playerIds.length + 1}> <th colSpan={this.state.players.length + 1}>
<Header inverted={true} > <Header inverted={true} >
<Image spaced={true} size={"small"} src={logo} /> <Image spaced={true} size={"small"} src={logo} />
<Header.Content> <Header.Content>
@@ -228,7 +240,7 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ColumnHeadersRow playerIds={this.state.playerIds} /> <ColumnHeadersRow playerNames={this.state.players.map(player => player.nick)} />
{rows} {rows}
</tbody> </tbody>
</table> </table>
@@ -272,7 +284,9 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
loading={this.state.savingGame} loading={this.state.savingGame}
> >
<Icon name={"save"} /> <Icon name={"save"} />
{Locale.buttons.saveGameButton} {this.state.saved ?
Locale.buttons.saveGameButton.gameSaved :
Locale.buttons.saveGameButton.saveGame}
</Button> </Button>
</div> </div>
</Container> </Container>
@@ -284,10 +298,10 @@ class KadiBoard extends React.Component<KadiBoardProps, KadiBoardState> {
KadiBoard.contextType = LocaleContext; KadiBoard.contextType = LocaleContext;
interface ColumnHeadersRowProps { interface ColumnHeadersRowProps {
playerIds: string[]; playerNames: string[];
} }
const ColumnHeadersRow: React.FunctionComponent<ColumnHeadersRowProps> = ({ playerIds }) => { const ColumnHeadersRow: React.FunctionComponent<ColumnHeadersRowProps> = ({ playerNames }) => {
const Locale = useContext(LocaleContext).strings; const Locale = useContext(LocaleContext).strings;
const columnHeaders: ReactNode[] = [( const columnHeaders: ReactNode[] = [(
@@ -295,7 +309,7 @@ const ColumnHeadersRow: React.FunctionComponent<ColumnHeadersRowProps> = ({ play
{Locale.headers.rowLabels} {Locale.headers.rowLabels}
</td> </td>
)]; )];
for (const playerId of playerIds) { for (const playerId of playerNames) {
columnHeaders.push( columnHeaders.push(
<td className="playerNameCell" key={"header" + playerId}> <td className="playerNameCell" key={"header" + playerId}>
{playerId} {playerId}

0
src/filetypes.d.ts vendored Executable file → Normal file
View File

View File

@@ -1,10 +1,9 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import App from "./App"; import App from "./App";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
import "./filetypes.d"; export {serverRoot as SERVER_BASE_NAME} from "../package.json";
export const SERVER_BASE_NAME = "/kadi";
ReactDOM.render(( ReactDOM.render((
<React.StrictMode> <React.StrictMode>

View File

@@ -377,15 +377,24 @@ div.globalTotalField {
.playerOption { .playerOption {
cursor: default; cursor: default;
text-align: left; text-align: left;
display: flex;
} }
.playerOption .trashButton { span.newPlayerText {
color: #ebb600;
float: right;
font-style: italic;
padding-right: 15px;
}
.playerOption .playerText {
flex-grow: 1;
}
.trashButton {
cursor: pointer; cursor: pointer;
height: 20px; height: 20px;
width: 20px; width: 20px;
right: 10px;
top: 10px;
position: absolute;
background-size: contain; background-size: contain;
background-image: url(../images/trash.png); background-image: url(../images/trash.png);
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -1,6 +1,4 @@
{ {
"players": [],
"ruleset": "default_en", "ruleset": "default_en",
"maxHistoryLength": 256, "maxHistoryLength": 256
"rootUrl": "/kadi"
} }

View File

@@ -46,13 +46,11 @@ export const IntlStrings = {
undoButton: "Undo", undoButton: "Undo",
redoButton: "Redo", redoButton: "Redo",
returnToSetupButton: "Back to setup", returnToSetupButton: "Back to setup",
saveGameButton: "Save game", saveGameButton: {
savingGame: "Saving...", saveGame: "Save game",
gameSaved: "Success!",
}, },
languageNames: { savingGame: "Saving...",
en: "English",
de: "German",
it: "Italian"
}, },
setupScreen: { setupScreen: {
selectLanguage: "Change language:", selectLanguage: "Change language:",
@@ -61,6 +59,7 @@ export const IntlStrings = {
noPlayersEntered: "No players! Click here to add one...", noPlayersEntered: "No players! Click here to add one...",
clickToAddPlayer: "Click here to add a player...", clickToAddPlayer: "Click here to add a player...",
players: "Players:", players: "Players:",
playerNew: "new!",
}, },
}, },
de: { de: {
@@ -81,9 +80,12 @@ export const IntlStrings = {
undoButton: "Rückgängig", undoButton: "Rückgängig",
redoButton: "Wiederholen", redoButton: "Wiederholen",
returnToSetupButton: "Zurück zu Einstellungen", returnToSetupButton: "Zurück zu Einstellungen",
saveGameButton: "Spiel speichern", saveGameButton: {
saveGame: "Spiel speichern",
gameSaved: "Gespeichert!",
savingGame: "Wird gespeichert...", savingGame: "Wird gespeichert...",
}, },
},
setupScreen: { setupScreen: {
selectLanguage: "Sprache ändern:", selectLanguage: "Sprache ändern:",
selectRuleset: "Wähle ein Regelwerk aus:", selectRuleset: "Wähle ein Regelwerk aus:",
@@ -91,6 +93,7 @@ export const IntlStrings = {
noPlayersEntered: "Leer! Hier tippen und hinzufügen...", noPlayersEntered: "Leer! Hier tippen und hinzufügen...",
clickToAddPlayer: "Zum Hinzufügen hier tippen...", clickToAddPlayer: "Zum Hinzufügen hier tippen...",
players: "Mitspieler:", players: "Mitspieler:",
playerNew: "neu!",
}, },
}, },
it: { it: {
@@ -111,9 +114,12 @@ export const IntlStrings = {
undoButton: "Annullo", undoButton: "Annullo",
redoButton: "Ripristino", redoButton: "Ripristino",
returnToSetupButton: "Torna a impostazioni", returnToSetupButton: "Torna a impostazioni",
saveGameButton: "Salva gioco", saveGameButton: {
saveGame: "Salva gioco",
gameSaved: "Salvato!",
savingGame: "Salva...", savingGame: "Salva...",
}, },
},
setupScreen: { setupScreen: {
selectLanguage: "Cambia lingua:", selectLanguage: "Cambia lingua:",
selectRuleset: "Sceglia il regolamento:", selectRuleset: "Sceglia il regolamento:",
@@ -121,6 +127,7 @@ export const IntlStrings = {
noPlayersEntered: "Nessuno! Inserire un nome qui...", noPlayersEntered: "Nessuno! Inserire un nome qui...",
clickToAddPlayer: "Clicca per inserire un altro nome...", clickToAddPlayer: "Clicca per inserire un altro nome...",
players: "Giocatori:", players: "Giocatori:",
playerNew: "nuovo!",
}, },
}, },
} as const; } as const;

View File

@@ -13,11 +13,11 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true
},
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
"esnext" "esnext"
] ]
}
} }

View File

@@ -1,8 +1,10 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const Settings = require("./package.json");
const SERVER_ROOT = Settings.serverRoot;
module.exports = { module.exports = {
entry: "./src/index.tsx", entry: ["@babel/polyfill", "./src/index.tsx"],
mode: "production", mode: "production",
module: { module: {
rules: [ rules: [
@@ -10,11 +12,8 @@ module.exports = {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /(node_modules|bower_components|\.d\.ts$)/, exclude: /(node_modules|bower_components|\.d\.ts$)/,
use: [ use: [
{ "react-hot-loader/webpack",
loader: "babel-loader", "babel-loader",
options: { presets: ["@babel/env"] },
},
{ loader: "ts-loader" },
], ],
}, },
{ {
@@ -38,13 +37,14 @@ module.exports = {
resolve: { extensions: [".tsx", ".ts", ".js", "*"] }, resolve: { extensions: [".tsx", ".ts", ".js", "*"] },
output: { output: {
path: path.resolve(__dirname, "dist/"), path: path.resolve(__dirname, "dist/"),
publicPath: "/kadi/static/game/", publicPath: SERVER_ROOT + "/static/game/",
filename: "bundle.js" filename: "bundle.js"
}, },
devServer: { devServer: {
contentBase: path.join(__dirname, "public/"), contentBase: path.join(__dirname, "public/"),
contentBasePublicPath: SERVER_ROOT + "/",
port: 3000, port: 3000,
publicPath: "http://localhost:3000/", publicPath: "http://localhost:3000" + SERVER_ROOT + "/static/game/",
hotOnly: true hotOnly: true
}, },
plugins: [new webpack.HotModuleReplacementPlugin()] plugins: [new webpack.HotModuleReplacementPlugin()]