diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/account.tsx | 33 | ||||
-rw-r--r-- | components/dialogBox.tsx | 40 | ||||
-rw-r--r-- | components/footer.tsx | 52 | ||||
-rw-r--r-- | components/gameBar.tsx | 148 | ||||
-rw-r--r-- | components/gameSettings.tsx | 358 | ||||
-rw-r--r-- | components/globalState.tsx | 6 | ||||
-rw-r--r-- | components/logo.tsx | 29 | ||||
-rw-r--r-- | components/navbar.tsx | 192 | ||||
-rw-r--r-- | components/notificationsArea.tsx | 254 | ||||
-rw-r--r-- | components/page.tsx | 48 | ||||
-rw-r--r-- | components/preferencesContext.tsx | 66 | ||||
-rw-r--r-- | components/recentGames.tsx | 123 | ||||
-rw-r--r-- | components/socketContext.tsx | 11 | ||||
-rw-r--r-- | components/toast.tsx | 192 | ||||
-rw-r--r-- | components/ui.tsx | 415 | ||||
-rw-r--r-- | components/voerBord.tsx | 95 |
16 files changed, 1176 insertions, 886 deletions
diff --git a/components/account.tsx b/components/account.tsx index 7b825cd..f24135f 100644 --- a/components/account.tsx +++ b/components/account.tsx @@ -1,4 +1,5 @@ -var dummy = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2P4z/j/PwAHAQL/gXZXNQAAAABJRU5ErkJggg=="; +var dummy = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2P4z/j/PwAHAQL/gXZXNQAAAABJRU5ErkJggg=='; export function AccountAvatar(props: { size: number; @@ -8,24 +9,24 @@ export function AccountAvatar(props: { id?: string; }) { - var image = ""; - image += "/api/user/avatar"; - if (typeof props.id === "string") { - if (!props.id) image = ""; + var image = ''; + image += '/api/user/avatar'; + if (typeof props.id === 'string') { + if (!props.id) image = ''; else image += `?id=${props.id}`; } if (props.dummy) image = dummy; - return <div style={{ - width: props.size, - height: props.size, - backgroundColor: props.fallbackFill || "var(--background)", - backgroundImage: `url(${image})`, - backgroundSize: "cover", - display: "inline-block", - borderRadius: props.size / 2 * Number(props.round || 0) - }}/>; + return <div + style={{ + width: props.size, + height: props.size, + backgroundColor: props.fallbackFill || 'var(--background)', + backgroundImage: `url(${image})`, + backgroundSize: 'cover', + display: 'inline-block', + borderRadius: props.size / 2 * Number(props.round || 0), + }} + />; } - - diff --git a/components/dialogBox.tsx b/components/dialogBox.tsx index 5ef5c3f..7abbded 100644 --- a/components/dialogBox.tsx +++ b/components/dialogBox.tsx @@ -1,4 +1,4 @@ -import { ReactNode, CSSProperties } from 'react'; +import { CSSProperties, ReactNode } from 'react'; import { Vierkant } from './ui'; @@ -10,24 +10,30 @@ export function DialogBox(props: { style?: CSSProperties; onclick?: () => void; }) { - return <Vierkant style={{ - position: "fixed", - top: "50%", left: "50%", - transform: "translate(-50%, -50%)", - boxShadow: "0 8px 32px -5px #0007", - width: 392, - ...props.style - }}> + return <Vierkant + style={{ + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + boxShadow: '0 8px 32px -5px #0007', + width: 392, + ...props.style, + }} + > <h2 style={{ marginBottom: 24 }}>{props.title}</h2> <span onClick={props.onclick}> - <CancelIcon style={{ - position: "absolute", - top: 25, right: 25, - color: "var(--text)", - opacity: .85, - cursor: "pointer" - }}/> + <CancelIcon + style={{ + position: 'absolute', + top: 25, + right: 25, + color: 'var(--text)', + opacity: .85, + cursor: 'pointer', + }} + /> </span> {props.children} - </Vierkant> + </Vierkant>; } diff --git a/components/footer.tsx b/components/footer.tsx index 2d84a1c..36ebef3 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,49 +1,49 @@ -import { LogoDark } from "../components/logo"; import { ReactNode } from 'react'; +import { LogoDark } from '../components/logo'; -import Home from '@material-ui/icons/Home'; -import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; +import ExitToAppOutlinedIcon from '@material-ui/icons/ExitToAppOutlined'; import ExtensionIcon from '@material-ui/icons/Extension'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import Home from '@material-ui/icons/Home'; +import LockIcon from '@material-ui/icons/Lock'; +import PersonIcon from '@material-ui/icons/Person'; import SearchIcon from '@material-ui/icons/Search'; import SettingsIcon from '@material-ui/icons/Settings'; -import PersonIcon from '@material-ui/icons/Person'; -import ExitToAppOutlinedIcon from '@material-ui/icons/ExitToAppOutlined'; -import LockIcon from '@material-ui/icons/Lock'; -import GitHubIcon from '@material-ui/icons/GitHub'; +import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; function PageLink(props: { icon: ReactNode; href: string; children: string; }) { - return <a href={props.href} className="pageLink"> + return <a href={props.href} className='pageLink'> {props.icon} <span>{props.children}</span> - </a> + </a>; } export function Footer() { - return <div className="footer"> - <div className="header"> - <LogoDark/> + return <div className='footer'> + <div className='header'> + <LogoDark /> <h2>4 op een rij</h2> </div> - <div className="content"> - <div className="column"> - <PageLink icon={<Home/>} href="/" children="Home"/> - <PageLink icon={<VideogameAssetIcon/>} href="/game" children="Spelen"/> - <PageLink icon={<ExtensionIcon/>} href="/" children="Puzzels"/> - <PageLink icon={<SearchIcon/>} href="/search" children="Zoeken"/> + <div className='content'> + <div className='column'> + <PageLink icon={<Home />} href='/' children='Home' /> + <PageLink icon={<VideogameAssetIcon />} href='/game' children='Spelen' /> + <PageLink icon={<ExtensionIcon />} href='/' children='Puzzels' /> + <PageLink icon={<SearchIcon />} href='/search' children='Zoeken' /> </div> - <div className="column"> - <PageLink icon={<LockIcon/>} href="/privacy" children="Privacy"/> - <PageLink icon={<GitHubIcon/>} href="https://github.com/lonkaars/connect-4" children="Broncode"/> + <div className='column'> + <PageLink icon={<LockIcon />} href='/privacy' children='Privacy' /> + <PageLink icon={<GitHubIcon />} href='https://github.com/lonkaars/connect-4' children='Broncode' /> </div> - <div className="column"> - <PageLink icon={<SettingsIcon/>} href="/settings" children="Instellingen"/> - <PageLink icon={<PersonIcon/>} href="/user" children="Profiel"/> - <PageLink icon={<ExitToAppOutlinedIcon/>} href="/logout" children="Uitloggen"/> + <div className='column'> + <PageLink icon={<SettingsIcon />} href='/settings' children='Instellingen' /> + <PageLink icon={<PersonIcon />} href='/user' children='Profiel' /> + <PageLink icon={<ExitToAppOutlinedIcon />} href='/logout' children='Uitloggen' /> </div> </div> - </div> + </div>; } diff --git a/components/gameBar.tsx b/components/gameBar.tsx index 67712e2..0d7d4d9 100644 --- a/components/gameBar.tsx +++ b/components/gameBar.tsx @@ -1,30 +1,35 @@ import { CSSProperties, ReactNode } from 'react'; -import { Vierkant, Bubble } from './ui'; +import { Bubble, Vierkant } from './ui'; -import SettingsRoundedIcon from '@material-ui/icons/SettingsRounded'; import ExitToAppRoundedIcon from '@material-ui/icons/ExitToAppRounded'; -import NavigateNextRoundedIcon from '@material-ui/icons/NavigateNextRounded'; import NavigateBeforeRoundedIcon from '@material-ui/icons/NavigateBeforeRounded'; +import NavigateNextRoundedIcon from '@material-ui/icons/NavigateNextRounded'; +import SettingsRoundedIcon from '@material-ui/icons/SettingsRounded'; function GameBarModule(props: { children?: ReactNode; onclick?: () => void; }) { - return <Vierkant style={{ - backgroundColor: "var(--background-alt)", - padding: 12, - borderRadius: 6, - margin: 0, - verticalAlign: "top", - cursor: props.onclick ? "pointer" : "default" - }} onclick={props.onclick}>{props.children}</Vierkant> + return <Vierkant + style={{ + backgroundColor: 'var(--background-alt)', + padding: 12, + borderRadius: 6, + margin: 0, + verticalAlign: 'top', + cursor: props.onclick ? 'pointer' : 'default', + }} + onclick={props.onclick} + > + {props.children} + </Vierkant>; } -var GameBarSpacer = () => <div style={{ width: 8, display: "inline-block" }}></div>; +var GameBarSpacer = () => <div style={{ width: 8, display: 'inline-block' }}></div>; var GameBarAlignStyle: CSSProperties = { - display: "inline-block" -} + display: 'inline-block', +}; export function GameBar(props: { turn: boolean; @@ -32,64 +37,85 @@ export function GameBar(props: { active: boolean; resignFunction: () => void; }) { - return <Vierkant className="gameBar" style={{ - padding: 8, - width: "calc(100% - 12px)" - }}> - <div style={{ gridAutoColumns: "auto" }}> - <div style={{ ...GameBarAlignStyle, float: "left" }}> - <div style={{ - width: 32, height: 32, - margin: 8, - backgroundColor: props.turn ? "var(--disk-b)" : "var(--disk-a)", - borderRadius: 16, - display: "inline-block" - }}/> - <h2 style={{ - fontSize: 20, - margin: 12, - verticalAlign: "top", - display: "inline-block" - }}>{ - !props.active ? "Wachten op tegenstander..." : - props.turn == props.player1 ? - "Jouw beurt" : "Tegenstander" - }</h2> + return <Vierkant + className='gameBar' + style={{ + padding: 8, + width: 'calc(100% - 12px)', + }} + > + <div style={{ gridAutoColumns: 'auto' }}> + <div style={{ ...GameBarAlignStyle, float: 'left' }}> + <div + style={{ + width: 32, + height: 32, + margin: 8, + backgroundColor: props.turn ? 'var(--disk-b)' : 'var(--disk-a)', + borderRadius: 16, + display: 'inline-block', + }} + /> + <h2 + style={{ + fontSize: 20, + margin: 12, + verticalAlign: 'top', + display: 'inline-block', + }} + > + {!props.active + ? 'Wachten op tegenstander...' + : props.turn == props.player1 + ? 'Jouw beurt' + : 'Tegenstander'} + </h2> </div> - <div style={{ - ...GameBarAlignStyle, - position: "absolute", - top: "50%", left: "50%", - transform: "translate(-50%, -50%)" - }}> - <span style={{ - color: "var(--text)", - fontSize: 20, - opacity: .75 - .75 - }}>0-0</span> + <div + style={{ + ...GameBarAlignStyle, + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }} + > + <span + style={{ + color: 'var(--text)', + fontSize: 20, + opacity: .75 - .75, + }} + > + 0-0 + </span> </div> - <div style={{ ...GameBarAlignStyle, float: "right" }}> + <div style={{ ...GameBarAlignStyle, float: 'right' }}> <GameBarModule> - <SettingsRoundedIcon/> + <SettingsRoundedIcon /> </GameBarModule> - <GameBarSpacer/> + <GameBarSpacer /> <GameBarModule> - <span style={{ - margin: "0 4px", - fontSize: 20 - }}>00:00</span> + <span + style={{ + margin: '0 4px', + fontSize: 20, + }} + > + 00:00 + </span> </GameBarModule> - <GameBarSpacer/> + <GameBarSpacer /> <GameBarModule onclick={props.resignFunction}> - <ExitToAppRoundedIcon/> + <ExitToAppRoundedIcon /> </GameBarModule> - <GameBarSpacer/> + <GameBarSpacer /> <GameBarModule> - <NavigateBeforeRoundedIcon/> + <NavigateBeforeRoundedIcon /> </GameBarModule> - <GameBarSpacer/> + <GameBarSpacer /> <GameBarModule> - <NavigateNextRoundedIcon/> + <NavigateNextRoundedIcon /> </GameBarModule> </div> </div> diff --git a/components/gameSettings.tsx b/components/gameSettings.tsx index be45112..f562e5d 100644 --- a/components/gameSettings.tsx +++ b/components/gameSettings.tsx @@ -1,17 +1,16 @@ -import { ReactNode, Component, CSSProperties } from 'react'; import axios from 'axios'; +import { Component, CSSProperties, ReactNode } from 'react'; -import { Button, Vierkant, CheckBox, Input } from './ui'; -import { DialogBox } from './dialogBox'; import { ruleset, userPreferences } from '../api/api'; +import { DialogBox } from './dialogBox'; +import { Button, CheckBox, Input, Vierkant } from './ui'; import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined'; - type CurrentGameSettingsStateType = { editGameRulesDialogVisible: boolean; ruleset: ruleset; -} +}; export class CurrentGameSettings extends Component { state: CurrentGameSettingsStateType = { @@ -19,25 +18,25 @@ export class CurrentGameSettings extends Component { ruleset: { timelimit: { enabled: false, - shared: false + shared: false, }, - ranked: false - } - } + ranked: false, + }, + }; constructor(props: {}) { super(props); - if (typeof window === "undefined") return; // return if run on server + if (typeof window === 'undefined') return; // return if run on server axios.request<userPreferences>({ - method: "get", + method: 'get', url: `/api/user/preferences`, - headers: {"content-type": "application/json"} + headers: { 'content-type': 'application/json' }, }) - //FIXME: this assumes the request ruleset has all properties of a ruleset - .then(request => this.setState({ ruleset: request.data.ruleset || this.state.ruleset })) - .catch(() => {}); + // FIXME: this assumes the request ruleset has all properties of a ruleset + .then(request => this.setState({ ruleset: request.data.ruleset || this.state.ruleset })) + .catch(() => {}); } showEditGameRules = () => this.setState({ editGameRulesDialogVisible: true }); @@ -45,51 +44,67 @@ export class CurrentGameSettings extends Component { setGameRules = (newRules: ruleset) => this.setState({ ruleset: newRules }); render() { - var timelimit_str = this.state.ruleset.timelimit.enabled ? - `${this.state.ruleset.timelimit.minutes}m${this.state.ruleset.timelimit.seconds}s plus ${this.state.ruleset.timelimit.addmove}` : - "Geen tijdslimiet" - var ranked_str = this.state.ruleset.ranked ? - "Gerangschikt" : - "Niet gerangschikt" - return <div style={{ - position: "relative", - height: 80, - overflow: "visible", - zIndex: 1 - }}> - <p style={{ - opacity: .75, - fontStyle: "italic", - userSelect: "none", - position: "absolute", - top: "50%", - left: 0, - transform: "translateY(-50%)" - }}> - {timelimit_str}<br/> + var timelimit_str = this.state.ruleset.timelimit.enabled + ? `${this.state.ruleset.timelimit.minutes}m${this.state.ruleset.timelimit.seconds}s plus ${this.state.ruleset.timelimit.addmove}` + : 'Geen tijdslimiet'; + var ranked_str = this.state.ruleset.ranked + ? 'Gerangschikt' + : 'Niet gerangschikt'; + return <div + style={{ + position: 'relative', + height: 80, + overflow: 'visible', + zIndex: 1, + }} + > + <p + style={{ + opacity: .75, + fontStyle: 'italic', + userSelect: 'none', + position: 'absolute', + top: '50%', + left: 0, + transform: 'translateY(-50%)', + }} + > + {timelimit_str} + <br /> {ranked_str} </p> - <Button style={{ - width: 150, - position: "absolute", - top: "50%", - right: 0, - transform: "translateY(-50%)" - }} onclick={this.showEditGameRules}> - <BuildOutlinedIcon style={{ fontSize: 48 }}/> - <span style={{ - fontWeight: 600, - position: "absolute", - right: 24, - top: "50%", - width: 85, - verticalAlign: "middle", - textAlign: "center", - transform: "translateY(-50%)", - userSelect: "none" - }}>Spelregels aanpassen</span> + <Button + style={{ + width: 150, + position: 'absolute', + top: '50%', + right: 0, + transform: 'translateY(-50%)', + }} + onclick={this.showEditGameRules} + > + <BuildOutlinedIcon style={{ fontSize: 48 }} /> + <span + style={{ + fontWeight: 600, + position: 'absolute', + right: 24, + top: '50%', + width: 85, + verticalAlign: 'middle', + textAlign: 'center', + transform: 'translateY(-50%)', + userSelect: 'none', + }} + > + Spelregels aanpassen + </span> </Button> - <EditGameSettings parentState={this.state} hideEditGameRules={this.hideEditGameRules} setGameRules={this.setGameRules}/> + <EditGameSettings + parentState={this.state} + hideEditGameRules={this.hideEditGameRules} + setGameRules={this.setGameRules} + /> </div>; } } @@ -101,25 +116,36 @@ function GameSettingsSection(props: { noMarginBottom?: boolean; id: string; }) { - return <Vierkant id={props.id} style={{ - backgroundColor: "var(--background-alt)", - width: "100%", - padding: 16, - margin: 0, - marginBottom: props.noMarginBottom ? 0 : 24 - }}> - <span style={{ - verticalAlign: "top", - fontSize: 14, - fontWeight: 600 - }}>{props.title}</span> - <CheckBox state={props.state} id={`${props.id}_enabled`} style={{ - verticalAlign: "top", - float: "right", - margin: -3 - }}/> - <div>{props.children}</div> - </Vierkant> + return <Vierkant + id={props.id} + style={{ + backgroundColor: 'var(--background-alt)', + width: '100%', + padding: 16, + margin: 0, + marginBottom: props.noMarginBottom ? 0 : 24, + }} + > + <span + style={{ + verticalAlign: 'top', + fontSize: 14, + fontWeight: 600, + }} + > + {props.title} + </span> + <CheckBox + state={props.state} + id={`${props.id}_enabled`} + style={{ + verticalAlign: 'top', + float: 'right', + margin: -3, + }} + /> + <div>{props.children}</div> + </Vierkant>; } function GameRule(props: { @@ -127,15 +153,17 @@ function GameRule(props: { description: string; style?: CSSProperties; }) { - return <div style={{ - backgroundColor: "var(--page-background)", - borderRadius: 8, - padding: "16px 0", - textAlign: "center", - ...props.style - }}> - <h1 style={{ color: "var(--disk-a)", fontSize: 42 }}>{props.title}</h1> - <p style={{ color: "var(--text-alt)", maxWidth: 250, margin: "0 auto" }}>{props.description}</p> + return <div + style={{ + backgroundColor: 'var(--page-background)', + borderRadius: 8, + padding: '16px 0', + textAlign: 'center', + ...props.style, + }} + > + <h1 style={{ color: 'var(--disk-a)', fontSize: 42 }}>{props.title}</h1> + <p style={{ color: 'var(--text-alt)', maxWidth: 250, margin: '0 auto' }}>{props.description}</p> </div>; } @@ -146,66 +174,114 @@ type editGameSettingsProps = { }; export class EditGameSettings extends Component<editGameSettingsProps> { - render () { - return <DialogBox title="Spelregels aanpassen" style={{ - margin: 0, - display: this.props.parentState.editGameRulesDialogVisible ? "block" : "none" - }} onclick={this.props.hideEditGameRules}> - <div style={{ - marginTop: 24, - maxHeight: 500, - overflowY: "scroll", - borderRadius: 8, - }}> - <GameSettingsSection title="Tijdslimiet" state={this.props.parentState.ruleset.timelimit.enabled} id="timelimit"> - <div style={{ - display: "grid", - gridTemplateColumns: "1fr 1fr 1fr", - gridGap: 16, - margin: "16px 0" - }}> - <Input id="timelimit_minutes" type="number" label="min" min={0} max={60} value={this.props.parentState.ruleset.timelimit.minutes}/> - <Input id="timelimit_seconds" type="number" label="sec" min={0} max={60} value={this.props.parentState.ruleset.timelimit.seconds}/> - <Input id="timelimit_addmove" type="number" label="plus" min={0} value={this.props.parentState.ruleset.timelimit.addmove}/> + render() { + return <DialogBox + title='Spelregels aanpassen' + style={{ + margin: 0, + display: this.props.parentState.editGameRulesDialogVisible ? 'block' : 'none', + }} + onclick={this.props.hideEditGameRules} + > + <div + style={{ + marginTop: 24, + maxHeight: 500, + overflowY: 'scroll', + borderRadius: 8, + }} + > + <GameSettingsSection + title='Tijdslimiet' + state={this.props.parentState.ruleset.timelimit.enabled} + id='timelimit' + > + <div + style={{ + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gridGap: 16, + margin: '16px 0', + }} + > + <Input + id='timelimit_minutes' + type='number' + label='min' + min={0} + max={60} + value={this.props.parentState.ruleset.timelimit.minutes} + /> + <Input + id='timelimit_seconds' + type='number' + label='sec' + min={0} + max={60} + value={this.props.parentState.ruleset.timelimit.seconds} + /> + <Input + id='timelimit_addmove' + type='number' + label='plus' + min={0} + value={this.props.parentState.ruleset.timelimit.addmove} + /> </div> - <CheckBox id="timelimit_shared" state={this.props.parentState.ruleset.timelimit.shared}/> - <span style={{ - verticalAlign: "super", - marginLeft: 4 - }}>Timer gebruiken voor bijde spelers</span> + <CheckBox id='timelimit_shared' state={this.props.parentState.ruleset.timelimit.shared} /> + <span + style={{ + verticalAlign: 'super', + marginLeft: 4, + }} + > + Timer gebruiken voor bijde spelers + </span> </GameSettingsSection> - { false && <GameSettingsSection title="Regelset" state={false}> - <div style={{ - display: "grid", - gridTemplateColumns: "1fr 1fr", - gridGap: 16, - margin: "16px 0" - }}> - <GameRule title="+2" description="Extra kolommen"/> - <GameRule title="+4" description="Extra kolommen"/> + {false && <GameSettingsSection title='Regelset' state={false}> + <div + style={{ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gridGap: 16, + margin: '16px 0', + }} + > + <GameRule title='+2' description='Extra kolommen' /> + <GameRule title='+4' description='Extra kolommen' /> </div> - <GameRule style={{ marginBottom: 16 }} title="Gravity" description="De zwaartekracht draait soms"/> - <GameRule title="Flashlight" description="Het veld wordt opgelicht door de vallende fiches"/> - </GameSettingsSection> } - <GameSettingsSection title="Gerangschikt spel" state={this.props.parentState.ruleset.ranked} id="ranked" noMarginBottom/> + <GameRule style={{ marginBottom: 16 }} title='Gravity' description='De zwaartekracht draait soms' /> + <GameRule title='Flashlight' description='Het veld wordt opgelicht door de vallende fiches' /> + </GameSettingsSection>} + <GameSettingsSection + title='Gerangschikt spel' + state={this.props.parentState.ruleset.ranked} + id='ranked' + noMarginBottom + /> </div> - <Button style={{ - textAlign: "center", - marginTop: 24 - }} onclick={() => { - var rules: ruleset = { - timelimit: { - enabled: document.getElementById("timelimit_enabled").classList.contains("on"), - minutes: Number((document.getElementById("timelimit_minutes") as HTMLInputElement).value), - seconds: Number((document.getElementById("timelimit_seconds") as HTMLInputElement).value), - addmove: Number((document.getElementById("timelimit_addmove") as HTMLInputElement).value), - shared: document.getElementById("timelimit_shared").classList.contains("on"), - }, - ranked: document.getElementById("ranked_enabled").classList.contains("on") - } - this.props.setGameRules(rules); - this.props.hideEditGameRules(); - }}>Instellingen opslaan</Button> + <Button + style={{ + textAlign: 'center', + marginTop: 24, + }} + onclick={() => { + var rules: ruleset = { + timelimit: { + enabled: document.getElementById('timelimit_enabled').classList.contains('on'), + minutes: Number((document.getElementById('timelimit_minutes') as HTMLInputElement).value), + seconds: Number((document.getElementById('timelimit_seconds') as HTMLInputElement).value), + addmove: Number((document.getElementById('timelimit_addmove') as HTMLInputElement).value), + shared: document.getElementById('timelimit_shared').classList.contains('on'), + }, + ranked: document.getElementById('ranked_enabled').classList.contains('on'), + }; + this.props.setGameRules(rules); + this.props.hideEditGameRules(); + }} + > + Instellingen opslaan + </Button> </DialogBox>; } } diff --git a/components/globalState.tsx b/components/globalState.tsx index 5b01947..0ec7838 100644 --- a/components/globalState.tsx +++ b/components/globalState.tsx @@ -6,9 +6,9 @@ type globalState = { on: boolean; time: number; useForBoth: boolean; - } + }; rankedGame: boolean; - } -} + }; +}; export var GlobalStateContext = React.createContext(); diff --git a/components/logo.tsx b/components/logo.tsx index b0f358f..e43aa88 100644 --- a/components/logo.tsx +++ b/components/logo.tsx @@ -1,12 +1,12 @@ export function LogoDark() { return ( - <div className="noclick"> - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <rect width="24" height="24" fill="var(--background)"/> - <circle cx="6.5" cy="6.5" r="4.5" fill="var(--disk-b)"/> - <circle cx="6.5" cy="17.5" r="4.5" fill="var(--disk-a)"/> - <circle cx="17.5" cy="17.5" r="4.5" fill="var(--disk-b)"/> - <circle cx="17.5" cy="6.5" r="3.5" stroke="var(--text)" strokeWidth="2"/> + <div className='noclick'> + <svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> + <rect width='24' height='24' fill='var(--background)' /> + <circle cx='6.5' cy='6.5' r='4.5' fill='var(--disk-b)' /> + <circle cx='6.5' cy='17.5' r='4.5' fill='var(--disk-a)' /> + <circle cx='17.5' cy='17.5' r='4.5' fill='var(--disk-b)' /> + <circle cx='17.5' cy='6.5' r='3.5' stroke='var(--text)' strokeWidth='2' /> </svg> </div> ); @@ -14,15 +14,14 @@ export function LogoDark() { export function LogoLight() { return ( - <div className="noclick"> - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <rect width="24" height="24" fill="var(--page-background)"/> - <circle cx="6.5" cy="6.5" r="4.5" fill="var(--disk-b)"/> - <circle cx="6.5" cy="17.5" r="4.5" fill="var(--disk-a)"/> - <circle cx="17.5" cy="17.5" r="4.5" fill="var(--disk-b)"/> - <circle cx="17.5" cy="6.5" r="3.5" stroke="var(--background)" strokeWidth="2"/> + <div className='noclick'> + <svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> + <rect width='24' height='24' fill='var(--page-background)' /> + <circle cx='6.5' cy='6.5' r='4.5' fill='var(--disk-b)' /> + <circle cx='6.5' cy='17.5' r='4.5' fill='var(--disk-a)' /> + <circle cx='17.5' cy='17.5' r='4.5' fill='var(--disk-b)' /> + <circle cx='17.5' cy='6.5' r='3.5' stroke='var(--background)' strokeWidth='2' /> </svg> </div> ); } - diff --git a/components/navbar.tsx b/components/navbar.tsx index d6775ee..70de574 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -1,121 +1,147 @@ -import { CSSProperties, useEffect, useState, useContext } from "react"; -import axios from "axios"; +import axios from 'axios'; +import { CSSProperties, useContext, useEffect, useState } from 'react'; -import { LogoDark } from "../components/logo"; -import { AccountAvatar } from "./account"; -import { userInfo } from "../api/api"; -import { NotificationsArea } from "./notificationsArea"; -import { SocketContext } from "./socketContext"; +import { userInfo } from '../api/api'; +import { LogoDark } from '../components/logo'; +import { AccountAvatar } from './account'; +import { NotificationsArea } from './notificationsArea'; +import { SocketContext } from './socketContext'; -import Home from '@material-ui/icons/Home'; -import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; import ExtensionIcon from '@material-ui/icons/Extension'; +import Home from '@material-ui/icons/Home'; +import NotificationsIcon from '@material-ui/icons/Notifications'; +import PersonIcon from '@material-ui/icons/Person'; import SearchIcon from '@material-ui/icons/Search'; import SettingsIcon from '@material-ui/icons/Settings'; -import PersonIcon from '@material-ui/icons/Person'; -import NotificationsIcon from '@material-ui/icons/Notifications'; +import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; var NavBarItemStyle: CSSProperties = { margin: 12, marginBottom: 16, - display: "block" -} + display: 'block', +}; export function NavBar() { - var [ loggedIn, setLoggedIn ] = useState(false); - var [ gotData, setGotData ] = useState(false); + var [loggedIn, setLoggedIn] = useState(false); + var [gotData, setGotData] = useState(false); - var [ friendRequests, setFriendRequests ] = useState<Array<userInfo>>(null); + var [friendRequests, setFriendRequests] = useState<Array<userInfo>>(null); - var [ notificationsAreaVisible, setNotificationsAreaVisible ] = useState(false); - var [ gotNotifications, setGotNotifications ] = useState(false); + var [notificationsAreaVisible, setNotificationsAreaVisible] = useState(false); + var [gotNotifications, setGotNotifications] = useState(false); var { io } = useContext(SocketContext); async function getNotifications() { - var friendRequestsReq = await axios.request<{ requests: Array<userInfo> }>({ - method: "get", - url: `/api/social/list/requests` + var friendRequestsReq = await axios.request<{ requests: Array<userInfo>; }>({ + method: 'get', + url: `/api/social/list/requests`, }); setFriendRequests(friendRequestsReq.data.requests); setGotNotifications(friendRequestsReq.data.requests.length > 0); } - useEffect(() => {(async () => { - if (gotData) return; - if (typeof window === "undefined") return; - - var loggedIn = document.cookie.includes("token"); - setLoggedIn(loggedIn); - - if (loggedIn) { - await getNotifications(); - io.on("incomingFriendRequest", getNotifications); - io.on("changedRelation", getNotifications); - } + useEffect(() => { + (async () => { + if (gotData) return; + if (typeof window === 'undefined') return; - setGotData(true); - })()}, []); + var loggedIn = document.cookie.includes('token'); + setLoggedIn(loggedIn); - return <div className="navbar" style={{ - width: 48, - height: "100%", + if (loggedIn) { + await getNotifications(); + io.on('incomingFriendRequest', getNotifications); + io.on('changedRelation', getNotifications); + } - lineHeight: 0, + setGotData(true); + })(); + }, []); - backgroundColor: "var(--background)", - display: "inline-block", + return <div + className='navbar' + style={{ + width: 48, + height: '100%', - position: "fixed", - top: 0, - left: 0, + lineHeight: 0, - overflow: "visible", - whiteSpace: "nowrap", - zIndex: 2 - }}> - <div style={NavBarItemStyle}><LogoDark/></div> - <a href="/" style={NavBarItemStyle}><Home/></a> - <a href="/game" style={NavBarItemStyle}><VideogameAssetIcon/></a> - { false && <a href="/" style={NavBarItemStyle}><ExtensionIcon/></a> } - <a href="/search" style={NavBarItemStyle}><SearchIcon/></a> + backgroundColor: 'var(--background)', + display: 'inline-block', - <div style={{ - position: "absolute", - bottom: -4, + position: 'fixed', + top: 0, left: 0, - backgroundColor: "var(--background)" - }}> - { loggedIn && <a style={{ - overflow: "visible", - position: "relative", - ...NavBarItemStyle - }}> - <div style={{ cursor: "pointer" }} onClick={() => setNotificationsAreaVisible(!notificationsAreaVisible)}> - <NotificationsIcon/> - { gotNotifications && <div style={{ - backgroundColor: "var(--disk-a)", - width: 8, height: 8, - borderRadius: 4, - position: "absolute", - top: 2, right: 2 - }}/> } + + overflow: 'visible', + whiteSpace: 'nowrap', + zIndex: 2, + }} + > + <div style={NavBarItemStyle}> + <LogoDark /> + </div> + <a href='/' style={NavBarItemStyle}> + <Home /> + </a> + <a href='/game' style={NavBarItemStyle}> + <VideogameAssetIcon /> + </a> + {false && <a href='/' style={NavBarItemStyle}> + <ExtensionIcon /> + </a>} + <a href='/search' style={NavBarItemStyle}> + <SearchIcon /> + </a> + + <div + style={{ + position: 'absolute', + bottom: -4, + left: 0, + backgroundColor: 'var(--background)', + }} + > + {loggedIn && <a + style={{ + overflow: 'visible', + position: 'relative', + ...NavBarItemStyle, + }} + > + <div + style={{ cursor: 'pointer' }} + onClick={() => setNotificationsAreaVisible(!notificationsAreaVisible)} + > + <NotificationsIcon /> + {gotNotifications && <div + style={{ + backgroundColor: 'var(--disk-a)', + width: 8, + height: 8, + borderRadius: 4, + position: 'absolute', + top: 2, + right: 2, + }} + />} </div> <NotificationsArea visible={notificationsAreaVisible} friendRequests={friendRequests} - rerender={getNotifications}/> - </a> } - <a href={loggedIn ? "/user" : "/login"} style={NavBarItemStyle}> - { - loggedIn ? - <AccountAvatar size={24} round/> : - <PersonIcon/> - } + rerender={getNotifications} + /> + </a>} + <a href={loggedIn ? '/user' : '/login'} style={NavBarItemStyle}> + {loggedIn + ? <AccountAvatar size={24} round /> + : <PersonIcon />} </a> - { loggedIn && <a href="/settings" style={NavBarItemStyle}><SettingsIcon/></a> } + {loggedIn && <a href='/settings' style={NavBarItemStyle}> + <SettingsIcon /> + </a>} </div> - </div> + </div>; } - diff --git a/components/notificationsArea.tsx b/components/notificationsArea.tsx index b427941..9573b72 100644 --- a/components/notificationsArea.tsx +++ b/components/notificationsArea.tsx @@ -1,13 +1,13 @@ -import { CSSProperties, ReactNode, useState, useContext, useEffect } from 'react'; import axios from 'axios'; +import { CSSProperties, ReactNode, useContext, useEffect, useState } from 'react'; -import { userInfo, gameInfo } from "../api/api"; -import { AccountAvatar } from "./account"; -import { Bubble, Vierkant, IconLabelButton } from './ui'; +import { gameInfo, userInfo } from '../api/api'; +import { AccountAvatar } from './account'; import { ToastContext } from './toast'; +import { Bubble, IconLabelButton, Vierkant } from './ui'; -import DoneIcon from '@material-ui/icons/Done'; import CloseIcon from '@material-ui/icons/Close'; +import DoneIcon from '@material-ui/icons/Done'; import NotificationsActiveOutlinedIcon from '@material-ui/icons/NotificationsActiveOutlined'; export function NotificationsArea(props: { @@ -17,71 +17,82 @@ export function NotificationsArea(props: { rerender: () => void; }) { var { toast } = useContext(ToastContext); - var [ previousMessages, setPreviousMessages ] = useState(0); + var [previousMessages, setPreviousMessages] = useState(0); var messages = ( - (props.friendRequests ? props.friendRequests.length : 0) + - (props.gameInvites ? props.gameInvites.length : 0) - ) + (props.friendRequests ? props.friendRequests.length : 0) + + (props.gameInvites ? props.gameInvites.length : 0) + ); useEffect(() => { - if(messages > previousMessages) { - toast({ message: "Je hebt nieuwe meldingen!", - type: "confirmation", - icon: <NotificationsActiveOutlinedIcon/>}); + if (messages > previousMessages) { + toast({ + message: 'Je hebt nieuwe meldingen!', + type: 'confirmation', + icon: <NotificationsActiveOutlinedIcon />, + }); } setPreviousMessages(messages); - }) + }); - return props.visible && <Bubble style={{ - left: 48 + 12, - top: 92, - transform: "translateY(-100%)", - textAlign: "left", - width: 400, - height: 450 - }} tuitjeStyle={{ - left: 12, - bottom: 86, - transform: "translate(-100%, 100%) rotate(90deg)" - }}> + return props.visible && <Bubble + style={{ + left: 48 + 12, + top: 92, + transform: 'translateY(-100%)', + textAlign: 'left', + width: 400, + height: 450, + }} + tuitjeStyle={{ + left: 12, + bottom: 86, + transform: 'translate(-100%, 100%) rotate(90deg)', + }} + > <h2 style={{ marginBottom: 24 }}>Meldingen</h2> - <div style={{ - overflowY: "scroll", - whiteSpace: "normal", - height: 450 - 24 * 4, - borderRadius: 6 - }}> - { props.gameInvites?.map(game => <GameInvite hide={props.rerender} game={game}/>) } - { props.friendRequests?.map(user => <FriendRequest hide={props.rerender} user={user}/>) } - { - messages == 0 && - <div style={{ - position: "absolute", - left: 0, - right: 0, - bottom: 0, - top: 0 - }}> - <h1 style={{ - position: "absolute", - top: "50%", - left: "50%", - whiteSpace: "nowrap", - transform: "translate(-50%, -50%)", - opacity: .7 - }}>Geen meldingen</h1> - </div> - } + <div + style={{ + overflowY: 'scroll', + whiteSpace: 'normal', + height: 450 - 24 * 4, + borderRadius: 6, + }} + > + {props.gameInvites?.map(game => <GameInvite hide={props.rerender} game={game} />)} + {props.friendRequests?.map(user => <FriendRequest hide={props.rerender} user={user} />)} + {messages == 0 + && <div + style={{ + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + top: 0, + }} + > + <h1 + style={{ + position: 'absolute', + top: '50%', + left: '50%', + whiteSpace: 'nowrap', + transform: 'translate(-50%, -50%)', + opacity: .7, + }} + > + Geen meldingen + </h1> + </div>} </div> - </Bubble> + </Bubble>; } var FriendRequestButtonStyle: CSSProperties = { borderRadius: 6, - display: "inline-block", + display: 'inline-block', marginLeft: 0, - textAlign: "center" + textAlign: 'center', }; function Acceptable(props: { @@ -89,78 +100,89 @@ function Acceptable(props: { onAccept?: () => void; onDeny?: () => void; }) { - return <Vierkant style={{ - borderRadius: 8, - background: "var(--background-alt)", - margin: 0, - padding: 12, - width: "100%", - marginBottom: 12 - }}> - <div style={{ position: "relative" }}> + return <Vierkant + style={{ + borderRadius: 8, + background: 'var(--background-alt)', + margin: 0, + padding: 12, + width: '100%', + marginBottom: 12, + }} + > + <div style={{ position: 'relative' }}> {props.children} - <div style={{ - display: "grid", - gridTemplateColumns: "1fr, 1fr", - gridGap: 12, - marginTop: 12, - gridAutoFlow: "column", - }}> + <div + style={{ + display: 'grid', + gridTemplateColumns: '1fr, 1fr', + gridGap: 12, + marginTop: 12, + gridAutoFlow: 'column', + }} + > <IconLabelButton onclick={props.onAccept} style={FriendRequestButtonStyle} - icon={<DoneIcon/>} - text="Accepteren"/> + icon={<DoneIcon />} + text='Accepteren' + /> <IconLabelButton onclick={props.onDeny} style={FriendRequestButtonStyle} - icon={<CloseIcon/>} - text="Verwijderen"/> + icon={<CloseIcon />} + text='Verwijderen' + /> </div> </div> - </Vierkant> + </Vierkant>; } function FriendRequest(props: { user: userInfo; hide: () => void; }) { - var [ gone, setGone ] = useState(false); + var [gone, setGone] = useState(false); var hide = () => { setGone(true); props.hide(); - } + }; - return !gone && <Acceptable onAccept={() => { - axios.request({ - method: "post", - url: "/api/social/accept", - headers: {"content-type": "application/json"}, - data: { "id": props.user?.id } - }) - .then(hide); - }} onDeny={() => { - axios.request({ - method: "post", - url: "/api/social/remove", - headers: {"content-type": "application/json"}, - data: { "id": props.user?.id } - }) - .then(hide); - }}> - <a href={"/user?id=" + props.user.id}> - <AccountAvatar size={48} id={props.user.id}/> - <div style={{ - display: "inline-block", - verticalAlign: "top", - marginLeft: 6 - }}> - <i style={{ display: "block" }}>Vriendschapsverzoek</i> + return !gone && <Acceptable + onAccept={() => { + axios.request({ + method: 'post', + url: '/api/social/accept', + headers: { 'content-type': 'application/json' }, + data: { 'id': props.user?.id }, + }) + .then(hide); + }} + onDeny={() => { + axios.request({ + method: 'post', + url: '/api/social/remove', + headers: { 'content-type': 'application/json' }, + data: { 'id': props.user?.id }, + }) + .then(hide); + }} + > + <a href={'/user?id=' + props.user.id}> + <AccountAvatar size={48} id={props.user.id} /> + <div + style={{ + display: 'inline-block', + verticalAlign: 'top', + marginLeft: 6, + }} + > + <i style={{ display: 'block' }}>Vriendschapsverzoek</i> <b>{props.user.username}</b> </div> </a> - </Acceptable> + </Acceptable>; } function GameInvite(props: { @@ -169,14 +191,20 @@ function GameInvite(props: { }) { return <Acceptable> <a> - <div style={{ - display: "inline-block", - verticalAlign: "top", - }}> - <i style={{ display: "block" }}>Partijuitnodiging</i> - <p><b><a href={"/user?id=" + props.game.opponent?.id}>{props.game.opponent?.username}</a></b> wil een potje 4 op een rij spelen!</p> + <div + style={{ + display: 'inline-block', + verticalAlign: 'top', + }} + > + <i style={{ display: 'block' }}>Partijuitnodiging</i> + <p> + <b> + <a href={'/user?id=' + props.game.opponent?.id}>{props.game.opponent?.username}</a> + </b>{" "} + wil een potje 4 op een rij spelen! + </p> </div> </a> - </Acceptable> + </Acceptable>; } - diff --git a/components/page.tsx b/components/page.tsx index d8a4a2b..506e2db 100644 --- a/components/page.tsx +++ b/components/page.tsx @@ -6,27 +6,39 @@ interface CenteredPageProps { style?: CSSProperties; } -export function CenteredPage (props: CenteredPageProps) { - return <div className="CenteredPageOuter" style={{ - maxWidth: props.width, - margin: "0 auto" - }}> - <div className="CenteredPageInner" style={{ - margin: "0 6px", - lineHeight: 0, - ...props.style - }}>{props.children}</div> +export function CenteredPage(props: CenteredPageProps) { + return <div + className='CenteredPageOuter' + style={{ + maxWidth: props.width, + margin: '0 auto', + }} + > + <div + className='CenteredPageInner' + style={{ + margin: '0 6px', + lineHeight: 0, + ...props.style, + }} + > + {props.children} + </div> </div>; } export class PageTitle extends Component { - render () { - return <h1 style={{ - color: "var(--text-alt)", - marginLeft: 6, - marginTop: 32, - marginBottom: 64, - fontSize: 25, - }}>{this.props.children}</h1>; + render() { + return <h1 + style={{ + color: 'var(--text-alt)', + marginLeft: 6, + marginTop: 32, + marginBottom: 64, + fontSize: 25, + }} + > + {this.props.children} + </h1>; } } diff --git a/components/preferencesContext.tsx b/components/preferencesContext.tsx index 1b58a4f..a169be6 100644 --- a/components/preferencesContext.tsx +++ b/components/preferencesContext.tsx @@ -1,43 +1,48 @@ -import { useState, useEffect, createContext, ReactNode } from 'react'; import axios from 'axios'; +import { createContext, ReactNode, useEffect, useState } from 'react'; import { userPreferences } from '../api/api'; function applyPreferences(preferences: userPreferences) { - if(typeof preferences === "undefined") return; - if(typeof preferences.darkMode !== "undefined") - document.getElementsByTagName("html")[0].classList[preferences.darkMode ? "add" : "remove"]("dark"); + if (typeof preferences === 'undefined') return; + if (typeof preferences.darkMode !== 'undefined') { + document.getElementsByTagName('html')[0].classList[preferences.darkMode ? 'add' : 'remove']('dark'); + } } -var PreferencesContext = createContext<{ preferences?: userPreferences; updatePreference?: (newPreference: userPreferences) => void }>({}); +var PreferencesContext = createContext< + { preferences?: userPreferences; updatePreference?: (newPreference: userPreferences) => void; } +>({}); -export function PreferencesContextWrapper(props: { children?: ReactNode }) { - var server = typeof window === "undefined"; - var loggedIn = !server && document.cookie.includes("token"); +export function PreferencesContextWrapper(props: { children?: ReactNode; }) { + var server = typeof window === 'undefined'; + var loggedIn = !server && document.cookie.includes('token'); var [preferences, setPreferences] = useState<userPreferences>(); - useEffect(() => {(async() => { - if (!loggedIn) return; + useEffect(() => { + (async () => { + if (!loggedIn) return; - var local_prefs = window.localStorage.getItem("preferences"); - if (local_prefs) { - var local_prefs_json = JSON.parse(local_prefs) as userPreferences; - setPreferences(local_prefs_json); - applyPreferences(local_prefs_json); - } + var local_prefs = window.localStorage.getItem('preferences'); + if (local_prefs) { + var local_prefs_json = JSON.parse(local_prefs) as userPreferences; + setPreferences(local_prefs_json); + applyPreferences(local_prefs_json); + } - if (!preferences) { - var preferencesReq = await axios.request<{ preferences: userPreferences; }>({ - method: "get", - url: `/api/user/preferences`, - headers: {"content-type": "application/json"} - }); + if (!preferences) { + var preferencesReq = await axios.request<{ preferences: userPreferences; }>({ + method: 'get', + url: `/api/user/preferences`, + headers: { 'content-type': 'application/json' }, + }); - window.localStorage.setItem("preferences", JSON.stringify(preferencesReq.data.preferences)); - setPreferences(preferencesReq.data.preferences); - } - })()}, []); + window.localStorage.setItem('preferences', JSON.stringify(preferencesReq.data.preferences)); + setPreferences(preferencesReq.data.preferences); + } + })(); + }, []); useEffect(() => applyPreferences(preferences), [preferences]); @@ -46,17 +51,16 @@ export function PreferencesContextWrapper(props: { children?: ReactNode }) { setPreferences(prefs); applyPreferences(prefs); axios.request({ - method: "post", + method: 'post', url: `/api/user/preferences`, - headers: {"content-type": "application/json"}, - data: { "newPreferences": prefs } + headers: { 'content-type': 'application/json' }, + data: { 'newPreferences': prefs }, }); } return <PreferencesContext.Provider value={{ preferences, updatePreference }}> {props.children} - </PreferencesContext.Provider> + </PreferencesContext.Provider>; } export default PreferencesContext; - diff --git a/components/recentGames.tsx b/components/recentGames.tsx index 150520c..988126f 100644 --- a/components/recentGames.tsx +++ b/components/recentGames.tsx @@ -1,79 +1,92 @@ -import { CSSProperties } from 'react'; import friendlyTime from 'friendly-time'; +import { CSSProperties } from 'react'; import { gameInfo } from '../api/api'; var LeftAlignedTableColumn: CSSProperties = { - textAlign: "left", - paddingLeft: 16 -} + textAlign: 'left', + paddingLeft: 16, +}; var RightAlignedTableColumn: CSSProperties = { - textAlign: "right", - paddingRight: 16 -} + textAlign: 'right', + paddingRight: 16, +}; -function GameOutcome(props: { game: gameInfo }) { +function GameOutcome(props: { game: gameInfo; }) { var gameStatus = (() => { return { - "resign": () => "Opgegeven", - "wait_for_opponent": () => "Aan het wachten op een tegenstander", - "in_progress": () => "Bezig", - "finished": () => { + 'resign': () => 'Opgegeven', + 'wait_for_opponent': () => 'Aan het wachten op een tegenstander', + 'in_progress': () => 'Bezig', + 'finished': () => { return { - "w": "Gewonnen", - "l": "Verloren", - "d": "Gelijkspel" - }[props.game.outcome] + 'w': 'Gewonnen', + 'l': 'Verloren', + 'd': 'Gelijkspel', + }[props.game.outcome]; }, }[props.game.status](); })(); var outcome = props.game.outcome; - return <td style={{ - color: - outcome == "w" ? "var(--disk-b-text)" : - outcome == "l" ? "var(--disk-a-text)" : - "var(--text)", - opacity: !["w", "l"].includes(outcome) ? 0.75 : 1.0 - }}>{gameStatus}</td> + return <td + style={{ + color: outcome == 'w' + ? 'var(--disk-b-text)' + : outcome == 'l' + ? 'var(--disk-a-text)' + : 'var(--text)', + opacity: !['w', 'l'].includes(outcome) ? 0.75 : 1.0, + }} + > + {gameStatus} + </td>; } -export default function RecentGames(props: { games?: Array<gameInfo> }) { +export default function RecentGames(props: { games?: Array<gameInfo>; }) { return <div> <h2>Recente partijen</h2> - { - props.games?.length > 0 ? - <table width="100%" style={{ marginTop: "16px", textAlign: "center" }}> + {props.games?.length > 0 + ? <table width='100%' style={{ marginTop: '16px', textAlign: 'center' }}> <tbody> <tr> - <th style={{ width: "50%" }}>Tegenstander</th> - <th style={{ width: "15%" }}>Uitkomst</th> - <th style={{ width: "15%" }}>Zetten</th> - <th style={{ width: "20%" }}>Datum</th> + <th style={{ width: '50%' }}>Tegenstander</th> + <th style={{ width: '15%' }}>Uitkomst</th> + <th style={{ width: '15%' }}>Zetten</th> + <th style={{ width: '20%' }}>Datum</th> </tr> - { - props.games?.map(game => <tr key={game.id}> - <td style={LeftAlignedTableColumn}> - <a href={"/user?id=" + game.opponent?.id} style={{ - fontWeight: 500 - }}>{game.opponent?.username}</a> - </td> - <GameOutcome game={game}/> - <td>{Math.max(0, game.moves.length -1)}</td> - <td style={RightAlignedTableColumn}>{(() => { - var timeCreated = new Date(game.created); - return friendlyTime(timeCreated); - })()}</td> - </tr>) - } + {props.games?.map(game => + <tr key={game.id}> + <td style={LeftAlignedTableColumn}> + <a + href={'/user?id=' + game.opponent?.id} + style={{ + fontWeight: 500, + }} + > + {game.opponent?.username} + </a> + </td> + <GameOutcome game={game} /> + <td>{Math.max(0, game.moves.length - 1)}</td> + <td style={RightAlignedTableColumn}> + {(() => { + var timeCreated = new Date(game.created); + return friendlyTime(timeCreated); + })()} + </td> + </tr> + )} </tbody> - </table> : - <h1 style={{ - textAlign: "center", - opacity: .6, - margin: "32px 64px" - }}>Deze gebruiker heeft nog geen partijen gespeeld</h1> - } - </div> + </table> + : <h1 + style={{ + textAlign: 'center', + opacity: .6, + margin: '32px 64px', + }} + > + Deze gebruiker heeft nog geen partijen gespeeld + </h1>} + </div>; } - diff --git a/components/socketContext.tsx b/components/socketContext.tsx index f493d73..435f4a7 100644 --- a/components/socketContext.tsx +++ b/components/socketContext.tsx @@ -1,12 +1,11 @@ -import { ReactNode, createContext } from 'react'; +import { createContext, ReactNode } from 'react'; import { io as socket, Socket } from 'socket.io-client'; -export var SocketContext = createContext<{ io?: Socket }>({}); -export function SocketContextWrapper(props: { children?: ReactNode }) { +export var SocketContext = createContext<{ io?: Socket; }>({}); +export function SocketContextWrapper(props: { children?: ReactNode; }) { var io = socket(); return <SocketContext.Provider value={{ io }}> - { props.children } - </SocketContext.Provider> + {props.children} + </SocketContext.Provider>; } - diff --git a/components/toast.tsx b/components/toast.tsx index 91e67f7..97e17e6 100644 --- a/components/toast.tsx +++ b/components/toast.tsx @@ -1,114 +1,138 @@ -import { CSSProperties, ReactNode, useState, createContext } from "react"; +import { createContext, CSSProperties, ReactNode, useState } from 'react'; import CloseIcon from '@material-ui/icons/Close'; function ToastArea(props: { - style?: CSSProperties - children?: ReactNode + style?: CSSProperties; + children?: ReactNode; rerender?: boolean; }) { - return <div id="ToastArea" style={{ - position: "fixed", - whiteSpace: "nowrap", - bottom: 12, - left: "50%", - transform: "translateX(-50%)", - zIndex: 1, - maxWidth: 600, - width: "calc(100% - 48px - 48px)", - margin: "0 24px", - ...props.style - }}>{props.children}</div> + return <div + id='ToastArea' + style={{ + position: 'fixed', + whiteSpace: 'nowrap', + bottom: 12, + left: '50%', + transform: 'translateX(-50%)', + zIndex: 1, + maxWidth: 600, + width: 'calc(100% - 48px - 48px)', + margin: '0 24px', + ...props.style, + }} + > + {props.children} + </div>; } function Toast(props: { - text?: string - description?: string - icon?: ReactNode - children?: ReactNode - type?: "normal"|"confirmation"|"error" - style?: CSSProperties + text?: string; + description?: string; + icon?: ReactNode; + children?: ReactNode; + type?: 'normal' | 'confirmation' | 'error'; + style?: CSSProperties; }) { var [visible, setVisibility] = useState(true); setTimeout(() => setVisibility(false), 10e3); - return visible && <div style={{ - padding: 0, - marginBottom: 12, - borderRadius: 6, - boxShadow: "0 8px 12px -4px #00000033", - color: - props.type === "confirmation" ? "var(--background)" : "var(--text)", - backgroundColor: - props.type === "normal" ? "var(--background)" : - props.type === "confirmation" ? "var(--disk-b)" : - props.type === "error" ? "var(--disk-a)" : "var(--background)", - ...props.style - }}> - { - props.children || - <div style={{ - lineHeight: 0, - padding: 12, - minHeight: props.description ? 36 : 24, - position: "relative" - }}> - <div style={{ - position: "absolute", - left: 12, - top: "50%", - transform: "translateY(-50%)" - }}>{props.icon}</div> - <div style={{ - userSelect: "none", - position: "absolute", - left: props.icon ? 48 : 12, - top: "50%", - transform: "translateY(-50%)" - }}> + return visible && <div + style={{ + padding: 0, + marginBottom: 12, + borderRadius: 6, + boxShadow: '0 8px 12px -4px #00000033', + color: props.type === 'confirmation' ? 'var(--background)' : 'var(--text)', + backgroundColor: props.type === 'normal' + ? 'var(--background)' + : props.type === 'confirmation' + ? 'var(--disk-b)' + : props.type === 'error' + ? 'var(--disk-a)' + : 'var(--background)', + ...props.style, + }} + > + {props.children + || <div + style={{ + lineHeight: 0, + padding: 12, + minHeight: props.description ? 36 : 24, + position: 'relative', + }} + > + <div + style={{ + position: 'absolute', + left: 12, + top: '50%', + transform: 'translateY(-50%)', + }} + > + {props.icon} + </div> + <div + style={{ + userSelect: 'none', + position: 'absolute', + left: props.icon ? 48 : 12, + top: '50%', + transform: 'translateY(-50%)', + }} + > <h2 style={{ fontSize: 16 }}>{props.text}</h2> <p>{props.description}</p> </div> - <div style={{ - cursor: "pointer", - position: "absolute", - right: 12, - top: "50%", - transform: "translateY(-50%)" - }} onClick={() => setVisibility(false)}> - <CloseIcon style={{ fontSize: 24 }}/> + <div + style={{ + cursor: 'pointer', + position: 'absolute', + right: 12, + top: '50%', + transform: 'translateY(-50%)', + }} + onClick={() => setVisibility(false)} + > + <CloseIcon style={{ fontSize: 24 }} /> </div> - </div> - } - </div> + </div>} + </div>; } export type toastSettings = { - message: string, - description?: string, - type: "confirmation"|"normal"|"error", - icon?: ReactNode -} + message: string; + description?: string; + type: 'confirmation' | 'normal' | 'error'; + icon?: ReactNode; +}; export type toastType = (settings: toastSettings) => void; -export var ToastContext = createContext<{ toast?: toastType }>({}); +export var ToastContext = createContext<{ toast?: toastType; }>({}); var toasts: Array<JSX.Element> = []; -export function ToastContextWrapper(props: { children?: ReactNode }) { +export function ToastContextWrapper(props: { children?: ReactNode; }) { var [dummyState, rerender] = useState(false); - return <ToastContext.Provider value={{ toast: options => { - toasts.push(<Toast - type={options.type} - text={options.message} - description={options.description} - icon={options.icon}/>); - rerender(!dummyState); - } }}> - { props.children } + return <ToastContext.Provider + value={{ + toast: options => { + toasts.push( + <Toast + type={options.type} + text={options.message} + description={options.description} + icon={options.icon} + />, + ); + rerender(!dummyState); + }, + }} + > + {props.children} <ToastArea rerender={dummyState}> {toasts} </ToastArea> - </ToastContext.Provider> + </ToastContext.Provider>; } - diff --git a/components/ui.tsx b/components/ui.tsx index 9d532f8..c3f950b 100644 --- a/components/ui.tsx +++ b/components/ui.tsx @@ -1,9 +1,9 @@ -import { Component, CSSProperties, ReactNode, useState, useEffect } from "react"; +import { Component, CSSProperties, ReactNode, useEffect, useState } from 'react'; -import SearchIcon from '@material-ui/icons/Search'; -import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import EditOutlinedIcon from '@material-ui/icons/EditOutlined'; +import SearchIcon from '@material-ui/icons/Search'; export function Vierkant(props: { href?: string; @@ -16,27 +16,31 @@ export function Vierkant(props: { fullwidth?: boolean; onclick?: () => void; }) { - return <a style={{ - padding: 24, - backgroundColor: "var(--background)", - borderRadius: 8, - color: "var(--text)", - margin: 6, // geen margin collapse = 12px marge - display: "inline-block", - position: "relative", - boxSizing: "border-box", - width: - props.width ? props.width : - props.fullwidth ? "calc(100% - 12px)" : - undefined, - height: props.height ? props.height : undefined, - ...props.style - }} - href={props.href} - className={props.className} - id={props.id} - onClick={props.onclick} - >{props.children}</a> + return <a + style={{ + padding: 24, + backgroundColor: 'var(--background)', + borderRadius: 8, + color: 'var(--text)', + margin: 6, // geen margin collapse = 12px marge + display: 'inline-block', + position: 'relative', + boxSizing: 'border-box', + width: props.width + ? props.width + : props.fullwidth + ? 'calc(100% - 12px)' + : undefined, + height: props.height ? props.height : undefined, + ...props.style, + }} + href={props.href} + className={props.className} + id={props.id} + onClick={props.onclick} + > + {props.children} + </a>; } export function Button(props: { @@ -47,27 +51,34 @@ export function Button(props: { onclick?: (() => void); id?: string; }) { - return <a onClick={props.onclick} href={props.href} id={props.id} style={{ - padding: props.text ? 8 : 16, - textAlign: props.text ? "center" : "left", - borderRadius: 8, - backgroundColor: "var(--disk-a)", - cursor: "pointer", - position: "relative", - textDecoration: "none", - display: "block", - userSelect: "none", - ...props.style - }}> - { - props.text ? - <span style={{ - fontWeight: 600, - userSelect: "none" - }}>{props.text}</span> - : undefined - } - { props.children } + return <a + onClick={props.onclick} + href={props.href} + id={props.id} + style={{ + padding: props.text ? 8 : 16, + textAlign: props.text ? 'center' : 'left', + borderRadius: 8, + backgroundColor: 'var(--disk-a)', + cursor: 'pointer', + position: 'relative', + textDecoration: 'none', + display: 'block', + userSelect: 'none', + ...props.style, + }} + > + {props.text + ? <span + style={{ + fontWeight: 600, + userSelect: 'none', + }} + > + {props.text} + </span> + : undefined} + {props.children} </a>; } @@ -78,23 +89,33 @@ export function IconLabelButton(props: { style?: CSSProperties; href?: string; }) { - return <Button onclick={props.onclick} href={props.href} style={{ - display: "inline-block", - verticalAlign: "top", - padding: 8, - float: "right", - marginLeft: 12, - ...props.style - }}> + return <Button + onclick={props.onclick} + href={props.href} + style={{ + display: 'inline-block', + verticalAlign: 'top', + padding: 8, + float: 'right', + marginLeft: 12, + ...props.style, + }} + > {props.icon} - <span style={{ - display: "inline-block", - verticalAlign: "top", - fontWeight: 500, - marginLeft: 8, - marginTop: 3, marginBottom: 3, marginRight: 3 - }}>{props.text}</span> - </Button> + <span + style={{ + display: 'inline-block', + verticalAlign: 'top', + fontWeight: 500, + marginLeft: 8, + marginTop: 3, + marginBottom: 3, + marginRight: 3, + }} + > + {props.text} + </span> + </Button>; } export function Input(props: { @@ -104,61 +125,73 @@ export function Input(props: { id?: string; min?: number; max?: number; - value?: string|number; + value?: string | number; dark?: boolean; autocomplete?: string; autofocus?: boolean; }) { return <input - id={props.id} - type={props.type || "text"} - min={props.min} max={props.max} - placeholder={props.label} - spellCheck={false} - defaultValue={props.value ? String(props.value) : ""} - className={props.dark ? "dark" : "light"} - autoComplete={props.autocomplete} - autoFocus={props.autofocus} - style={{ - padding: 12, - border: 0, - width: "calc(100% - 24px)", - fontSize: 14, - backgroundColor: "var(--page-background)", - color: "var(--text-alt)", - borderRadius: 8, - fontFamily: "Inter", - ...props.style - }}/> + id={props.id} + type={props.type || 'text'} + min={props.min} + max={props.max} + placeholder={props.label} + spellCheck={false} + defaultValue={props.value ? String(props.value) : ''} + className={props.dark ? 'dark' : 'light'} + autoComplete={props.autocomplete} + autoFocus={props.autofocus} + style={{ + padding: 12, + border: 0, + width: 'calc(100% - 24px)', + fontSize: 14, + backgroundColor: 'var(--page-background)', + color: 'var(--text-alt)', + borderRadius: 8, + fontFamily: 'Inter', + ...props.style, + }} + />; } -export function SearchBar(props: { label?: string }) { - return <div style={{ - marginTop: 24, - borderRadius: 8, - overflow: "hidden", - width: "100%" - }}> - <Input label={props.label} style={{ - width: "calc(100% - 24px - 41px)", - borderTopRightRadius: 0, - borderBottomRightRadius: 0 - }}/> - <div style={{ - width: 41, - height: 41, - backgroundColor: "var(--disk-a)", - display: "inline-block", - verticalAlign: "top", - position: "relative" - }}> - <SearchIcon style={{ - position: "absolute", - top: "50%", left: "50%", - transform: "translate(-50%, -50%)" - }}/> +export function SearchBar(props: { label?: string; }) { + return <div + style={{ + marginTop: 24, + borderRadius: 8, + overflow: 'hidden', + width: '100%', + }} + > + <Input + label={props.label} + style={{ + width: 'calc(100% - 24px - 41px)', + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }} + /> + <div + style={{ + width: 41, + height: 41, + backgroundColor: 'var(--disk-a)', + display: 'inline-block', + verticalAlign: 'top', + position: 'relative', + }} + > + <SearchIcon + style={{ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }} + /> </div> - </div> + </div>; } export function CheckBox(props: { @@ -173,25 +206,28 @@ export function CheckBox(props: { useEffect(() => { if (gotDefaultState) return; setOn(props.state); - if (typeof props.state !== "undefined") setGotDefaultState(true); + if (typeof props.state !== 'undefined') setGotDefaultState(true); }); var toggle = () => { setOn(!on); props.onclick && props.onclick(!on); - } + }; - return <div onClick={toggle} id={props.id} className={on ? "on" : "off"} style={{ - ...props.style, - display: "inline-block", - cursor: "pointer" - }}> - { - on ? - <CheckBoxIcon/> : - <CheckBoxOutlineBlankIcon/> - } - </div> + return <div + onClick={toggle} + id={props.id} + className={on ? 'on' : 'off'} + style={{ + ...props.style, + display: 'inline-block', + cursor: 'pointer', + }} + > + {on + ? <CheckBoxIcon /> + : <CheckBoxOutlineBlankIcon />} + </div>; } export class ColorPicker extends Component<{ @@ -201,44 +237,53 @@ export class ColorPicker extends Component<{ color: string; dark: boolean; } = { - color: "#012345", - dark: true - } + color: '#012345', + dark: true, + }; render() { - return <Button style={{ - display: "inline-block", - verticalAlign: "top", - padding: 6, - float: "right", - marginLeft: 12, - color: this.state.dark ? "var(--text)" : "var(--text-alt)", - borderColor: this.state.dark ? "var(--text)" : "var(--text-alt)", - borderWidth: 2, - borderStyle: "solid", - backgroundColor: this.state.color, - ...this.props.style - }}> + return <Button + style={{ + display: 'inline-block', + verticalAlign: 'top', + padding: 6, + float: 'right', + marginLeft: 12, + color: this.state.dark ? 'var(--text)' : 'var(--text-alt)', + borderColor: this.state.dark ? 'var(--text)' : 'var(--text-alt)', + borderWidth: 2, + borderStyle: 'solid', + backgroundColor: this.state.color, + ...this.props.style, + }} + > <div> - <EditOutlinedIcon/> - <div style={{ - width: 150, - height: 24, - display: "inline-block", - textAlign: "center", - verticalAlign: "top", - position: "relative" - }}> - <span style={{ - position: "absolute", - top: "50%", left: "50%", - transform: "translate(-50%, -50%)", - fontWeight: 600, - fontFeatureSettings: '"tnum", "ss01"' - }}>{this.state.color}</span> + <EditOutlinedIcon /> + <div + style={{ + width: 150, + height: 24, + display: 'inline-block', + textAlign: 'center', + verticalAlign: 'top', + position: 'relative', + }} + > + <span + style={{ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + fontWeight: 600, + fontFeatureSettings: '"tnum", "ss01"', + }} + > + {this.state.color} + </span> </div> </div> - </Button> + </Button>; } } @@ -246,12 +291,21 @@ export function Tuitje(props: { style?: CSSProperties; rotation?: number; }) { - return <svg width="36" height="12" viewBox="0 0 36 12" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ - ...props.style - }}> - <path d="M18 12C24 12 27 0 36 0L0 0C9 0 12 12 18 12Z" - fill={ props.style?.background as string || "var(--background)" }/> - </svg> + return <svg + width='36' + height='12' + viewBox='0 0 36 12' + fill='none' + xmlns='http://www.w3.org/2000/svg' + style={{ + ...props.style, + }} + > + <path + d='M18 12C24 12 27 0 36 0L0 0C9 0 12 12 18 12Z' + fill={props.style?.background as string || 'var(--background)'} + /> + </svg>; } export function Bubble(props: { @@ -259,24 +313,27 @@ export function Bubble(props: { style?: CSSProperties; tuitjeStyle?: CSSProperties; }) { - return <Vierkant style={{ - position: "absolute", - textAlign: "center", - margin: 0, - overflow: "visible", - left: "50%", - top: -24, - transform: "translateY(-100%) translateX(-50%)", - boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)", - ...props.style - }}> + return <Vierkant + style={{ + position: 'absolute', + textAlign: 'center', + margin: 0, + overflow: 'visible', + left: '50%', + top: -24, + transform: 'translateY(-100%) translateX(-50%)', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', + ...props.style, + }} + > {props.children} - <Tuitje style={{ - position: "absolute", - bottom: -12, - transform: "translate(-50%, 0%) rotate(0deg)", - ...props.tuitjeStyle - }}/> - </Vierkant> + <Tuitje + style={{ + position: 'absolute', + bottom: -12, + transform: 'translate(-50%, 0%) rotate(0deg)', + ...props.tuitjeStyle, + }} + /> + </Vierkant>; } - diff --git a/components/voerBord.tsx b/components/voerBord.tsx index 946aa9c..93e350c 100644 --- a/components/voerBord.tsx +++ b/components/voerBord.tsx @@ -1,10 +1,16 @@ function Disc() { - return <div className="disk" style={{ - position: "absolute", - top: 0, left: 0, right: 0, bottom: 0, - borderRadius: 999999, - margin: 3 - }}/> + return <div + className='disk' + style={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderRadius: 999999, + margin: 3, + }} + />; } export function VoerBord(props: { @@ -13,40 +19,53 @@ export function VoerBord(props: { onMove: (move: number) => void; active: boolean; }) { - return <table className="voerBord" style={{ - borderSpacing: 8, - width: "100%" - }}> + return <table + className='voerBord' + style={{ + borderSpacing: 8, + width: '100%', + }} + > <tbody> - { - [...Array(props.height).keys()].map((row) => ( - <tr key={`r-${row}`}> - {[...Array(props.width).keys()].map((column) => ( - <td style={{ - position: "relative", - width: "100%", - padding: 0 - }} key={`c-${row}x${column}`}> - <div style={{ - display: "block", - marginTop: "100%" - }}/> - <Disc/> - <div style={{ - position: "absolute", - top: 0, left: 0, right: 0, bottom: 0, + {[...Array(props.height).keys()].map((row) => ( + <tr key={`r-${row}`}> + {[...Array(props.width).keys()].map((column) => ( + <td + style={{ + position: 'relative', + width: '100%', + padding: 0, + }} + key={`c-${row}x${column}`} + > + <div + style={{ + display: 'block', + marginTop: '100%', + }} + /> + <Disc /> + <div + style={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, borderRadius: 6, - border: "2px solid var(--background-alt)", + border: '2px solid var(--background-alt)', opacity: .5, - cursor: props.active ? "pointer" : "default" - }} id={`pos-${(props.height - row - 1) * props.width + column}`} onClick={event => { - props.onMove(Number((event.target as HTMLElement).id.split("-")[1])) - }}/> - </td> - ))} - </tr> - )) - } + cursor: props.active ? 'pointer' : 'default', + }} + id={`pos-${(props.height - row - 1) * props.width + column}`} + onClick={event => { + props.onMove(Number((event.target as HTMLElement).id.split('-')[1])); + }} + /> + </td> + ))} + </tr> + ))} </tbody> - </table> + </table>; } |