diff --git a/.babelrc b/.babelrc index 0e8ee5e..4da33a6 100755 --- a/.babelrc +++ b/.babelrc @@ -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}] + ] } diff --git a/package-lock.json b/package-lock.json index 8d843a9..db62f11 100755 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8d54fa3..52c26f2 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100755 index 4db7ebc..0000000 --- a/src/App.test.js +++ /dev/null @@ -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(); - const linkElement = getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/App.tsx b/src/App.tsx index eef6711..da24293 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { 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 { .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"}} @@ -89,45 +84,23 @@ class App extends React.Component { return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + ); } } -interface KadiPageRouteProps { - pageId: PageId -} - -const KadiPageRoute: React.FunctionComponent = (props: KadiPageRouteProps) => { - const {pageId} = props; - const PageComponent = pageComponentFromId[pageId]; - return ( - - - - - - ); -}; - - export default App; \ No newline at end of file diff --git a/src/Components/CreateRulesetPanel.tsx b/src/Components/CreateRulesetPanel.tsx new file mode 100644 index 0000000..be89e25 --- /dev/null +++ b/src/Components/CreateRulesetPanel.tsx @@ -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 = (props) => { + const {onSubmitRuleset} = props; + const {strings: Locale} = useContext(UserContext); + const [currentRulesetBuild, updateCurrentRulesetBuild] = useState({ + 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 ( + <> +
+ {Locale.rulesetsPage.newRuleset} +
+ +
+ {Locale.rulesetsPage.newBlock} +
+ +
+ + + updateNewBlockInput({...newBlockInput, label: e.target.value})} + /> + + + updateNewBlockInput({...newBlockInput, hasBonus: c.checked ?? false})} + label={Locale.rulesetsPage.bonus} + /> + + {newBlockInput.hasBonus && ( + <> + + + updateNewBlockInput({...newBlockInput, bonusScore: Number(e.target.value)})} + /> + + + + updateNewBlockInput({...newBlockInput, bonusFor: Number(e.target.value)})} + /> + + + )} + + + +
+
+ + + ); +}; + +export default CreateRulesetPanel; \ No newline at end of file diff --git a/src/Components/GamesList.tsx b/src/Components/GamesList.tsx index 36eeb95..555e27b 100644 --- a/src/Components/GamesList.tsx +++ b/src/Components/GamesList.tsx @@ -10,10 +10,11 @@ interface GamesListProps { const GamesList: React.FunctionComponent = (props) => { const {loading, gamesList} = props; const Uctx = React.useContext(UserContext); - const listItems = gamesList.map(listing => - - Game played on: {Uctx.dateTimeFormatter.format(new Date(listing.createdAt))} + const listItems = gamesList.map(listing => ( + + Game: {JSON.stringify(listing)} + ) ); return ( <> diff --git a/src/Components/GuestList.tsx b/src/Components/GuestList.tsx index ba33bec..deca159 100644 --- a/src/Components/GuestList.tsx +++ b/src/Components/GuestList.tsx @@ -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 = (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 ( + + + + + {blockDef.label} + + + + + {idAndCellList.map(idAndCell => ( + + ))} + {editable && ( + + + + + + updateCurrentCellInput({...currentCellInput, label: e.target.value})} + /> + + + onChangeDropdown(c.value as FieldType)} + options={fieldTypeOptions} + button={true} + /> + + + {currentCellInput.fieldType === FieldType.multiplier && ( + updateCurrentCellInput( + {...currentCellInput, multiplier: Number(e.target.value)})} + /> + )} + {(currentCellInput.fieldType === FieldType.superkadi + || currentCellInput.fieldType === FieldType.multiplier) && ( + updateCurrentCellInput({ + ...currentCellInput, + maxMultiples: Number(e.target.value), + })} + /> + )} + {(currentCellInput.fieldType === FieldType.bool + || currentCellInput.fieldType === FieldType.superkadi + || currentCellInput.fieldType === FieldType.number) && ( + updateCurrentCellInput( + {...currentCellInput, score: Number(e.target.value)})} + /> + )} + + + + + + + + + )} + + + {subText} + + + +
+ ); +}; + +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 ( + + {rulesetNames.map(name => ( + onItemChange(name)} + /> + ))} + onItemChange("addNewRuleset")} + > + {Locale.rulesetsPage.newRuleset} + + + + ); +}; + +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/",