Big update
This commit is contained in:
@@ -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();
|
||||
});
|
||||
67
src/App.tsx
67
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<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"}}
|
||||
@@ -89,45 +84,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={this.state.userContext}>
|
||||
<Router basename={SERVER_BASE_NAME}>
|
||||
<Route exact={true} path={"/"}>
|
||||
<KadiPage activePage={PageId.home}>
|
||||
<HomePage/>
|
||||
</KadiPage>
|
||||
</Route>
|
||||
<KadiPageRoute pageId={PageId.history}/>
|
||||
<KadiPageRoute pageId={PageId.friends}/>
|
||||
<KadiPageRoute pageId={PageId.stats}/>
|
||||
<KadiPageRoute pageId={PageId.profile}/>
|
||||
<KadiPageRoute pageId={PageId.rulesets}/>
|
||||
<Route path={"/"}>
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/",
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
<Router basename={SERVER_BASE_NAME}>
|
||||
<Route exact={true} path={"/"}>
|
||||
<KadiPage activePage={PageId.home}>
|
||||
<HomePage/>
|
||||
</KadiPage>
|
||||
</Route>
|
||||
<KadiPageRoute pageId={PageId.history}/>
|
||||
<KadiPageRoute pageId={PageId.friends}/>
|
||||
<KadiPageRoute pageId={PageId.stats}/>
|
||||
<KadiPageRoute pageId={PageId.profile}/>
|
||||
<KadiPageRoute pageId={PageId.rulesets}/>
|
||||
<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;
|
||||
138
src/Components/CreateRulesetPanel.tsx
Normal file
138
src/Components/CreateRulesetPanel.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
24
src/Components/KadiPageRoute.tsx
Normal file
24
src/Components/KadiPageRoute.tsx
Normal 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;
|
||||
14
src/Components/Loading.tsx
Normal file
14
src/Components/Loading.tsx
Normal 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;
|
||||
@@ -18,9 +18,7 @@ const KadiPageMainContent: React.FunctionComponent<KadiPageMainContentProps> = (
|
||||
<Container
|
||||
className={"mainPageContentContainer"}
|
||||
>
|
||||
<Segment>
|
||||
{children}
|
||||
</Segment>
|
||||
{children}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
207
src/Components/RulesetBlockTable.tsx
Normal file
207
src/Components/RulesetBlockTable.tsx
Normal 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;
|
||||
31
src/Components/RulesetCellTableRow.tsx
Normal file
31
src/Components/RulesetCellTableRow.tsx
Normal 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;
|
||||
67
src/Components/RulesetDisplayPanel.tsx
Normal file
67
src/Components/RulesetDisplayPanel.tsx
Normal 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;
|
||||
39
src/Components/RulesetList.tsx
Normal file
39
src/Components/RulesetList.tsx
Normal 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;
|
||||
@@ -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>
|
||||
{Locale.rulesetsPage.title}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{Locale.statsPage.title}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
95
src/Components/StatsTable.tsx
Normal file
95
src/Components/StatsTable.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
62
src/Services/KadiStatsService.ts
Normal file
62
src/Services/KadiStatsService.ts
Normal 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;
|
||||
53
src/Services/RulesetSchemaDto.ts
Normal file
53
src/Services/RulesetSchemaDto.ts
Normal 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
22
src/Services/Service.ts
Normal 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
43
src/Services/StatsDto.ts
Normal 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;
|
||||
13
src/enums.ts
13
src/enums.ts
@@ -17,4 +17,15 @@ export const supportedLangToIntlDTF: Record<SupportedLang, Intl.DateTimeFormat>
|
||||
gb: Intl.DateTimeFormat('en-AU'),
|
||||
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
5
src/errors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class ServiceError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user