= (props) => {
const {loading, guestList, deleteGuest} = props;
const Uctx = React.useContext(UserContext);
- const listItems = guestList.map(guest =>
+ const listItems = guestList.map(guest => (
{guest.nick} - deleteGuest(guest.id)}>{Uctx.strings.general.deleteCommand}
- );
+ ));
return (
<>
{Uctx.strings.profilePage.guestsHeader}
- {loading ? (
+ {loading && guestList.length === 0 ? (
{Uctx.strings.profilePage.loadingGuests}
) : (
diff --git a/src/Components/KadiPageRoute.tsx b/src/Components/KadiPageRoute.tsx
new file mode 100644
index 0000000..6b06d8a
--- /dev/null
+++ b/src/Components/KadiPageRoute.tsx
@@ -0,0 +1,24 @@
+import {PageId} from "../enums";
+import React from "react";
+import {pageComponentFromId} from "../pageListings";
+import {Route, useRouteMatch} from "react-router-dom";
+import KadiPage from "./KadiPage";
+
+interface KadiPageRouteProps {
+ pageId: PageId
+}
+
+const KadiPageRoute: React.FunctionComponent = (props: KadiPageRouteProps) => {
+ const {pageId} = props;
+ const {path} = useRouteMatch();
+ const PageComponent = pageComponentFromId[pageId];
+ return (
+
+
+
+
+
+ );
+};
+
+export default KadiPageRoute;
\ No newline at end of file
diff --git a/src/Components/Loading.tsx b/src/Components/Loading.tsx
new file mode 100644
index 0000000..d8e378c
--- /dev/null
+++ b/src/Components/Loading.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import {Dimmer, Loader} from "semantic-ui-react";
+
+interface LoadingProps {}
+
+const Loading: React.FunctionComponent = (props) => {
+ return (
+
+ Loading
+
+ );
+};
+
+export default Loading;
\ No newline at end of file
diff --git a/src/Components/MainPageContent.tsx b/src/Components/MainPageContent.tsx
index 4970161..ab6c27e 100755
--- a/src/Components/MainPageContent.tsx
+++ b/src/Components/MainPageContent.tsx
@@ -18,9 +18,7 @@ const KadiPageMainContent: React.FunctionComponent = (
-
- {children}
-
+ {children}
);
diff --git a/src/Components/RulesetBlockTable.tsx b/src/Components/RulesetBlockTable.tsx
new file mode 100644
index 0000000..f0d8204
--- /dev/null
+++ b/src/Components/RulesetBlockTable.tsx
@@ -0,0 +1,207 @@
+import {BlockDef, CellDef} from "../Services/RulesetSchemaDto";
+import React, {useContext, useState} from "react";
+import RulesetCellTableRow from "./RulesetCellTableRow";
+import {Button, Dropdown, DropdownItemProps, Grid, Input, Table} from "semantic-ui-react";
+import UserContext from "../Contexts/UserContext";
+import {FieldType} from "../enums";
+
+interface RulesetBlockTableProps {
+ id: string;
+ blockDef: BlockDef | null;
+ editable?: boolean;
+ onAddCell?: (cellDef: CellDef, blockId: string) => any;
+}
+
+const RulesetBlockTable: React.FunctionComponent = (props) => {
+ const {id, blockDef, editable, onAddCell} = props;
+ const [currentCellInput, updateCurrentCellInput] = useState({
+ label: "",
+ maxMultiples: 0,
+ multiplier: 0,
+ fieldType: FieldType.number,
+ score: 0,
+ });
+ const {strings: Locale} = useContext(UserContext);
+
+ if (!blockDef) {
+ return (
+
+
+
+
+ {Locale.rulesetsPage.noBlocks}
+
+
+
+
+ );
+ }
+
+ const idAndCellList = Object.entries(blockDef.cells);
+ let subText = `${Locale.rulesetsPage.bonus}: `;
+
+ if (blockDef.hasBonus) {
+ subText += `${Locale.general.yes},
+ ${Locale.rulesetsPage.bonusScore}: ${blockDef.bonusScore},
+ ${Locale.rulesetsPage.bonusThreshold}: ${blockDef.bonusFor}`;
+ }
+ else {
+ subText += Locale.general.no;
+ }
+
+ const fieldTypeOptions: DropdownItemProps[] = [
+ {value: FieldType.bool, key: FieldType.bool, text: Locale.rulesetsPage[FieldType.bool]},
+ {value: FieldType.multiplier, key: FieldType.multiplier, text: Locale.rulesetsPage[FieldType.multiplier]},
+ {value: FieldType.number, key: FieldType.number, text: Locale.rulesetsPage[FieldType.number]},
+ {value: FieldType.superkadi, key: FieldType.superkadi, text: Locale.rulesetsPage[FieldType.superkadi]},
+ ];
+
+ const onChangeDropdown = (value: FieldType) => {
+ const newCell = {
+ ...currentCellInput,
+ fieldType: value,
+ };
+ if (value === FieldType.bool) {
+ newCell.score = 0;
+ }
+ else if (value === FieldType.multiplier) {
+ newCell.multiplier = 0;
+ newCell.maxMultiples = 5;
+ }
+ else if (value === FieldType.superkadi) {
+ newCell.maxMultiples = 5;
+ newCell.score = 50;
+ }
+ updateCurrentCellInput(newCell);
+ };
+
+ return (
+
+ );
+};
+
+export default RulesetBlockTable;
\ No newline at end of file
diff --git a/src/Components/RulesetCellTableRow.tsx b/src/Components/RulesetCellTableRow.tsx
new file mode 100644
index 0000000..f3a83e6
--- /dev/null
+++ b/src/Components/RulesetCellTableRow.tsx
@@ -0,0 +1,31 @@
+import {BoolCellDef, CellDef, MultiplierCellDef, SuperkadiCellDef} from "../Services/RulesetSchemaDto";
+import React, {useContext} from "react";
+import UserContext from "../Contexts/UserContext";
+import {Table} from "semantic-ui-react";
+
+interface TableCellRowProps {
+ id: string;
+ cellDef: CellDef;
+}
+
+const RulesetCellTableRow: React.FunctionComponent = (props) => {
+ const {id, cellDef} = props;
+ const {strings: Locale} = useContext(UserContext);
+ const displayValue = (cellDef as MultiplierCellDef).multiplier ??
+ (cellDef as SuperkadiCellDef | BoolCellDef).score ?? Locale.general.nA;
+ return (
+
+
+ {cellDef.label}
+
+
+ {Locale.rulesetsPage[cellDef.fieldType]}
+
+
+ {displayValue}
+
+
+ );
+};
+
+export default RulesetCellTableRow;
\ No newline at end of file
diff --git a/src/Components/RulesetDisplayPanel.tsx b/src/Components/RulesetDisplayPanel.tsx
new file mode 100644
index 0000000..c3d8df1
--- /dev/null
+++ b/src/Components/RulesetDisplayPanel.tsx
@@ -0,0 +1,67 @@
+import React, {useContext} from "react";
+import {
+ BlockDef,
+ BoolCellDef,
+ CellDef,
+ MultiplierCellDef,
+ RulesetSchemaDto,
+ SuperkadiCellDef
+} from "../Services/RulesetSchemaDto";
+import {Header, List, Table, TableBody} from "semantic-ui-react";
+import UserContext from "../Contexts/UserContext";
+import Loading from "./Loading";
+import RulesetBlockTable from "./RulesetBlockTable";
+
+interface RulesetDisplayPanelProps {
+ ruleset: RulesetSchemaDto;
+ loading: boolean;
+ editable?: boolean;
+ onAddCell?: (cellDef: CellDef, blockId: string) => any;
+}
+
+const RulesetDisplayPanel: React.FunctionComponent = (props) => {
+ const {ruleset, loading, editable, onAddCell} = props;
+ const {strings: Locale} = useContext(UserContext);
+
+ if (loading) {
+ return ;
+ }
+ else {
+ return (
+ <>
+
+
+
+
+ {Locale.rulesetsPage.fieldLabelHeader}
+
+
+ {Locale.rulesetsPage.fieldTypeHeader}
+
+
+ {Locale.rulesetsPage.fieldValueHeader}
+
+
+
+
+ {Object.entries(ruleset.blocks).length > 0 ?
+ Object.entries(ruleset.blocks).map(idAndBlock => (
+
+ )) : (
+
+ )}
+ >
+ );
+ }
+};
+
+export default RulesetDisplayPanel;
\ No newline at end of file
diff --git a/src/Components/RulesetList.tsx b/src/Components/RulesetList.tsx
new file mode 100644
index 0000000..6b71cc4
--- /dev/null
+++ b/src/Components/RulesetList.tsx
@@ -0,0 +1,39 @@
+import React, {useContext, useState} from "react";
+import {Icon, List, Menu} from "semantic-ui-react";
+import UserContext from "../Contexts/UserContext";
+
+interface RulesetListProps {
+ rulesetNames: string[];
+ onItemChange: (newItemId: string | "addNewRuleset") => any;
+ selectedItemIndex: number;
+ creatingRuleset: boolean;
+}
+
+const RulesetList: React.FunctionComponent = (props) => {
+ const {rulesetNames, onItemChange, selectedItemIndex, creatingRuleset} = props;
+ const {strings: Locale} = useContext(UserContext);
+
+ const selectedItem = creatingRuleset ? "" : rulesetNames[selectedItemIndex];
+
+ return (
+
+ );
+};
+
+export default RulesetList;
\ No newline at end of file
diff --git a/src/Components/RulesetsPage.tsx b/src/Components/RulesetsPage.tsx
index 317445b..8401b69 100755
--- a/src/Components/RulesetsPage.tsx
+++ b/src/Components/RulesetsPage.tsx
@@ -1,10 +1,19 @@
import React, {ReactElement} from "react";
-import {Header} from "semantic-ui-react";
+import {Container, Grid, Header, List, Segment} from "semantic-ui-react";
import UserContext from "../Contexts/UserContext";
+import KadiStatsService from "../Services/KadiStatsService";
+import RulesetList from "./RulesetList";
+import RulesetDisplayPanel from "./RulesetDisplayPanel";
+import {RulesetSchemaDto} from "../Services/RulesetSchemaDto";
+import CreateRulesetPanel from "./CreateRulesetPanel";
interface RulesetsPageProps {}
interface RulesetsPageState {
+ loading: boolean;
+ addingNewRuleset: boolean;
+ rulesets: RulesetSchemaDto[];
+ selectedRulesetIndex: number;
}
class RulesetsPage extends React.Component {
@@ -12,15 +21,70 @@ class RulesetsPage extends React.Component
super(props);
this.state = {
+ loading: true,
+ addingNewRuleset: false,
+ rulesets: [],
+ selectedRulesetIndex: 0,
};
}
+ componentDidMount() {
+ this.getRulesets();
+ }
+
+ async getRulesets() {
+ const rulesets = await KadiStatsService.getAllRulesets();
+ this.setState({rulesets, loading: false});
+ }
+
+ onRulesetSelect(newRulesetId: string | "addNewRuleset") {
+ if (newRulesetId === "addNewRuleset") {
+ this.setState({addingNewRuleset: true});
+ }
+ else {
+ this.setState({
+ addingNewRuleset: false,
+ selectedRulesetIndex: this.state.rulesets.findIndex(item => item.label === newRulesetId)
+ });
+ }
+ }
+
+ submitNewRuleset(ruleset: RulesetSchemaDto) {
+
+ }
+
render(): ReactElement {
const Locale = this.context.strings;
return (
-
- {Locale.rulesetsPage.title}
-
+ <>
+
+ {Locale.rulesetsPage.title}
+
+
+
+
+ this.onRulesetSelect(r)}
+ selectedItemIndex={this.state.selectedRulesetIndex}
+ creatingRuleset={this.state.addingNewRuleset}
+ rulesetNames={this.state.rulesets.map(ruleset => ruleset.label)}
+ />
+
+
+ {this.state.addingNewRuleset ? (
+ this.submitNewRuleset(r)}
+ />
+ ) : (
+
+ )}
+
+
+
+ >
);
}
}
diff --git a/src/Components/StatsPage.tsx b/src/Components/StatsPage.tsx
index 13cf236..3eae18a 100755
--- a/src/Components/StatsPage.tsx
+++ b/src/Components/StatsPage.tsx
@@ -1,10 +1,30 @@
import React, {ReactNode} from "react";
-import {Header} from "semantic-ui-react";
+import {
+ Container,
+ Dropdown, DropdownItemProps, DropdownMenuProps,
+ Grid,
+ GridColumn,
+ GridRow,
+ Header, Segment,
+} from "semantic-ui-react";
import UserContext from "../Contexts/UserContext";
+import {StatsTable} from "./StatsTable";
+import KadiStatsService from "../Services/KadiStatsService";
+import {ServiceError} from "../errors";
+import Loading from "./Loading";
+import KadiStatsServiceSingleton from "../Services/KadiStatsService";
+import {RulesetSchemaDto} from "../Services/RulesetSchemaDto";
+import {StatsDto} from "../Services/StatsDto";
interface StatsPageProps {}
interface StatsPageState {
+ error: boolean;
+ stats: any;
+ loadingStats: boolean;
+ rulesetChoices: DropdownItemProps[];
+ selectedRulesetId: string;
+ rulesetSchemas: Record;
}
class StatsPage extends React.Component {
@@ -12,15 +32,97 @@ class StatsPage extends React.Component {
super(props);
this.state = {
+ selectedRulesetId: "",
+ error: false,
+ rulesetChoices: [],
+ rulesetSchemas: {},
+ stats: {},
+ loadingStats: true,
};
}
+ componentDidMount(): void {
+ this.getStats();
+ }
+
+ async getStats(): Promise {
+ this.setState({loadingStats: true}, async () => {
+ try {
+ await this.loadStatsAndRulesetChoices();
+ }
+ catch (e) {
+ if (e instanceof ServiceError) {
+ this.setState({error: true});
+ console.log(e);
+ }
+ else {
+ throw e;
+ }
+ }
+ finally {
+ this.setState({loadingStats: false});
+ }
+ });
+ }
+
+ async loadStatsAndRulesetChoices(): Promise {
+ const stats = await KadiStatsService.getStats();
+ const rulesetIds = Object.keys(stats.pStats[0].stats.statsByRuleset);
+ const schemas: Record = {};
+ for (const rulesetId of rulesetIds) {
+ schemas[rulesetId] = await KadiStatsService.getRulesetById(rulesetId);
+ }
+ const rulesetChoices = rulesetIds.map(rulesetId =>
+ ({key: rulesetId, value: rulesetId, text: schemas[rulesetId].label}));
+ this.setState({stats, rulesetChoices, rulesetSchemas: schemas, selectedRulesetId: rulesetIds[0]});
+ }
+
+ handleError(error: any): void {
+ console.log(error);
+ }
+
+ changeRulesetSelection(rulesetId: string): void {
+ this.setState({selectedRulesetId: rulesetId});
+ }
+
render(): ReactNode {
const Locale = this.context.strings;
return (
-
- {Locale.statsPage.title}
-
+ <>
+
+
+
+
+ {Locale.statsPage.title}
+
+
+
+ {Locale.statsPage.pickRuleset + " "}
+ this.changeRulesetSelection(choice.value as string)}
+ />
+
+
+
+
+ {this.state.error ?
+ {Locale.general.databaseError}
:
+ this.state.loadingStats ?
+ : (
+
+ )
+ }
+
+
+
+ >
);
}
}
diff --git a/src/Components/StatsTable.tsx b/src/Components/StatsTable.tsx
new file mode 100644
index 0000000..4de3f79
--- /dev/null
+++ b/src/Components/StatsTable.tsx
@@ -0,0 +1,95 @@
+import {Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow} from "semantic-ui-react";
+import React, {useContext} from "react";
+import {AccountStatsDto, PlayerStatsDto, StatsDto} from "../Services/StatsDto";
+import UserContext from "../Contexts/UserContext";
+import KadiStatsServiceSingleton from "../Services/KadiStatsService";
+import {RulesetSchemaDto} from "../Services/RulesetSchemaDto";
+
+interface StatsTableProps {
+ data: StatsDto;
+ displayedRulesetSchema: RulesetSchemaDto;
+}
+
+export const StatsTable: React.FunctionComponent = (props) => {
+ const {data, displayedRulesetSchema} = props;
+ console.log(data, displayedRulesetSchema);
+ const rulesetId = displayedRulesetSchema.id;
+ const {strings: Locale} = useContext(UserContext);
+ const headerCellLocations: {blockId: string, cellId: string}[] = [];
+ for (const blockId in data.pStats[0].stats.statsByRuleset[rulesetId].blockStats) {
+ for (const cellId in data.pStats[0].stats.statsByRuleset[rulesetId].blockStats[blockId].cellStats) {
+ headerCellLocations.push({blockId, cellId});
+ }
+ }
+ const rows = data.pStats.map(statsEntry => (
+
+ ));
+ return (
+
+
+
+
+ Player
+
+ {headerCellLocations.map(loc => (
+ <>
+
+ {displayedRulesetSchema.blocks[loc.blockId].cells[loc.cellId].label}
+
+
+ {Locale.statsPage.struckCellHeader}
+
+ >
+ ))}
+
+
+
+ {rows}
+
+
+ );
+};
+
+interface StatsTablePlayerRowProps {
+ rowData: any;
+ displayedRuleset: string;
+ cellOrder: {blockId: string, cellId: string}[];
+}
+
+const StatsTablePlayerRow: React.FunctionComponent = (props) => {
+ const {rowData, displayedRuleset, cellOrder} = props;
+ const cellStatTableCells = [];
+ for (const location of cellOrder) {
+ const currentCellStats = rowData.stats.statsByRuleset[displayedRuleset].blockStats[location.blockId].cellStats[location.cellId];
+ const average = Math.round(currentCellStats.runningTotal / rowData.stats.gamesPlayed * 10) / 10;
+ cellStatTableCells.push(
+
+ );
+ }
+ return (
+
+
+ {rowData.nick}
+
+ {cellStatTableCells}
+
+ );
+};
+
+interface CellStatsTableCellProps {
+ average: number;
+ timesStruck: number;
+}
+
+const CellStatsTableCell: React.FunctionComponent = ({average, timesStruck}) => {
+ return (
+ <>
+
+ {average}
+
+
+ {timesStruck}
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/src/Services/KadiStatsService.ts b/src/Services/KadiStatsService.ts
new file mode 100644
index 0000000..780e561
--- /dev/null
+++ b/src/Services/KadiStatsService.ts
@@ -0,0 +1,62 @@
+import axios from "axios";
+import {SERVER_BASE_NAME} from "../index";
+import {StatsDto} from "./StatsDto";
+import {RulesetSchemaDto} from "./RulesetSchemaDto";
+
+const dummyRulesets = [
+ {"id":"DEFAULT_RULESET","label":"Standard Kadi Rules (en)","blocks":{"top":{"label":"Upper","hasBonus":true,"bonusScore":35,"bonusFor":63,"cells":{"aces":{"fieldType":"multiplierField","label":"Aces","multiplier":1,"maxMultiples":5},"twos":{"fieldType":"multiplierField","label":"Twos","multiplier":2,"maxMultiples":5},"threes":{"fieldType":"multiplierField","label":"Threes","multiplier":3,"maxMultiples":5},"fours":{"fieldType":"multiplierField","label":"Fours","multiplier":4,"maxMultiples":5},"fives":{"fieldType":"multiplierField","label":"Fives","multiplier":5,"maxMultiples":5},"sixes":{"fieldType":"multiplierField","label":"Sixes","multiplier":6,"maxMultiples":5}}},"bottom":{"label":"Lower","hasBonus":false,"cells":{"threeKind":{"fieldType":"numberField","label":"Three of a Kind"},"fourKind":{"fieldType":"numberField","label":"Four of a Kind"},"fullHouse":{"fieldType":"boolField","label":"Full House","score":25},"smlStraight":{"fieldType":"boolField","label":"Small Straight","score":30},"lgSraight":{"fieldType":"boolField","label":"Large Straight","score":40},"superkadi":{"fieldType":"superkadiField","label":"Super Kadis","score":50,"maxSuperkadis":5},"chance":{"fieldType":"numberField","label":"Chance"}}}}}
+];
+
+class KadiStatsService {
+ private userStats: StatsDto | null = null;
+ private rulesets: Record = {};
+ private allRulesetsLoaded: boolean = false;
+ constructor() {}
+
+ private async loadStats(): Promise {
+ const statsSlug = await axios.get(SERVER_BASE_NAME + "/api/stats");
+ this.userStats = statsSlug.data as StatsDto;
+ return this.userStats;
+ }
+
+ async getStats(): Promise {
+ if (this.userStats) {
+ return this.userStats;
+ }
+ else {
+ return this.loadStats();
+ }
+ }
+
+ async getRulesetById(id: string): Promise {
+ if (this.rulesets[id]) {
+ return this.rulesets[id];
+ }
+ else {
+ const rulesetSchema = (await axios.get(SERVER_BASE_NAME + "/api/ruleset/" + id)).data as RulesetSchemaDto;
+ this.rulesets[rulesetSchema.id] = rulesetSchema;
+ return this.rulesets[rulesetSchema.id];
+ }
+ }
+
+ async refreshStats(): Promise {
+ await this.loadStats();
+ }
+
+ async getAllRulesets(): Promise {
+ return dummyRulesets as RulesetSchemaDto[];
+ if (this.allRulesetsLoaded) {
+ return Object.values(this.rulesets);
+ }
+ else {
+ const rulesetSchemas = (await axios.get(SERVER_BASE_NAME + "/api/rulesets/")).data as RulesetSchemaDto[];
+ rulesetSchemas.forEach(schema => this.rulesets[schema.id] = schema);
+ this.allRulesetsLoaded = true;
+ return rulesetSchemas;
+ }
+ }
+}
+
+const KadiStatsServiceSingleton = new KadiStatsService();
+
+export default KadiStatsServiceSingleton;
\ No newline at end of file
diff --git a/src/Services/RulesetSchemaDto.ts b/src/Services/RulesetSchemaDto.ts
new file mode 100644
index 0000000..f388a77
--- /dev/null
+++ b/src/Services/RulesetSchemaDto.ts
@@ -0,0 +1,53 @@
+export interface RulesetSchemaDto {
+ id: string;
+ label: string;
+ blocks: Record;
+}
+
+export type BlockDef = BonusBlockDef | NoBonusBlockDef;
+
+export interface NoBonusBlockDef extends DefaultBlockDef {
+ hasBonus: false;
+}
+
+export interface BonusBlockDef extends DefaultBlockDef {
+ hasBonus: true;
+ bonusScore: number;
+ bonusFor: number;
+}
+
+interface DefaultBlockDef {
+ label: string;
+ cells: Record;
+}
+
+export type CellDef =
+ | BoolCellDef
+ | MultiplierCellDef
+ | NumberCellDef
+ | SuperkadiCellDef;
+
+export interface BoolCellDef extends DefaultCellDef {
+ fieldType: FieldType.bool;
+ score: number;
+}
+
+export interface MultiplierCellDef extends DefaultCellDef {
+ fieldType: FieldType.multiplier;
+ multiplier: number;
+ maxMultiples: number;
+}
+
+export interface SuperkadiCellDef extends DefaultCellDef {
+ fieldType: FieldType.superkadi;
+ score: number;
+ maxSuperkadis: number;
+}
+
+export interface NumberCellDef extends DefaultCellDef {
+ fieldType: FieldType.number;
+}
+
+interface DefaultCellDef {
+ label: string;
+}
\ No newline at end of file
diff --git a/src/Services/Service.ts b/src/Services/Service.ts
new file mode 100644
index 0000000..9045290
--- /dev/null
+++ b/src/Services/Service.ts
@@ -0,0 +1,22 @@
+import {ServiceError} from "../errors";
+
+type ClassDef = { new (...args: any[]): {} };
+
+export function Service(constructor: T): T {
+ for (const property of Object.getOwnPropertyNames(constructor.prototype)) {
+ if (typeof constructor.prototype[property] === "function") {
+ const originalFunction = constructor.prototype[property];
+ constructor.prototype[property] = async (...args: any[]) => {
+ try {
+ // @ts-ignore
+ const result = await originalFunction.apply(this, ...args);
+ return result;
+ }
+ catch (e) {
+ throw new ServiceError(e.message);
+ }
+ };
+ }
+ }
+ return constructor;
+}
\ No newline at end of file
diff --git a/src/Services/StatsDto.ts b/src/Services/StatsDto.ts
new file mode 100644
index 0000000..299030d
--- /dev/null
+++ b/src/Services/StatsDto.ts
@@ -0,0 +1,43 @@
+export interface StatsDto {
+ pStats: {
+ nick: string,
+ playerId: string,
+ stats: PlayerStatsDto
+ }[];
+ accStats: AccountStatsDto;
+}
+export interface PlayerStatsDto extends BaseStatsDto {}
+export interface AccountStatsDto extends BaseStatsDto {}
+export interface BaseStatsDto {
+ statsByRuleset: Record
+ gamesPlayed: number;
+}
+export interface RulesetStatsDto {
+ blockStats: Record;
+ wins: number;
+ runnerUps: number;
+ draws: number;
+ losses: number;
+ grandTotal: TotalFieldStatsDto;
+}
+export interface BlockStatsDto {
+ cellStats: Record;
+ timesHadBonus?: number;
+ total: TotalFieldStatsDto;
+}
+export interface BaseCellStatsDto {
+ runningTotal: number;
+}
+export interface StrikeableFieldStatsDto extends BaseCellStatsDto {
+ timesStruck: number;
+}
+export interface BestableFieldStatsDto extends BaseCellStatsDto {
+ best: number;
+ worst: number;
+}
+export type TotalFieldStatsDto = BestableFieldStatsDto;
+export type BoolFieldStatsDto = StrikeableFieldStatsDto & { total: number };
+export type NumberFieldStatsDto = StrikeableFieldStatsDto & BestableFieldStatsDto;
+export type MultiplierFieldStatsDto = NumberFieldStatsDto;
+export type SuperkadiFieldStatsDto = NumberFieldStatsDto;
+export type CellStatsDto = BoolFieldStatsDto | NumberFieldStatsDto | MultiplierFieldStatsDto | SuperkadiFieldStatsDto;
\ No newline at end of file
diff --git a/src/enums.ts b/src/enums.ts
index da5b527..c32cae1 100755
--- a/src/enums.ts
+++ b/src/enums.ts
@@ -17,4 +17,15 @@ export const supportedLangToIntlDTF: Record
gb: Intl.DateTimeFormat('en-AU'),
de: Intl.DateTimeFormat('de-DE'),
it: Intl.DateTimeFormat('it-IT'),
-};
\ No newline at end of file
+};
+
+export enum FieldType {
+ number = "numberField",
+ bool = "boolField",
+ bonus = "bonusField",
+ subtotal = "subtotalField",
+ globalTotal = "globalTotalField",
+ total = "totalField",
+ superkadi = "superkadiField",
+ multiplier = "multiplierField",
+}
\ No newline at end of file
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..7da56e9
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,5 @@
+export class ServiceError extends Error {
+ constructor(message?: string) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index f5f0feb..14b70a1 100755
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,9 +2,9 @@ import React from "react";
import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import App from "./App";
-import * as serviceWorker from "./serviceWorker";
export {homepage as SERVER_BASE_NAME} from "../package.json";
+
ReactDOM.render((
@@ -13,7 +13,3 @@ ReactDOM.render((
document.getElementById('root')
);
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.unregister();
diff --git a/src/serviceWorker.js b/src/serviceWorker.js
deleted file mode 100755
index b04b771..0000000
--- a/src/serviceWorker.js
+++ /dev/null
@@ -1,141 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.0/8 are considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { 'Service-Worker': 'script' },
- })
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready
- .then(registration => {
- registration.unregister();
- })
- .catch(error => {
- console.error(error.message);
- });
- }
-}
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100755
index 74b1a27..0000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect';
diff --git a/src/static/strings.ts b/src/static/strings.ts
index 0ac0e75..f2adcdc 100755
--- a/src/static/strings.ts
+++ b/src/static/strings.ts
@@ -30,6 +30,10 @@ export const IntlStrings = {
gb: {
general: {
deleteCommand: "Delete",
+ databaseError: "An error occurred communicating with the database.",
+ yes: "Yes",
+ no: "No",
+ nA: "N/A",
},
menu: {
profileTab: "Profile",
@@ -52,9 +56,37 @@ export const IntlStrings = {
},
statsPage: {
title: "Stats",
+ struckCellHeader: "X",
+ pickRuleset: "Filter by ruleset",
},
rulesetsPage: {
title: "Rulesets",
+ myRulesets: "My Rulesets",
+ newRuleset: "New Ruleset",
+ blocksHeader: "Blocks",
+ fieldLabelHeader: "Label",
+ fieldTypeHeader: "Type",
+ fieldValueHeader: "Value",
+ superkadiField: "Superkadi",
+ multiplierField: "Multiplier Cell",
+ boolField: "Boolean Cell",
+ numberField: "Number Cell",
+ bonus: "Bonus",
+ newBlock: "New Block",
+ blockName: "Name",
+ cellName: "Name",
+ bonusScore: "Score for Bonus",
+ bonusThreshold: "Score Required",
+ blockNamePlaceholder: "My New Block",
+ addBlock: "Add Block",
+ noBlocks: "No Blocks",
+ addCell: "Add Cell",
+ cellNamePlaceholder: "My New Cell",
+ fieldTypePlaceholder: "Select a field type",
+ multiplierPlaceholder: "Multiplier",
+ maxMultiplesPlaceholder: "Max of kind",
+ valuePlaceholder: "Value",
+ submit: "Submit",
},
friendsPage: {
title: "Friends",
@@ -67,6 +99,10 @@ export const IntlStrings = {
de: {
general: {
deleteCommand: "Löschen",
+ databaseError: "Ein Fehler ist während der Datenbankabfrage aufgetreten.",
+ yes: "Ja",
+ no: "Nein",
+ nA: "k.A.",
},
menu: {
profileTab: "Profil",
@@ -89,9 +125,38 @@ export const IntlStrings = {
},
statsPage: {
title: "Statistiken",
+ struckCellHeader: "X",
+ pickRuleset: "Nach Regelwerk filtern:",
},
rulesetsPage: {
title: "Regelwerke",
+ myRulesets: "Meine Regelwerke",
+ newRuleset: "Neues Regelwerk",
+ blocksHeader: "Blöcke",
+ fieldLabelHeader: "Name",
+ fieldTypeHeader: "Typ",
+ fieldValueHeader: "Wert",
+ superkadiField: "Superkadi",
+ multiplierField: "Multiplikator-Feld",
+ boolField: "Bool'sches Feld",
+ numberField: "Zahleingabefeld",
+ value: "Wert",
+ bonus: "Bonus",
+ newBlock: "Neuer Block",
+ blockName: "Name",
+ cellName: "Name",
+ bonusScore: "Bonuspunkte",
+ bonusThreshold: "Punkte zu erreichen",
+ blockNamePlaceholder: "Mein neuer Block",
+ addBlock: "Block hinzufügen",
+ noBlocks: "Keine Blöcke vorhanden",
+ addCell: "Feld hinzufügen",
+ cellNamePlaceholder: "Mein neues Feld",
+ fieldTypePlaceholder: "Feldtyp auswählen",
+ multiplierPlaceholder: "Multiplikator",
+ maxMultiplesPlaceholder: "Max. Anzahl",
+ valuePlaceholder: "Wert",
+ submit: "Fertig",
},
friendsPage: {
title: "Freunde",
@@ -104,6 +169,10 @@ export const IntlStrings = {
it: {
general: {
deleteCommand: "Cancella",
+ databaseError: "===TRANSLATE ME===",
+ yes: "Sì",
+ no: "No",
+ nA: "===TRANSLATE ME===",
},
menu: {
profileTab: "Profilo",
@@ -126,9 +195,30 @@ export const IntlStrings = {
},
statsPage: {
title: "Statistiche",
+ struckCellHeader: "X",
+ pickRuleset: "===TRANSLATE ME===",
},
rulesetsPage: {
title: "Regolamenti",
+ myRulesets: "I miei Regolamenti",
+ newRuleset: "===TRANSLATE ME===",
+ blocksHeader: "===TRANSLATE ME===",
+ fieldLabelHeader: "===TRANSLATE ME===",
+ fieldTypeHeader: "===TRANSLATE ME===",
+ fieldValueHeader: "===TRANSLATE ME===",
+ superkadiField: "===TRANSLATE ME===",
+ multiplierField: "===TRANSLATE ME===",
+ boolField: "===TRANSLATE ME===",
+ numberField: "===TRANSLATE ME===",
+ value: "Valore",
+ bonus: "Bonus",
+ newBlock: "===TRANSLATE ME===",
+ blockName: "===TRANSLATE ME===",
+ bonusScore: "===TRANSLATE ME===",
+ bonusThreshold: "===TRANSLATE ME===",
+ blockNamePlaceholder: "===TRANSLATE ME===",
+ addBlock: "===TRANSLATE ME===",
+ noBlocks: "===TRANSLATE ME===",
},
friendsPage: {
title: "Amici",
diff --git a/tsconfig.json b/tsconfig.json
index cbace80..c207b85 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,8 @@
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
- "isolatedModules": true
+ "isolatedModules": true,
+ "experimentalDecorators": true
},
"lib": [
"dom",
diff --git a/tslint.json b/tslint.json
index 06739e6..902e445 100755
--- a/tslint.json
+++ b/tslint.json
@@ -13,7 +13,6 @@
"prefer-readonly": true,
"typedef": [
true,
- "call-signature",
"property-declaration"
],
"ordered-imports": false,
diff --git a/webpack.config.js b/webpack.config.js
index 69dfa0c..33c317e 100755
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -42,6 +42,9 @@ module.exports = {
},
devServer: {
contentBase: path.join(__dirname, "public/"),
+ historyApiFallback: {
+ index: '/kadi/',
+ },
contentBasePublicPath: SERVER_ROOT + "/",
port: 3000,
publicPath: "http://localhost:3000" + SERVER_ROOT + "/static/frontend/",