Big update

This commit is contained in:
Daniel Ledda
2020-08-13 15:10:36 +02:00
parent 901cb04955
commit 602f9fd3f5
31 changed files with 1382 additions and 231 deletions

View File

@@ -4,5 +4,9 @@
"@babel/env",
"@babel/preset-react"
],
"plugins": ["@babel/plugin-proposal-class-properties"]
"plugins": [
"@babel/plugin-transform-runtime",
["@babel/plugin-proposal-decorators", {"legacy": true}],
["@babel/plugin-proposal-class-properties", {"loose": true}]
]
}

264
package-lock.json generated
View File

@@ -571,6 +571,201 @@
"@babel/helper-plugin-utils": "^7.8.3"
}
},
"@babel/plugin-proposal-decorators": {
"version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz",
"integrity": "sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ==",
"dev": true,
"requires": {
"@babel/helper-create-class-features-plugin": "^7.10.5",
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-decorators": "^7.10.4"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"dev": true,
"requires": {
"@babel/highlight": "^7.10.4"
}
},
"@babel/generator": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz",
"integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==",
"dev": true,
"requires": {
"@babel/types": "^7.11.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz",
"integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.10.4",
"@babel/helper-member-expression-to-functions": "^7.10.5",
"@babel/helper-optimise-call-expression": "^7.10.4",
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/helper-replace-supers": "^7.10.4",
"@babel/helper-split-export-declaration": "^7.10.4"
}
},
"@babel/helper-function-name": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
"integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.10.4",
"@babel/template": "^7.10.4",
"@babel/types": "^7.10.4"
}
},
"@babel/helper-get-function-arity": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz",
"integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz",
"integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==",
"dev": true,
"requires": {
"@babel/types": "^7.11.0"
}
},
"@babel/helper-optimise-call-expression": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz",
"integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
},
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
},
"@babel/helper-replace-supers": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz",
"integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.10.4",
"@babel/helper-optimise-call-expression": "^7.10.4",
"@babel/traverse": "^7.10.4",
"@babel/types": "^7.10.4"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz",
"integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==",
"dev": true,
"requires": {
"@babel/types": "^7.11.0"
}
},
"@babel/helper-validator-identifier": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
"dev": true
},
"@babel/highlight": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.11.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz",
"integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==",
"dev": true
},
"@babel/template": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
"integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/parser": "^7.10.4",
"@babel/types": "^7.10.4"
}
},
"@babel/traverse": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz",
"integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.0",
"@babel/helper-function-name": "^7.10.4",
"@babel/helper-split-export-declaration": "^7.11.0",
"@babel/parser": "^7.11.0",
"@babel/types": "^7.11.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
}
},
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"@babel/plugin-proposal-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz",
@@ -661,6 +856,23 @@
"@babel/helper-plugin-utils": "^7.8.0"
}
},
"@babel/plugin-syntax-decorators": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz",
"integrity": "sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.10.4"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
}
}
},
"@babel/plugin-syntax-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
@@ -1052,6 +1264,58 @@
"@babel/helper-plugin-utils": "^7.8.3"
}
},
"@babel/plugin-transform-runtime": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz",
"integrity": "sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.10.4",
"@babel/helper-plugin-utils": "^7.10.4",
"resolve": "^1.8.1",
"semver": "^5.5.1"
},
"dependencies": {
"@babel/helper-module-imports": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz",
"integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==",
"dev": true,
"requires": {
"@babel/types": "^7.10.4"
}
},
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
"dev": true
},
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
}
}
},
"@babel/plugin-transform-shorthand-properties": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz",

View File

