diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/account.tsx | 4 | ||||
-rw-r--r-- | components/dialogBox.tsx | 29 | ||||
-rw-r--r-- | components/footer.tsx | 4 | ||||
-rw-r--r-- | components/gameBar.tsx | 92 | ||||
-rw-r--r-- | components/gameSettings.tsx | 372 | ||||
-rw-r--r-- | components/logo.tsx | 26 | ||||
-rw-r--r-- | components/navbar.tsx | 78 | ||||
-rw-r--r-- | components/notificationsArea.tsx | 103 | ||||
-rw-r--r-- | components/page.tsx | 34 | ||||
-rw-r--r-- | components/recentGames.tsx | 56 | ||||
-rw-r--r-- | components/toast.tsx | 75 | ||||
-rw-r--r-- | components/ui.tsx | 239 | ||||
-rw-r--r-- | components/voerBord.tsx | 50 |
13 files changed, 275 insertions, 887 deletions
diff --git a/components/account.tsx b/components/account.tsx index f24135f..cb44346 100644 --- a/components/account.tsx +++ b/components/account.tsx @@ -19,14 +19,12 @@ export function AccountAvatar(props: { if (props.dummy) image = dummy; return <div + className={'accountAvatar dispinbl ' + (props.round ? 'round' : '')} 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 7abbded..a5b02fa 100644 --- a/components/dialogBox.tsx +++ b/components/dialogBox.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, ReactNode } from 'react'; +import { ReactNode } from 'react'; import { Vierkant } from './ui'; @@ -7,32 +7,17 @@ import CancelIcon from '@material-ui/icons/Cancel'; export function DialogBox(props: { children: ReactNode; title: string; - style?: CSSProperties; onclick?: () => void; + hidden?: boolean; + className?: string; }) { return <Vierkant - style={{ - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - boxShadow: '0 8px 32px -5px #0007', - width: 392, - ...props.style, - }} + className={'dialogbox bg-800 drop-2 pad-l posfix abscenter ' + (props.hidden ? 'dispnone' : '') + ' ' + + props.className} > - <h2 style={{ marginBottom: 24 }}>{props.title}</h2> + <h2 className='title'>{props.title}</h2> <span onClick={props.onclick}> - <CancelIcon - style={{ - position: 'absolute', - top: 25, - right: 25, - color: 'var(--text)', - opacity: .85, - cursor: 'pointer', - }} - /> + <CancelIcon className='posabs close icon subtile' /> </span> {props.children} </Vierkant>; diff --git a/components/footer.tsx b/components/footer.tsx index 36ebef3..f39946c 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { LogoDark } from '../components/logo'; +import Logo from '../components/logo'; import ExitToAppOutlinedIcon from '@material-ui/icons/ExitToAppOutlined'; import ExtensionIcon from '@material-ui/icons/Extension'; @@ -25,7 +25,7 @@ function PageLink(props: { export function Footer() { return <div className='footer'> <div className='header'> - <LogoDark /> + <Logo /> <h2>4 op een rij</h2> </div> <div className='content'> diff --git a/components/gameBar.tsx b/components/gameBar.tsx index 0d7d4d9..9a7ca59 100644 --- a/components/gameBar.tsx +++ b/components/gameBar.tsx @@ -1,5 +1,5 @@ -import { CSSProperties, ReactNode } from 'react'; -import { Bubble, Vierkant } from './ui'; +import { ReactNode } from 'react'; +import { Vierkant } from './ui'; import ExitToAppRoundedIcon from '@material-ui/icons/ExitToAppRounded'; import NavigateBeforeRoundedIcon from '@material-ui/icons/NavigateBeforeRounded'; @@ -9,61 +9,30 @@ import SettingsRoundedIcon from '@material-ui/icons/SettingsRounded'; function GameBarModule(props: { children?: ReactNode; onclick?: () => void; + className?: string; }) { return <Vierkant - style={{ - backgroundColor: 'var(--background-alt)', - padding: 12, - borderRadius: 6, - margin: 0, - verticalAlign: 'top', - cursor: props.onclick ? 'pointer' : 'default', - }} + className={'gameBarButton bg-700 pad-m round-t valigntop ' + props.className + ' ' + + (props.onclick ? 'cpointer' : '')} onclick={props.onclick} > {props.children} </Vierkant>; } -var GameBarSpacer = () => <div style={{ width: 8, display: 'inline-block' }}></div>; - -var GameBarAlignStyle: CSSProperties = { - display: 'inline-block', -}; - export function GameBar(props: { turn: boolean; player1: boolean; 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', - }} - > + return <Vierkant className='gameBar bg-800 w100m2m pad-s'> + <div> + <div className='floatl dispinbl'> + {props.active && <div + className={'move dispinbl ' + (props.turn ? 'move-a' : 'move-b')} + />} + <h2 className='pad-m valigntop dispinbl'> {!props.active ? 'Wachten op tegenstander...' : props.turn == props.player1 @@ -71,49 +40,22 @@ export function GameBar(props: { : '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 className='score winning dispnone subtile posabs abscenter'> + <span>0-0</span> </div> - <div style={{ ...GameBarAlignStyle, float: 'right' }}> + <div className='buttons floatr dispinbl'> <GameBarModule> <SettingsRoundedIcon /> </GameBarModule> - <GameBarSpacer /> - <GameBarModule> - <span - style={{ - margin: '0 4px', - fontSize: 20, - }} - > - 00:00 - </span> + <GameBarModule className='timer nosel'> + <span>00:00</span> </GameBarModule> - <GameBarSpacer /> <GameBarModule onclick={props.resignFunction}> <ExitToAppRoundedIcon /> </GameBarModule> - <GameBarSpacer /> <GameBarModule> <NavigateBeforeRoundedIcon /> </GameBarModule> - <GameBarSpacer /> <GameBarModule> <NavigateNextRoundedIcon /> </GameBarModule> diff --git a/components/gameSettings.tsx b/components/gameSettings.tsx index f562e5d..e2c60e6 100644 --- a/components/gameSettings.tsx +++ b/components/gameSettings.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Component, CSSProperties, ReactNode } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { ruleset, userPreferences } from '../api/api'; import { DialogBox } from './dialogBox'; @@ -7,106 +7,57 @@ 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 = { - editGameRulesDialogVisible: false, - ruleset: { - timelimit: { - enabled: false, - shared: false, - }, - ranked: false, +export function CurrentGameSettings() { + var [editGameRulesDialogVisible, setEditGameRulesDialogVisible] = useState(false); + var [ruleset, setRuleset] = useState<ruleset>({ + timelimit: { + enabled: false, + shared: false, + minutes: 0, + seconds: 0, + addmove: 0, }, - }; - - constructor(props: {}) { - super(props); + ranked: false, + }); - if (typeof window === 'undefined') return; // return if run on server - - axios.request<userPreferences>({ + useEffect(() => { + axios.request<{ preferences: userPreferences; }>({ method: 'get', url: `/api/user/preferences`, 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 })) + .then(request => setRuleset(request.data.preferences.ruleset)) .catch(() => {}); - } - - showEditGameRules = () => this.setState({ editGameRulesDialogVisible: true }); - hideEditGameRules = () => this.setState({ editGameRulesDialogVisible: false }); - 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, - }} + var timelimit_str = ruleset.timelimit.enabled + ? `${ruleset.timelimit.minutes}m${ruleset.timelimit.seconds}s plus ${ruleset.timelimit.addmove}` + : 'Geen tijdslimiet'; + var ranked_str = ruleset.ranked + ? 'Gerangschikt' + : 'Niet gerangschikt'; + return <div className='editGameSettings posrel'> + <p className='currentRules subtile nosel posabs l0'> + {timelimit_str} + <br /> + {ranked_str} + </p> + <Button + className='posabs r0 pad-m' + onclick={() => setEditGameRulesDialogVisible(true)} > - <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> - <EditGameSettings - parentState={this.state} - hideEditGameRules={this.hideEditGameRules} - setGameRules={this.setGameRules} - /> - </div>; - } + <BuildOutlinedIcon className='icon' /> + <span className='posabs center nosel text'> + Spelregels aanpassen + </span> + </Button> + <EditGameSettings + hideEditGameRules={() => setEditGameRulesDialogVisible(false)} + setGameRules={setRuleset} + ruleset={ruleset} + visible={editGameRulesDialogVisible} + /> + </div>; } function GameSettingsSection(props: { @@ -118,32 +69,17 @@ function GameSettingsSection(props: { }) { return <Vierkant id={props.id} - style={{ - backgroundColor: 'var(--background-alt)', - width: '100%', - padding: 16, - margin: 0, - marginBottom: props.noMarginBottom ? 0 : 24, - }} + className='pad-m editableRulesSection' > - <span - style={{ - verticalAlign: 'top', - fontSize: 14, - fontWeight: 600, - }} - > + <span className='valigntop nosel'> {props.title} </span> - <CheckBox - state={props.state} - id={`${props.id}_enabled`} - style={{ - verticalAlign: 'top', - float: 'right', - margin: -3, - }} - /> + <div className='checkboxWrapper valigntop floatr'> + <CheckBox + state={props.state} + id={props.id + '_enabled'} + /> + </div> <div>{props.children}</div> </Vierkant>; } @@ -151,137 +87,99 @@ function GameSettingsSection(props: { function GameRule(props: { title: string; 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 className='gamerule pad-m round-t center bg-900'> + <h1>{props.title}</h1> + <p>{props.description}</p> </div>; } type editGameSettingsProps = { - parentState: CurrentGameSettingsStateType; + visible: boolean; + ruleset: ruleset; hideEditGameRules: () => void; setGameRules: (newRules: ruleset) => void; }; -export class EditGameSettings extends Component<editGameSettingsProps> { - render() { - return <DialogBox - title='Spelregels aanpassen' - style={{ - margin: 0, - display: this.props.parentState.editGameRulesDialogVisible ? 'block' : 'none', +export function EditGameSettings(props: editGameSettingsProps) { + return <DialogBox + title='Spelregels aanpassen' + hidden={!props.visible} + onclick={props.hideEditGameRules} + className='gameRuleEdit' + > + <div className='editableRules round-t'> + <GameSettingsSection + title='Tijdslimiet' + state={props.ruleset.timelimit.enabled} + id='timelimit' + > + <div className='sidebyside timeControls'> + <Input + id='timelimit_minutes' + type='number' + label='min' + min={0} + max={60} + value={props.ruleset.timelimit.minutes} + className='pad-m round-t' + /> + <Input + id='timelimit_seconds' + type='number' + label='sec' + min={0} + max={60} + value={props.ruleset.timelimit.seconds} + className='pad-m round-t' + /> + <Input + id='timelimit_addmove' + type='number' + label='plus' + min={0} + value={props.ruleset.timelimit.addmove} + className='pad-m round-t' + /> + </div> + <CheckBox id='timelimit_shared' state={props.ruleset.timelimit.shared} /> + <span className='valignsup'> + Timer gebruiken voor bijde spelers + </span> + </GameSettingsSection> + {false && <GameSettingsSection id='gamemodes' title='Regelset' state={false}> + <div className='sidebyside'> + <GameRule title='+2' description='Extra kolommen' /> + <GameRule title='+4' description='Extra kolommen' /> + </div> + <GameRule 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={props.ruleset.ranked} + id='ranked' + noMarginBottom + /> + </div> + <Button + className='save' + 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'), + }; + props.setGameRules(rules); + props.hideEditGameRules(); }} - 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> - </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' /> - </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 - /> - </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> - </DialogBox>; - } + Instellingen opslaan + </Button> + </DialogBox>; } diff --git a/components/logo.tsx b/components/logo.tsx index e43aa88..df35370 100644 --- a/components/logo.tsx +++ b/components/logo.tsx @@ -1,26 +1,12 @@ -export function LogoDark() { +export default function Logo() { 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' /> - </svg> - </div> - ); -} - -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' /> + <rect width='24' height='24' fill='none' /> + <circle className='green' cx='6.5' cy='6.5' r='4.5' fill='var(--confirm)' /> + <circle className='red' cx='6.5' cy='17.5' r='4.5' fill='var(--error)' /> + <circle className='green' cx='17.5' cy='17.5' r='4.5' fill='var(--confirm)' /> + <circle className='circle' cx='17.5' cy='6.5' r='3.5' stroke='var(--foreground)' strokeWidth='2' /> </svg> </div> ); diff --git a/components/navbar.tsx b/components/navbar.tsx index 70de574..5ce43bb 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -1,8 +1,8 @@ import axios from 'axios'; -import { CSSProperties, useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { userInfo } from '../api/api'; -import { LogoDark } from '../components/logo'; +import Logo from '../components/logo'; import { AccountAvatar } from './account'; import { NotificationsArea } from './notificationsArea'; import { SocketContext } from './socketContext'; @@ -15,12 +15,6 @@ import SearchIcon from '@material-ui/icons/Search'; import SettingsIcon from '@material-ui/icons/Settings'; import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; -var NavBarItemStyle: CSSProperties = { - margin: 12, - marginBottom: 16, - display: 'block', -}; - export function NavBar() { var [loggedIn, setLoggedIn] = useState(false); var [gotData, setGotData] = useState(false); @@ -60,73 +54,31 @@ export function NavBar() { })(); }, []); - return <div - className='navbar' - style={{ - width: 48, - height: '100%', - - lineHeight: 0, - - backgroundColor: 'var(--background)', - display: 'inline-block', - - position: 'fixed', - top: 0, - left: 0, - - overflow: 'visible', - whiteSpace: 'nowrap', - zIndex: 2, - }} - > - <div style={NavBarItemStyle}> - <LogoDark /> + return <div className='navbar bg-800 h100vh dispinbl t0 l0 posfix'> + <div className='item'> + <Logo /> </div> - <a href='/' style={NavBarItemStyle}> + <a href='/' className='item'> <Home /> </a> - <a href='/game' style={NavBarItemStyle}> + <a href='/game' className='item'> <VideogameAssetIcon /> </a> - {false && <a href='/' style={NavBarItemStyle}> + {false && <a href='/' className='item'> <ExtensionIcon /> </a>} - <a href='/search' style={NavBarItemStyle}> + <a href='/search' className='item'> <SearchIcon /> </a> - <div - style={{ - position: 'absolute', - bottom: -4, - left: 0, - backgroundColor: 'var(--background)', - }} - > - {loggedIn && <a - style={{ - overflow: 'visible', - position: 'relative', - ...NavBarItemStyle, - }} - > + <div className='bg-800 bottomArea'> + {loggedIn && <a className='notifications item posrel'> <div - style={{ cursor: 'pointer' }} + className='iconWrapper' onClick={() => setNotificationsAreaVisible(!notificationsAreaVisible)} > <NotificationsIcon /> - {gotNotifications && <div - style={{ - backgroundColor: 'var(--disk-a)', - width: 8, - height: 8, - borderRadius: 4, - position: 'absolute', - top: 2, - right: 2, - }} - />} + {gotNotifications && <div className='notificationDot posabs' />} </div> <NotificationsArea visible={notificationsAreaVisible} @@ -134,12 +86,12 @@ export function NavBar() { rerender={getNotifications} /> </a>} - <a href={loggedIn ? '/user' : '/login'} style={NavBarItemStyle}> + <a href={loggedIn ? '/user' : '/login'} className='item'> {loggedIn ? <AccountAvatar size={24} round /> : <PersonIcon />} </a> - {loggedIn && <a href='/settings' style={NavBarItemStyle}> + {loggedIn && <a href='/settings' className='item'> <SettingsIcon /> </a>} </div> diff --git a/components/notificationsArea.tsx b/components/notificationsArea.tsx index 9573b72..8ee554a 100644 --- a/components/notificationsArea.tsx +++ b/components/notificationsArea.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { CSSProperties, ReactNode, useContext, useEffect, useState } from 'react'; +import { ReactNode, useContext, useEffect, useState } from 'react'; import { gameInfo, userInfo } from '../api/api'; import { AccountAvatar } from './account'; @@ -35,52 +35,14 @@ export function NotificationsArea(props: { 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)', - }} - > - <h2 style={{ marginBottom: 24 }}>Meldingen</h2> - <div - style={{ - overflowY: 'scroll', - whiteSpace: 'normal', - height: 450 - 24 * 4, - borderRadius: 6, - }} - > + return props.visible && <Bubble className='notificationsArea bg-700 pad-l'> + <h2 className='title'>Meldingen</h2> + <div className='inner round-t'> {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, - }} - > + && <div className='noMsgsWrapper posabs a0'> + <h1 className='posabs abscenter subtile'> Geen meldingen </h1> </div>} @@ -88,48 +50,24 @@ export function NotificationsArea(props: { </Bubble>; } -var FriendRequestButtonStyle: CSSProperties = { - borderRadius: 6, - display: 'inline-block', - marginLeft: 0, - textAlign: 'center', -}; - function Acceptable(props: { children?: ReactNode; 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 className='acceptable bg-800 round-t pad-m fullwidth'> + <div className='posrel'> {props.children} - <div - style={{ - display: 'grid', - gridTemplateColumns: '1fr, 1fr', - gridGap: 12, - marginTop: 12, - gridAutoFlow: 'column', - }} - > + <div className='sidebyside buttons'> <IconLabelButton + className='accept' onclick={props.onAccept} - style={FriendRequestButtonStyle} icon={<DoneIcon />} text='Accepteren' /> <IconLabelButton + className='deny' onclick={props.onDeny} - style={FriendRequestButtonStyle} icon={<CloseIcon />} text='Verwijderen' /> @@ -171,14 +109,8 @@ function FriendRequest(props: { > <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> + <div className='userInfo dispinbl valigntop'> + <i className='dispbl'>Vriendschapsverzoek</i> <b>{props.user.username}</b> </div> </a> @@ -191,13 +123,8 @@ function GameInvite(props: { }) { return <Acceptable> <a> - <div - style={{ - display: 'inline-block', - verticalAlign: 'top', - }} - > - <i style={{ display: 'block' }}>Partijuitnodiging</i> + <div className='userInfo dispinbl valigntop'> + <i className='dispbl'>Partijuitnodiging</i> <p> <b> <a href={'/user?id=' + props.game.opponent?.id}>{props.game.opponent?.username}</a> diff --git a/components/page.tsx b/components/page.tsx index 506e2db..b8e8770 100644 --- a/components/page.tsx +++ b/components/page.tsx @@ -1,27 +1,15 @@ -import { Component, CSSProperties, ReactNode } from 'react'; +import { Component, ReactNode } from 'react'; -interface CenteredPageProps { +export function CenteredPage(props: { width?: number; children?: ReactNode; - style?: CSSProperties; -} - -export function CenteredPage(props: CenteredPageProps) { + className?: string; +}) { return <div className='CenteredPageOuter' - style={{ - maxWidth: props.width, - margin: '0 auto', - }} + style={{ maxWidth: props.width }} > - <div - className='CenteredPageInner' - style={{ - margin: '0 6px', - lineHeight: 0, - ...props.style, - }} - > + <div className={'CenteredPageInner ' + props.className}> {props.children} </div> </div>; @@ -29,15 +17,7 @@ export function CenteredPage(props: CenteredPageProps) { export class PageTitle extends Component { render() { - return <h1 - style={{ - color: 'var(--text-alt)', - marginLeft: 6, - marginTop: 32, - marginBottom: 64, - fontSize: 25, - }} - > + return <h1 className='pageTitle'> {this.props.children} </h1>; } diff --git a/components/recentGames.tsx b/components/recentGames.tsx index 988126f..6683e20 100644 --- a/components/recentGames.tsx +++ b/components/recentGames.tsx @@ -1,18 +1,7 @@ import friendlyTime from 'friendly-time'; -import { CSSProperties } from 'react'; import { gameInfo } from '../api/api'; -var LeftAlignedTableColumn: CSSProperties = { - textAlign: 'left', - paddingLeft: 16, -}; - -var RightAlignedTableColumn: CSSProperties = { - textAlign: 'right', - paddingRight: 16, -}; - function GameOutcome(props: { game: gameInfo; }) { var gameStatus = (() => { return { @@ -28,26 +17,24 @@ function GameOutcome(props: { game: gameInfo; }) { }, }[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} + return <td> + <span + className={'outcome ' + { + 'w': 'win', + 'l': 'lose', + 'd': 'draw', + }[props.game.outcome]} + > + {gameStatus} + </span> </td>; } export default function RecentGames(props: { games?: Array<gameInfo>; }) { - return <div> + return <div className='recentGames'> <h2>Recente partijen</h2> {props.games?.length > 0 - ? <table width='100%' style={{ marginTop: '16px', textAlign: 'center' }}> + ? <table width='100%' className='recentGames center'> <tbody> <tr> <th style={{ width: '50%' }}>Tegenstander</th> @@ -57,19 +44,14 @@ export default function RecentGames(props: { games?: Array<gameInfo>; }) { </tr> {props.games?.map(game => <tr key={game.id}> - <td style={LeftAlignedTableColumn}> - <a - href={'/user?id=' + game.opponent?.id} - style={{ - fontWeight: 500, - }} - > + <td className='leftAlignedColumn'> + <a href={'/user?id=' + game.opponent?.id}> {game.opponent?.username} </a> </td> <GameOutcome game={game} /> <td>{Math.max(0, game.moves.length - 1)}</td> - <td style={RightAlignedTableColumn}> + <td className='rightAlignedColumn'> {(() => { var timeCreated = new Date(game.created); return friendlyTime(timeCreated); @@ -79,13 +61,7 @@ export default function RecentGames(props: { games?: Array<gameInfo>; }) { )} </tbody> </table> - : <h1 - style={{ - textAlign: 'center', - opacity: .6, - margin: '32px 64px', - }} - > + : <h1 className='nogames center subtile'> Deze gebruiker heeft nog geen partijen gespeeld </h1>} </div>; diff --git a/components/toast.tsx b/components/toast.tsx index 97e17e6..3a72ae1 100644 --- a/components/toast.tsx +++ b/components/toast.tsx @@ -1,26 +1,14 @@ -import { createContext, CSSProperties, ReactNode, useState } from 'react'; +import { createContext, ReactNode, useState } from 'react'; import CloseIcon from '@material-ui/icons/Close'; function ToastArea(props: { - 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, - }} + className='posfix abscenterh' > {props.children} </div>; @@ -32,71 +20,30 @@ function Toast(props: { 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, - }} - > + return visible && <div className={'round-t drop-1 toast ' + props.type}> {props.children || <div - style={{ - lineHeight: 0, - padding: 12, - minHeight: props.description ? 36 : 24, - position: 'relative', - }} + className={'inner pad-m posrel ' + + (props.description ? 'hasDescription' : '') + ' ' + + (props.icon ? 'hasIcon' : '')} > - <div - style={{ - position: 'absolute', - left: 12, - top: '50%', - transform: 'translateY(-50%)', - }} - > + <div className='icon posabs abscenterv'> {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> + <div className='content nosel posabs abscenterv'> + <h2>{props.text}</h2> <p>{props.description}</p> </div> <div - style={{ - cursor: 'pointer', - position: 'absolute', - right: 12, - top: '50%', - transform: 'translateY(-50%)', - }} + className='closeIcon posabs abscenterv' onClick={() => setVisibility(false)} > - <CloseIcon style={{ fontSize: 24 }} /> + <CloseIcon /> </div> </div>} </div>; diff --git a/components/ui.tsx b/components/ui.tsx index c3f950b..7474240 100644 --- a/components/ui.tsx +++ b/components/ui.tsx @@ -1,4 +1,4 @@ -import { Component, CSSProperties, ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; @@ -9,33 +9,14 @@ export function Vierkant(props: { href?: string; width?: string; height?: string; - style?: CSSProperties; children?: ReactNode; className?: string; id?: string; - 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} + className={['round-l', 'vierkant', props.className].join(' ')} id={props.id} onClick={props.onclick} > @@ -46,35 +27,19 @@ export function Vierkant(props: { export function Button(props: { text?: string; children?: ReactNode; - style?: CSSProperties; href?: string; - onclick?: (() => void); + className?: string; + 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, - }} + className={'button pad-s round-t ' + props.className} > {props.text - ? <span - style={{ - fontWeight: 600, - userSelect: 'none', - }} - > + ? <span> {props.text} </span> : undefined} @@ -86,33 +51,18 @@ export function IconLabelButton(props: { text: string; icon: ReactNode; onclick?: () => void; - style?: CSSProperties; href?: string; + className?: string; }) { return <Button onclick={props.onclick} href={props.href} - style={{ - display: 'inline-block', - verticalAlign: 'top', - padding: 8, - float: 'right', - marginLeft: 12, - ...props.style, - }} + className={'iconlabel dispinbl valigntop floatr' + ' ' + props.className} > - {props.icon} - <span - style={{ - display: 'inline-block', - verticalAlign: 'top', - fontWeight: 500, - marginLeft: 8, - marginTop: 3, - marginBottom: 3, - marginRight: 3, - }} - > + <div className='dispinbl icon'> + {props.icon} + </div> + <span className='dispinbl valigntop label'> {props.text} </span> </Button>; @@ -120,7 +70,6 @@ export function IconLabelButton(props: { export function Input(props: { label?: string; - style?: CSSProperties; type?: string; id?: string; min?: number; @@ -129,6 +78,7 @@ export function Input(props: { dark?: boolean; autocomplete?: string; autofocus?: boolean; + className?: string; }) { return <input id={props.id} @@ -138,65 +88,26 @@ export function Input(props: { placeholder={props.label} spellCheck={false} defaultValue={props.value ? String(props.value) : ''} - className={props.dark ? 'dark' : 'light'} + className={'input' + ' ' + (props.dark ? 'dark' : 'light') + ' ' + props.className} 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%', - }} - > + return <div className='searchBar round-t fullwidth'> <Input label={props.label} - style={{ - width: 'calc(100% - 24px - 41px)', - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }} + className='pad-m bg-700' /> - <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> + <Button className='dispinbl valigntop'> + <SearchIcon className='icon' /> + </Button> </div>; } export function CheckBox(props: { state?: boolean; - style?: CSSProperties; id?: string; onclick?: (state: boolean) => void; }) { @@ -217,12 +128,7 @@ export function CheckBox(props: { return <div onClick={toggle} id={props.id} - className={on ? 'on' : 'off'} - style={{ - ...props.style, - display: 'inline-block', - cursor: 'pointer', - }} + className={'checkbox dispinbl ' + (on ? 'on' : 'off')} > {on ? <CheckBoxIcon /> @@ -230,110 +136,41 @@ export function CheckBox(props: { </div>; } -export class ColorPicker extends Component<{ - style?: CSSProperties; -}> { - state: { - color: string; - dark: boolean; - } = { - color: '#012345', - dark: true, - }; +export function ColorPicker() { + var [dark, setDark] = useState(false); + var [color, setColor] = useState('#012345'); - 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, - }} - > - <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> - </div> + return <Button className='colorpicker dispinbl valigntop pad-s floatr'> + <div> + <EditOutlinedIcon /> + <div className='labelwrapper valigntop dispinbl center posrel'> + <span className='label posabs'> + {color} + </span> </div> - </Button>; - } + </div> + </Button>; } -export function Tuitje(props: { - style?: CSSProperties; - rotation?: number; -}) { +export function Tuitje() { return <svg width='36' height='12' viewBox='0 0 36 12' fill='none' xmlns='http://www.w3.org/2000/svg' - style={{ - ...props.style, - }} + className='tuitje posabs' > - <path - d='M18 12C24 12 27 0 36 0L0 0C9 0 12 12 18 12Z' - fill={props.style?.background as string || 'var(--background)'} - /> + <path d='M18 12C24 12 27 0 36 0L0 0C9 0 12 12 18 12Z' /> </svg>; } export function Bubble(props: { children?: ReactNode; - style?: CSSProperties; - tuitjeStyle?: CSSProperties; + className?: string; }) { - 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 className={'bubble posabs center drop-2 ' + props.className}> {props.children} - <Tuitje - style={{ - position: 'absolute', - bottom: -12, - transform: 'translate(-50%, 0%) rotate(0deg)', - ...props.tuitjeStyle, - }} - /> + <Tuitje /> </Vierkant>; } diff --git a/components/voerBord.tsx b/components/voerBord.tsx index 93e350c..ffb291b 100644 --- a/components/voerBord.tsx +++ b/components/voerBord.tsx @@ -1,62 +1,22 @@ -function Disc() { - return <div - className='disk' - style={{ - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - borderRadius: 999999, - margin: 3, - }} - />; -} - export function VoerBord(props: { width: number; height: number; onMove: (move: number) => void; active: boolean; }) { - return <table - className='voerBord' - style={{ - borderSpacing: 8, - width: '100%', - }} - > + return <table className={'voerBord fullwidth ' + (props.active ? 'active' : '')}> <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, - }} + className='posrel outer cell fullwidth' key={`c-${row}x${column}`} > + <div className='dispbl square' /> + <div className='disk posabs a0' /> <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)', - opacity: .5, - cursor: props.active ? 'pointer' : 'default', - }} + className='posabs a0 round-t inner cell' id={`pos-${(props.height - row - 1) * props.width + column}`} onClick={event => { props.onMove(Number((event.target as HTMLElement).id.split('-')[1])); |