@@ -8,7 +8,7 @@
"scripts": {
"build-dev": "webpack --mode development && npm postbuild",
"build": "webpack --mode production",
"postbuild": "rsync -avu --delete dist/ ../kadi_backend/static/frontend",
"postbuild": "rsync -avu --delete dist/ ../backend/static/frontend",
"start": "webpack-dev-server --mode development",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -16,6 +16,8 @@
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0",

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,5 +1,5 @@
import React, {ReactNode} from "react";
import {BrowserRouter as Router, Route} from "react-router-dom";
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import {Redirect} from "react-router";
import {IntlStrings} from "./static/strings";
import {PageId, SupportedLang, supportedLangToIntlDTF} from "./enums";
@@ -9,6 +9,8 @@ import HomePage from "./Components/HomePage";
import {SERVER_BASE_NAME} from "./index";
import axios from "axios";
import UserContext, {IUserContext} from "./Contexts/UserContext";
import KadiPageRoute from "./Components/KadiPageRoute";
import KadiStatsService from "./Services/KadiStatsService";
interface AppState {
userContext: IUserContext;
@@ -24,25 +26,18 @@ class App extends React.Component<AppProps, AppState> {
this.updateUserContext = (username, loggedIn) => {
this.setState({userContext: {
...this.state.userContext,
username: username,
loggedIn: loggedIn,
updateUserContext: this.updateUserContext,
dateTimeFormatter: this.state.userContext.dateTimeFormatter,
currentLang: this.state.userContext.currentLang,
strings: this.state.userContext.strings,
changeLang: this.state.userContext.changeLang,
}});
};
this.changeLang = (lang: SupportedLang, submit=true) => {
this.setState({userContext: {
...this.state.userContext,
dateTimeFormatter: supportedLangToIntlDTF[lang],
strings: IntlStrings[lang],
currentLang: lang,
changeLang: this.changeLang,
username: this.state.userContext.username,
loggedIn: this.state.userContext.loggedIn,
updateUserContext: this.state.userContext.updateUserContext,
}});
if (submit) {
this.submitLanguagePreference(lang);
@@ -78,7 +73,7 @@ class App extends React.Component<AppProps, AppState> {
.catch(err => console.log(err));
}
submitLanguagePreference(lang: SupportedLang) {
submitLanguagePreference(lang: SupportedLang): void {
axios.put(SERVER_BASE_NAME + "/api/lang",
{lang: lang},
{headers: {"Content-Type": "application/json"}}
@@ -100,34 +95,12 @@ class App extends React.Component<AppProps, AppState> {
<KadiPageRoute pageId={PageId.stats}/>
<KadiPageRoute pageId={PageId.profile}/>
<KadiPageRoute pageId={PageId.rulesets}/>
<Route path={"/"}>
<Redirect
to={{
pathname: "/",
}}
/>
</Route>
<Redirect strict={true} from="/stats/" to="/stats" />
<Redirect to={{pathname: "/"}} />
</Router>
</UserContext.Provider>
);
}
}
interface KadiPageRouteProps {
pageId: PageId
}
const KadiPageRoute: React.FunctionComponent<KadiPageRouteProps> = (props: KadiPageRouteProps) => {
const {pageId} = props;
const PageComponent = pageComponentFromId[pageId];
return (
<Route path={"/" + pageId}>
<KadiPage activePage={pageId}>
<PageComponent/>
</KadiPage>
</Route>
);
};
export default App;

View File

@@ -0,0 +1,138 @@
import React, {useContext, useState} from "react";
import {CellDef, RulesetSchemaDto} from "../Services/RulesetSchemaDto";
import {
Button,
Checkbox,
Form,
Header,
Segment,
Table
} from "semantic-ui-react";
import UserContext from "../Contexts/UserContext";
import RulesetBlockTable from "./RulesetBlockTable";
import RulesetDisplayPanel from "./RulesetDisplayPanel";
interface RulesetDisplayPanelProps {
onSubmitRuleset: (ruleset: RulesetSchemaDto) => any;
}
const CreateRulesetPanel: React.FunctionComponent<RulesetDisplayPanelProps> = (props) => {
const {onSubmitRuleset} = props;
const {strings: Locale} = useContext(UserContext);
const [currentRulesetBuild, updateCurrentRulesetBuild] = useState<RulesetSchemaDto>({
id: "",
label: Locale.rulesetsPage.newRuleset,
blocks: {},
});
const [newBlockInput, updateNewBlockInput] = useState({
label: "",
hasBonus: false,
bonusScore: 35,
bonusFor: 63,
cells: {},
});
const handleAddBlock = () => {
if (newBlockInput.label) {
updateCurrentRulesetBuild({
...currentRulesetBuild,
blocks: {
...currentRulesetBuild.blocks,
[newBlockInput.label]: {
...newBlockInput,
bonusFor: newBlockInput.hasBonus ? newBlockInput.bonusFor : undefined,
bonusScore: newBlockInput.hasBonus ? newBlockInput.bonusScore : undefined,
},
}
});
}
};
const handleAddCell = (cellDef: CellDef, blockId: string) => {
if (cellDef.label) {
updateCurrentRulesetBuild({
...currentRulesetBuild,
blocks: {
...currentRulesetBuild.blocks,
[blockId]: {
...currentRulesetBuild.blocks[blockId],
cells: {
...currentRulesetBuild.blocks[blockId].cells,
[cellDef.label]: cellDef
}
}
}
})
}
};
return (
<>
<Header size={"tiny"}>
{Locale.rulesetsPage.newRuleset}
</Header>
<RulesetDisplayPanel
ruleset={currentRulesetBuild}
editable={true}
loading={false}
onAddCell={handleAddCell}
/>
<Header size={"tiny"}>
{Locale.rulesetsPage.newBlock}
</Header>
<Segment>
<Form>
<Form.Field>
<label>{Locale.rulesetsPage.blockName + ": "}</label>
<input
placeholder={Locale.rulesetsPage.blockNamePlaceholder}
onChange={(e) => updateNewBlockInput({...newBlockInput, label: e.target.value})}
/>
</Form.Field>
<Form.Field>
<Checkbox
toggle={true}
onChange={(e, c) => updateNewBlockInput({...newBlockInput, hasBonus: c.checked ?? false})}
label={Locale.rulesetsPage.bonus}
/>
</Form.Field>
{newBlockInput.hasBonus && (
<>
<Form.Field width={4}>
<label>{Locale.rulesetsPage.bonusScore + ": "}</label>
<input
type={"number"}
value={newBlockInput.bonusScore}
onChange={(e) => updateNewBlockInput({...newBlockInput, bonusScore: Number(e.target.value)})}
/>
</Form.Field>
<Form.Field width={4}>
<label>{Locale.rulesetsPage.bonusThreshold + ": "}</label>
<input
type={"number"}
value={newBlockInput.bonusFor}
onChange={(e) => updateNewBlockInput({...newBlockInput, bonusFor: Number(e.target.value)})}
/>
</Form.Field>
</>
)}
<Form.Field>
<Button
onClick={handleAddBlock}
>
{Locale.rulesetsPage.addBlock}
</Button>
</Form.Field>
</Form>
</Segment>
<Button
fluid={true}
onClick={() => onSubmitRuleset(currentRulesetBuild)}
>
{Locale.rulesetsPage.submit}
</Button>
</>
);
};
export default CreateRulesetPanel;

View File

@@ -10,10 +10,11 @@ interface GamesListProps {
const GamesList: React.FunctionComponent<GamesListProps> = (props) => {
const {loading, gamesList} = props;
const Uctx = React.useContext(UserContext);
const listItems = gamesList.map(listing =>
<ListItem key={listing.createdAt}>
Game played on: {Uctx.dateTimeFormatter.format(new Date(listing.createdAt))}
const listItems = gamesList.map(listing => (
<ListItem key={listing.id}>
Game: {JSON.stringify(listing)}
</ListItem>
)
);
return (
<>

View File

@@ -2,8 +2,6 @@ import {Header, List, ListItem} from "semantic-ui-react";
import React from "react";
import UserContext from "../Contexts/UserContext";
import {Guest} from "./ProfilePage";
import HeaderSubHeader from "semantic-ui-react/dist/commonjs/elements/Header/HeaderSubheader";
import {SERVER_BASE_NAME} from "../index";
interface GuestListProps {
@@ -15,17 +13,17 @@ interface GuestListProps {
const GuestList: React.FunctionComponent<GuestListProps> = (props) => {
const {loading, guestList, deleteGuest} = props;
const Uctx = React.useContext(UserContext);
const listItems = guestList.map(guest =>
const listItems = guestList.map(guest => (
<ListItem key={guest.id}>
{guest.nick} - <a onClick={() => deleteGuest(guest.id)}>{Uctx.strings.general.deleteCommand}</a>
</ListItem>
);
));
return (
<>
<Header size={"medium"}>
{Uctx.strings.profilePage.guestsHeader}
</Header>
{loading ? (
{loading && guestList.length === 0 ? (
<p>{Uctx.strings.profilePage.loadingGuests}</p>
) : (
<List bulleted={true}>

View File

@@ -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<KadiPageRouteProps> = (props: KadiPageRouteProps) => {
const {pageId} = props;
const {path} = useRouteMatch();
const PageComponent = pageComponentFromId[pageId];
return (
<Route path={`${path}${pageId}`}>
<KadiPage activePage={pageId}>
<PageComponent/>
</KadiPage>
</Route>
);
};
export default KadiPageRoute;

View File

@@ -0,0 +1,14 @@
import React from "react";
import {Dimmer, Loader} from "semantic-ui-react";
interface LoadingProps {}
const Loading: React.FunctionComponent<LoadingProps> = (props) => {
return (
<Dimmer inverted={true} active={true} style={{height: "30vh"}}>
<Loader>Loading</Loader>
</Dimmer>
);
};
export default Loading;

View File

@@ -18,9 +18,7 @@ const KadiPageMainContent: React.FunctionComponent<KadiPageMainContentProps> = (
<Container
className={"mainPageContentContainer"}
>
<Segment>
{children}
</Segment>
</Container>
</div>
);

View File

@@ -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<RulesetBlockTableProps> = (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 (
<Table
attached={true}
fixed={true}
celled={true}
>
<Table.Header>
<Table.Row>
<Table.HeaderCell
colSpan={"3"}
textAlign={"center"}
>
{Locale.rulesetsPage.noBlocks}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
</Table>
);
}
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 (
<Table
attached={true}
fixed={true}
celled={true}
>
<Table.Header>
<Table.Row>
<Table.HeaderCell
colSpan={"3"}
textAlign={"center"}
>
{blockDef.label}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{idAndCellList.map(idAndCell => (
<RulesetCellTableRow
key={idAndCell[0]}
id={idAndCell[0]}
cellDef={idAndCell[1]}
/>
))}
{editable && (
<Table.Row>
<Table.Cell
style={{overflow: "visible"}}
colSpan={"3"}
>
<Grid>
<Grid.Row
columns={3}
verticalAlign={"middle"}
>
<Grid.Column width={4}>
<Input
fluid={true}
value={currentCellInput.label}
label={Locale.rulesetsPage.cellName}
placeholder={Locale.rulesetsPage.cellNamePlaceholder}
onChange={(e) => updateCurrentCellInput({...currentCellInput, label: e.target.value})}
/>
</Grid.Column>
<Grid.Column width={4}>
<Dropdown
selectedLabel={currentCellInput.fieldType}
placeholder={Locale.rulesetsPage.fieldTypePlaceholder}
onChange={(e, c) => onChangeDropdown(c.value as FieldType)}
options={fieldTypeOptions}
button={true}
/>
</Grid.Column>
<Grid.Column width={4}>
{currentCellInput.fieldType === FieldType.multiplier && (
<Input
label={Locale.rulesetsPage.multiplierPlaceholder}
fluid={true}
type={"number"}
value={currentCellInput.multiplier}
onChange={(e) => updateCurrentCellInput(
{...currentCellInput, multiplier: Number(e.target.value)})}
/>
)}
{(currentCellInput.fieldType === FieldType.superkadi
|| currentCellInput.fieldType === FieldType.multiplier) && (
<Input
label={Locale.rulesetsPage.maxMultiplesPlaceholder}
fluid={true}
type={"number"}
value={currentCellInput.maxMultiples}
onChange={(e) => updateCurrentCellInput({
...currentCellInput,
maxMultiples: Number(e.target.value),
})}
/>
)}
{(currentCellInput.fieldType === FieldType.bool
|| currentCellInput.fieldType === FieldType.superkadi
|| currentCellInput.fieldType === FieldType.number) && (
<Input
label={Locale.rulesetsPage.valuePlaceholder}
fluid={true}
type={"number"}
value={currentCellInput.score}
onChange={(e) => updateCurrentCellInput(
{...currentCellInput, score: Number(e.target.value)})}
/>
)}
</Grid.Column>
<Grid.Column
width={4}
textAlign={"right"}
>
<Button
onClick={() => {
if (onAddCell) {
onAddCell(currentCellInput, id)
}
}}
>
{Locale.rulesetsPage.addCell}
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</Table.Cell>
</Table.Row>
)}
<Table.Row>
<Table.Cell
colSpan={"3"}
textAlign={"center"}
>
{subText}
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
);
};
export default RulesetBlockTable;

View File

@@ -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<TableCellRowProps> = (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 (
<Table.Row>
<Table.Cell key={id}>
{cellDef.label}
</Table.Cell>
<Table.Cell>
{Locale.rulesetsPage[cellDef.fieldType]}
</Table.Cell>
<Table.Cell disabled={displayValue === Locale.general.nA}>
{displayValue}
</Table.Cell>
</Table.Row>
);
};
export default RulesetCellTableRow;

View File

@@ -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<RulesetDisplayPanelProps> = (props) => {
const {ruleset, loading, editable, onAddCell} = props;
const {strings: Locale} = useContext(UserContext);
if (loading) {
return <Loading/>;
}
else {
return (
<>
<Table attached={true} celled={true}>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
{Locale.rulesetsPage.fieldLabelHeader}
</Table.HeaderCell>
<Table.HeaderCell>
{Locale.rulesetsPage.fieldTypeHeader}
</Table.HeaderCell>
<Table.HeaderCell>
{Locale.rulesetsPage.fieldValueHeader}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
</Table>
{Object.entries(ruleset.blocks).length > 0 ?
Object.entries(ruleset.blocks).map(idAndBlock => (
<RulesetBlockTable
key={idAndBlock[0]}
id={idAndBlock[0]}
blockDef={idAndBlock[1]}
editable={editable}
onAddCell={onAddCell}
/>
)) : (
<RulesetBlockTable
id={"noBlocks"}
blockDef={null}
/>
)}
</>
);
}
};
export default RulesetDisplayPanel;

View File

@@ -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<RulesetListProps> = (props) => {
const {rulesetNames, onItemChange, selectedItemIndex, creatingRuleset} = props;
const {strings: Locale} = useContext(UserContext);
const selectedItem = creatingRuleset ? "" : rulesetNames[selectedItemIndex];
return (
<Menu pointing={true} vertical={true}>
{rulesetNames.map(name => (
<Menu.Item
key={name}
name={name}
active={selectedItem === name}
onClick={() => onItemChange(name)}
/>
))}
<Menu.Item
active={creatingRuleset}
onClick={() => onItemChange("addNewRuleset")}
>
{Locale.rulesetsPage.newRuleset}
<Icon name={"add"} />
</Menu.Item>
</Menu>
);
};
export default RulesetList;

View File

@@ -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<RulesetsPageProps, RulesetsPageState> {
@@ -12,15 +21,70 @@ class RulesetsPage extends React.Component<RulesetsPageProps, RulesetsPageState>
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 (
<Header>
<>
<Header size={"huge"}>
{Locale.rulesetsPage.title}
</Header>
<Grid>
<Grid.Row>
<Grid.Column width={4}>
<RulesetList
onItemChange={(r) => this.onRulesetSelect(r)}
selectedItemIndex={this.state.selectedRulesetIndex}
creatingRuleset={this.state.addingNewRuleset}
rulesetNames={this.state.rulesets.map(ruleset => ruleset.label)}
/>
</Grid.Column>
<Grid.Column width={12}>
{this.state.addingNewRuleset ? (
<CreateRulesetPanel
onSubmitRuleset={(r) => this.submitNewRuleset(r)}
/>
) : (
<RulesetDisplayPanel
loading={this.state.loading}
ruleset={this.state.rulesets[this.state.selectedRulesetIndex]}
/>
)}
</Grid.Column>
</Grid.Row>
</Grid>
</>
);
}
}

View File

@@ -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<string, RulesetSchemaDto>;
}
class StatsPage extends React.Component<StatsPageProps, StatsPageState> {
@@ -12,15 +32,97 @@ class StatsPage extends React.Component<StatsPageProps, StatsPageState> {
super(props);
this.state = {
selectedRulesetId: "",
error: false,
rulesetChoices: [],
rulesetSchemas: {},
stats: {},
loadingStats: true,
};
}
componentDidMount(): void {
this.getStats();
}
async getStats(): Promise<void> {
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<void> {
const stats = await KadiStatsService.getStats();
const rulesetIds = Object.keys(stats.pStats[0].stats.statsByRuleset);
const schemas: Record<string, RulesetSchemaDto> = {};
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 (
<Header>
<>
<Grid>
<GridRow columns={2}>
<GridColumn>
<Header size={"huge"}>
{Locale.statsPage.title}
</Header>
</GridColumn>
<GridColumn textAlign={"right"}>
{Locale.statsPage.pickRuleset + " "}
<Dropdown
loading={this.state.loadingStats}
selection={true}
options={this.state.rulesetChoices}
defaultValue={this.state.rulesetChoices[0]?.value}
onChange={(e, choice) => this.changeRulesetSelection(choice.value as string)}
/>
</GridColumn>
</GridRow>
<GridRow columns={1} style={{overflow: "auto"}}>
<GridColumn>
{this.state.error ?
<p>{Locale.general.databaseError}</p> :
this.state.loadingStats ?
<Loading/> : (
<StatsTable
data={this.state.stats}
displayedRulesetSchema={this.state.rulesetSchemas[this.state.selectedRulesetId]}
/>
)
}
</GridColumn>
</GridRow>
</Grid>
</>
);
}
}

View File

@@ -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<StatsTableProps> = (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 => (
<StatsTablePlayerRow key={statsEntry.playerId} rowData={statsEntry} displayedRuleset={rulesetId} cellOrder={headerCellLocations}/>
));
return (
<Table celled={true}>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
Player
</Table.HeaderCell>
{headerCellLocations.map(loc => (
<>
<Table.HeaderCell key={"h" + loc.blockId + loc.cellId}>
{displayedRulesetSchema.blocks[loc.blockId].cells[loc.cellId].label}
</Table.HeaderCell>
<Table.HeaderCell textAlign={"center"} error={true} key={"h" + loc.blockId + loc.cellId + "X"}>
{Locale.statsPage.struckCellHeader}
</Table.HeaderCell>
</>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{rows}
</Table.Body>
</Table>
);
};
interface StatsTablePlayerRowProps {
rowData: any;
displayedRuleset: string;
cellOrder: {blockId: string, cellId: string}[];
}
const StatsTablePlayerRow: React.FunctionComponent<StatsTablePlayerRowProps> = (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(
<CellStatsTableCell key={location.blockId + location.cellId} average={average} timesStruck={currentCellStats.timesStruck}/>
);
}
return (
<TableRow>
<TableCell>
{rowData.nick}
</TableCell>
{cellStatTableCells}
</TableRow>
);
};
interface CellStatsTableCellProps {
average: number;
timesStruck: number;
}
const CellStatsTableCell: React.FunctionComponent<CellStatsTableCellProps> = ({average, timesStruck}) => {
return (
<>
<TableCell>
{average}
</TableCell>
<TableCell negative={true}>
{timesStruck}
</TableCell>
</>
);
};

View File

@@ -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<string, RulesetSchemaDto> = {};
private allRulesetsLoaded: boolean = false;
constructor() {}
private async loadStats(): Promise<StatsDto> {
const statsSlug = await axios.get(SERVER_BASE_NAME + "/api/stats");
this.userStats = statsSlug.data as StatsDto;
return this.userStats;
}
async getStats(): Promise<StatsDto> {
if (this.userStats) {
return this.userStats;
}
else {
return this.loadStats();
}
}
async getRulesetById(id: string): Promise<RulesetSchemaDto> {
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<void> {
await this.loadStats();
}
async getAllRulesets(): Promise<RulesetSchemaDto[]> {
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;

View File

@@ -0,0 +1,53 @@
export interface RulesetSchemaDto {
id: string;
label: string;
blocks: Record<string, BlockDef>;
}
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<string, CellDef>;
}
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;
}

22
src/Services/Service.ts Normal file
View File

@@ -0,0 +1,22 @@
import {ServiceError} from "../errors";
type ClassDef = { new (...args: any[]): {} };
export function Service<T extends ClassDef>(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;
}

43
src/Services/StatsDto.ts Normal file
View File

@@ -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<string, RulesetStatsDto>
gamesPlayed: number;
}
export interface RulesetStatsDto {
blockStats: Record<string, BlockStatsDto>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStatsDto;
}
export interface BlockStatsDto {
cellStats: Record<string, CellStatsDto>;
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;

View File

@@ -18,3 +18,14 @@ export const supportedLangToIntlDTF: Record<SupportedLang, Intl.DateTimeFormat>
de: Intl.DateTimeFormat('de-DE'),
it: Intl.DateTimeFormat('it-IT'),
};
export enum FieldType {
number = "numberField",
bool = "boolField",
bonus = "bonusField",
subtotal = "subtotalField",
globalTotal = "globalTotalField",
total = "totalField",
superkadi = "superkadiField",
multiplier = "multiplierField",
}

5
src/errors.ts Normal file
View File

@@ -0,0 +1,5 @@
export class ServiceError extends Error {
constructor(message?: string) {
super(message);
}
}

View File

@@ -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((
<React.StrictMode>
<App />
@@ -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();

View File

@@ -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);
});
}
}

View File

@@ -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';

View File

@@ -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",

View File

@@ -13,7 +13,8 @@
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
"isolatedModules": true,
"experimentalDecorators": true
},
"lib": [
"dom",

View File

@@ -13,7 +13,6 @@
"prefer-readonly": true,
"typedef": [
true,
"call-signature",
"property-declaration"
],
"ordered-imports": false,

View File

@@ -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/",