diff options
author | Loek Le Blansch <32883851+lonkaars@users.noreply.github.com> | 2021-04-21 10:40:52 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-21 10:40:52 +0200 |
commit | dadc722875b2095bd3d6c4ab628a644197b85f7b (patch) | |
tree | 9e061708fad5bfdcc40f4c40662d77fbc42cfe64 | |
parent | c603cb79e7ba7fdbb101a506e36f6d8d70b3a8f4 (diff) | |
parent | 5cb39d822716c650e520c3855ef049ff308a348c (diff) |
Merge pull request #12 from lonkaars/css-files
big redesign css move thing
45 files changed, 1689 insertions, 1637 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])); diff --git a/package.json b/package.json index df620cc..75fa8b1 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "devDependencies": { "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", - "@types/react-router-dom": "^5.1.7" + "@types/react-router-dom": "^5.1.7", + "typescript-plugin-css-modules": "^3.2.0" } } diff --git a/pages/_app.tsx b/pages/_app.tsx index 0682a4d..c1347e8 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,6 +7,20 @@ import '../styles/dark.css'; import '../styles/disk.css'; import '../styles/footer.css'; import '../styles/global.css'; +import '../styles/navbar.css'; +import '../styles/notifications.css'; +import '../styles/recentGames.css'; +import '../styles/toast.css'; +import '../styles/ui.css'; +import '../styles/utility.css'; + +import '../styles/game.css'; +import '../styles/gameSettings.css'; +import '../styles/index.css'; +import '../styles/loginregister.css'; +import '../styles/search.css'; +import '../styles/settings.css'; +import '../styles/user.css'; export default function VierOpEenRijWebsite({ Component, pageProps }) { return <div> diff --git a/pages/game.tsx b/pages/game.tsx index b5200a7..de2c089 100644 --- a/pages/game.tsx +++ b/pages/game.tsx @@ -1,7 +1,7 @@ import Icon from '@mdi/react'; import axios from 'axios'; import copy from 'copy-to-clipboard'; -import { CSSProperties, useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import * as cookies from 'react-cookies'; import { Socket } from 'socket.io-client'; import { SocketContext } from '../components/socketContext'; @@ -65,15 +65,7 @@ function VoerGame(props: { }); }, []); - return <div - style={{ - position: 'relative', - top: '50%', - transform: 'translateY(-50%)', - maxWidth: '100vh', - margin: '0 auto', - }} - > + return <div className='voerGame posrel abscenterv'> <VoerBord width={width} height={height} @@ -109,30 +101,22 @@ function GameOutcomeDialog(props: { }) { return <DialogBox title='Speluitkomst' - style={{ display: props.visible ? 'inline-block' : 'none' }} + hidden={!props.visible} + className='outcomeDialog' onclick={() => { window.history.replaceState(null, null, '/'); window.location.reload(); }} > - <div - style={{ - width: '100%', - textAlign: 'center', - }} - > + <div className='inner fullwidth center'> <h2 - style={{ - color: props.outcome == 0 - ? 'var(--text)' - : props.outcome == props.player - ? 'var(--disk-a-text)' - : props.outcome != props.player - ? 'var(--disk-b-text)' - : 'var(--text)', - opacity: props.outcome == 0 ? .75 : 1, - marginTop: 8, - }} + className={'outcome' + ' ' + (props.outcome == 0 + ? 'draw' + : props.outcome == props.player + ? 'lose' + : props.outcome != props.player + ? 'win' + : 'draw')} > {props.outcome == 0 ? 'Gelijkspel' @@ -142,7 +126,7 @@ function GameOutcomeDialog(props: { ? 'Gewonnen' : '???'} </h2> - {false && <p style={{ marginTop: 24 }}> + {false && <p className='analysis'> 0 Gemiste winstzetten<br /> 6 Optimale zetten<br /> 0 Blunders @@ -150,42 +134,11 @@ function GameOutcomeDialog(props: { {false && <IconLabelButton text='Opnieuw spelen' icon={<RefreshIcon />} - style={{ - float: 'none', - marginTop: 24, - padding: '12px 32px', - }} />} </div> </DialogBox>; } -var InviteButtonStyle: CSSProperties = { - backgroundColor: 'var(--page-background)', - height: 160, - padding: 12, -}; - -var InviteButtonIconStyle: CSSProperties = { - fontSize: 100, - position: 'absolute', - top: 12, - left: '50%', - transform: 'translateX(-50%)', -}; - -var InviteButtonLabelStyle: CSSProperties = { - position: 'absolute', - bottom: 12, - left: '50%', - transform: 'translateX(-50%)', - textAlign: 'center', - color: 'var(--text-alt)', - width: 136, - fontSize: 20, - userSelect: 'none', -}; - export default function GamePage() { var [gameID, setGameID] = useState(''); var [player1, setPlayer1] = useState(true); @@ -226,7 +179,7 @@ export default function GamePage() { return <div> <NavBar /> - <CenteredPage width={900} style={{ height: '100vh' }}> + <CenteredPage width={900} className='h100vh'> <VoerGame active={active} gameID={gameID} @@ -236,22 +189,16 @@ export default function GamePage() { /> <DialogBox title='Nieuw spel' - style={{ display: gameIDUrl || gameID ? 'none' : 'inline-block' }} + className='newGameDialog' + hidden={!!(gameIDUrl || gameID)} onclick={() => { window.history.go(-1); }} > <CurrentGameSettings /> - <div - style={{ - marginTop: 24, - display: 'grid', - gridTemplateColumns: '1fr 1fr', - gridGap: 24, - }} - > + <div className='sidebyside gg-l'> <Button - style={InviteButtonStyle} + className='inviteButton random pad-m' onclick={() => { axios.request<{ id: string; player_1: boolean; game_started: boolean; }>({ url: '/api/game/random', @@ -266,16 +213,11 @@ export default function GamePage() { .catch(() => {}); }} > - <WifiTetheringRoundedIcon - style={{ - color: 'var(--disk-b)', - ...InviteButtonIconStyle, - }} - /> - <h2 style={InviteButtonLabelStyle}>Willekeurige speler</h2> + <WifiTetheringRoundedIcon className='icon posabs abscenterh' /> + <h2 className='label center posabs abscenterh nosel'>Willekeurige speler</h2> </Button> <Button - style={InviteButtonStyle} + className='inviteButton link pad-m' onclick={() => { axios.request<{ id: string; }>({ method: 'post', @@ -300,13 +242,8 @@ export default function GamePage() { .catch(() => {}); }} > - <LinkRoundedIcon - style={{ - color: 'var(--disk-a)', - ...InviteButtonIconStyle, - }} - /> - <h2 style={InviteButtonLabelStyle}>Uitnodigen via link</h2> + <LinkRoundedIcon className='icon posabs abscenterh' /> + <h2 className='label center posabs abscenterh nosel'>Uitnodigen via link</h2> </Button> </div> <SearchBar label='Zoeken in vriendenlijst' /> diff --git a/pages/index.tsx b/pages/index.tsx index 354efc5..c4b4baf 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { CSSProperties, useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { userGames, userGameTotals, userInfo } from '../api/api'; import { Footer } from '../components/footer'; import { SocketContext } from '../components/socketContext'; @@ -16,77 +16,14 @@ import VideogameAssetIcon from '@material-ui/icons/VideogameAsset'; import { mdiRobotExcited } from '@mdi/js'; import Icon from '@mdi/react'; -var GameModeIconStyle: CSSProperties = { - fontSize: 64, - width: 64, - height: 64, - display: 'inline-block', - position: 'absolute', - top: 24, - left: '50%', - transform: 'translateX(-50%)', -}; - -var GameModeTextStyle: CSSProperties = { - whiteSpace: 'nowrap', - display: 'inline-block', - position: 'absolute', - bottom: 24, - left: '50%', - transform: 'translateX(-50%)', - userSelect: 'none', - fontWeight: 500, -}; - -var SquareSize: CSSProperties = { - width: 90, - height: 90, -}; - -var LoginOrRegisterBoxStyle: CSSProperties = { - verticalAlign: 'top', - height: `calc(${SquareSize.height}px + 24px * 2)`, - width: '100%', - maxWidth: `calc(100% - ${SquareSize.width}px - 12px * 2 - 24px * 2)`, -}; - -var InnerLoginOrRegisterBoxStyle: CSSProperties = { - position: 'relative', - width: '100%', - height: '100%', -}; - function LoginOrRegisterBox() { - return <div style={{ ...InnerLoginOrRegisterBoxStyle, textAlign: 'center' }}> - <span - style={{ - userSelect: 'none', - display: 'inline-block', - position: 'absolute', - fontSize: 14, - left: 0, - right: 0, - top: 0, - margin: '0 auto', - minWidth: 240, - maxWidth: 350, - }} - > - Log in of maak een account aan om je scores op te slaan en toegang te krijgen tot meer functies + return <div className='inner'> + <span className='registerMessage posabs h0 t0'> + Log in of maak een account aan om toegang tot meer functies te krijgen </span> - <div - style={{ - display: 'grid', - gridGap: 24, - gridTemplateColumns: '1fr 1fr', - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - }} - > - <Button href='/register' text='Registreren' style={{ backgroundColor: 'var(--background-alt)' }} /> - <Button href='/login' text='Inloggen' /> + <div className='sidebyside posabs h0 b0'> + <Button href='/register' text='Registreren' className='register' /> + <Button href='/login' text='Inloggen' className='login' /> </div> </div>; } @@ -95,44 +32,19 @@ function AccountBox(props: { info: userInfo; sumGameInfo: userGameTotals; }) { - return <div style={InnerLoginOrRegisterBoxStyle}> - <div - style={{ - position: 'absolute', - top: 0, - left: 0, - ...SquareSize, - }} - > + return <div className='inner profile'> + <div className='picture posabs l0 t0'> <AccountAvatar size={90} /> </div> - <div - style={{ - position: 'absolute', - top: 0, - left: 102, - width: 'calc(100% - 90px - 12px)', - height: '100%', - }} - > - <h2 - style={{ - maxWidth: 178, - fontSize: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }} - > - {props.info?.username} - </h2> - <p style={{ marginTop: 6 }}>Score: {props.info?.rating}</p> - <p style={{ position: 'absolute', bottom: 0, left: 0 }}> - <span style={{ color: 'var(--disk-b-text)' }}>{props.sumGameInfo?.win} W</span> - <span style={{ margin: '0 3px' }}>/</span> - <span style={{ color: 'var(--disk-a-text)' }}>{props.sumGameInfo?.lose} V</span> - <span style={{ margin: '0 3px' }}>/</span> - <span style={{ opacity: .75 }}>{props.sumGameInfo?.draw} G</span> + <div className='info posabs t0'> + <h2 className='username truncate'>{props.info?.username}</h2> + <p className='score'>Score: {props.info?.rating}</p> + <p className='games posabs b0 l0'> + <span className='outcome win'>{props.sumGameInfo?.win} W</span> + <span className='divider'>/</span> + <span className='outcome lose'>{props.sumGameInfo?.lose} V</span> + <span className='divider'>/</span> + <span className='outcome draw'>{props.sumGameInfo?.draw} G</span> </p> </div> </div>; @@ -180,35 +92,32 @@ export default function HomePage() { <NavBar /> <CenteredPage width={802}> <PageTitle>4 op een rij</PageTitle> - <div> - <Vierkant href='/game'> - <VideogameAssetIcon style={GameModeIconStyle} /> - <span style={GameModeTextStyle}>Nieuw spel</span> - <div style={SquareSize}></div> + <div className='topbar'> + <Vierkant className='gamemode bg-800' href='/game'> + <VideogameAssetIcon className='icon' /> + <span className='text'>Nieuw spel</span> </Vierkant> {false - && <Vierkant href='/'> - <ExtensionIcon style={GameModeIconStyle} /> - <span style={GameModeTextStyle}>Puzzels</span> - <div style={SquareSize}></div> + && <Vierkant className='gamemode bg-800' href='/'> + <ExtensionIcon className='icon' /> + <span className='text'>Puzzels</span> </Vierkant>} {false - && <Vierkant href='/'> - <Icon path={mdiRobotExcited} style={GameModeIconStyle} /> - <span style={GameModeTextStyle}>Tegen computer</span> - <div style={SquareSize}></div> + && <Vierkant className='gamemode bg-800' href='/'> + <Icon path={mdiRobotExcited} className='icon' /> + <span className='text'>Tegen computer</span> </Vierkant>} - <Vierkant style={LoginOrRegisterBoxStyle}> + <Vierkant className='loginOrRegisterBox pad-l valigntop bg-800'> {loggedIn ? <AccountBox info={userInfo} sumGameInfo={gameInfo?.totals} /> : <LoginOrRegisterBox />} </Vierkant> </div> {loggedIn - && <Vierkant fullwidth> + && <Vierkant className='w100m2m pad-l bg-800'> <RecentGames games={gameInfo?.games} /> </Vierkant>} - <Vierkant fullwidth> + <Vierkant className='w100m2m pad-l bg-800'> <h2>Nieuws ofzo</h2> <p style={{ margin: '6px 0' }}>Chess.com heeft heel veel troep waar niemand naar kijkt</p> </Vierkant> diff --git a/pages/login.tsx b/pages/login.tsx index 1e14573..da13f45 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -48,45 +48,33 @@ export default function LoginPage() { return ( <div> <NavBar /> - <CenteredPage width={500} style={{ height: '100vh' }}> - <div - style={{ - position: 'relative', - top: '50%', - transform: 'translateY(-50%)', - margin: '0 auto', - textAlign: 'center', - }} - > - <Vierkant> + <CenteredPage width={500} className='h100vh'> + <div className='posrel center centeredForm'> + <Vierkant className='pad-l bg-800'> <form onSubmit={(e) => submitLogin(e, toast)}> <Input autofocus autocomplete='username' id='email' label='email of gebruikersnaam' - style={{ marginBottom: 12 }} - > - </Input> - <Input autocomplete='current-password' id='password' label='wachtwoord' type='password'> - </Input> - <div - style={{ - marginTop: 24, - gridGap: 24, - display: 'grid', - gridTemplateColumns: '1fr 1fr', - }} - > + className='pad-m fullwidth bg-900 round-t' + /> + <Input + autocomplete='current-password' + id='password' + label='wachtwoord' + type='password' + className='pad-m fullwidth bg-900 round-t' + /> + <div className='sidebyside'> <Button href='/register' text='Registreren' - style={{ backgroundColor: 'var(--background-alt)' }} - > - </Button> - <Button text='Inloggen' onclick={() => submitLogin(null, toast)}></Button> + className='register bg-700 fg-100' + /> + <Button text='Inloggen' className='login' onclick={() => submitLogin(null, toast)} /> </div> - <input type='submit' style={{ display: 'none' }} /> + <input type='submit' className='dispnone' /> </form> </Vierkant> </div> diff --git a/pages/register.tsx b/pages/register.tsx index f78d092..bc2fc0f 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -88,35 +88,35 @@ export default function RegisterPage() { return ( <div> <NavBar /> - <CenteredPage width={500} style={{ height: '100vh' }}> - <div - style={{ - position: 'relative', - top: '50%', - transform: 'translateY(-50%)', - margin: '0 auto', - textAlign: 'center', - }} - > - <Vierkant> + <CenteredPage width={500} className='h100vh'> + <div className='posrel center centeredForm'> + <Vierkant className='pad-l bg-800'> <form onSubmit={(e) => submitRegister(e, toast)}> <Input autofocus autocomplete='username' id='username' label='gebruikersnaam' - style={{ marginBottom: 12 }} - > - </Input> - <Input autocomplete='email' id='email' label='email' style={{ marginBottom: 12 }}></Input> - <Input autocomplete='new-password' id='password' label='wachtwoord' type='password'></Input> + className='pad-m fullwidth bg-900 round-t' + /> + <Input + autocomplete='email' + id='email' + label='email' + className='pad-m fullwidth bg-900 round-t' + /> + <Input + autocomplete='new-password' + id='password' + label='wachtwoord' + type='password' + className='pad-m fullwidth bg-900 round-t' + /> <Button text='Registreren' - style={{ marginTop: 24 }} onclick={() => submitRegister(null, toast)} - > - </Button> - <input type='submit' style={{ display: 'none' }} /> + /> + <input type='submit' className='dispnone' /> </form> </Vierkant> </div> diff --git a/pages/search.tsx b/pages/search.tsx index 2b8668a..99f99b0 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -10,7 +10,7 @@ import { Button, Input, Vierkant } from '../components/ui'; import SearchOutlinedIcon from '@material-ui/icons/SearchOutlined'; function search(callback: (results: Array<userInfo>) => void) { - var query: string = (document.getElementById('searchBar') as HTMLInputElement).value; + var query: string = (document.getElementById('bigSearchBar') as HTMLInputElement).value; if (query.length < 3) return; axios.request<{ 'results': Array<userInfo>; }>({ @@ -24,74 +24,45 @@ function search(callback: (results: Array<userInfo>) => void) { } function SearchResults(props: { userList: Array<userInfo>; }) { - return <div> + return <div className='results w100m2m'> {props.userList?.map(user => <SearchResult user={user} key={user.id} />)} </div>; } function SearchResult(props: { user: userInfo; }) { return <Vierkant - style={{ - padding: 12, - }} - fullwidth + className='result bg-800 pad-m fullwidth' href={'/user?id=' + props.user.id} > - <div style={{ position: 'relative' }}> + <div className='inner posrel'> <AccountAvatar size={48} id={props.user.id} /> - <div - style={{ - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 48 + 12, - }} - > - <b>{props.user.username}</b> - <p>{props.user.status}</p> + <div className='userInfo posabs v0 r0'> + <b className='username'>{props.user.username}</b> + <p className='status'>{props.user.status}</p> </div> </div> </Vierkant>; } -function SearchBar(props: { +function BigSearchBar(props: { searchFunction: (event?: FormEvent<HTMLFormElement>) => void; }) { - return <Vierkant - fullwidth - style={{ - padding: 8, - marginBottom: 24, - }} - > + return <Vierkant className='pad-m bg-800 w100m2m bigSearchBar posrel'> <form onSubmit={props.searchFunction}> <Input - id='searchBar' + id='bigSearchBar' label='Zoeken voor gebruikers...' autocomplete='off' - dark autofocus - style={{ - backgroundColor: 'var(--background)', - color: 'var(--text)', - padding: 14, - fontSize: 16, - width: 'calc(100% - 48px - 14px * 2)', - }} + className='pad-m posabs abscenterv' /> <Button - style={{ - padding: 12, - float: 'right', - display: 'inline-block', - borderRadius: 4, - }} + className='pad-m dispinbl valigntop floatr' onclick={props.searchFunction} > <SearchOutlinedIcon /> </Button> - <input type='submit' style={{ display: 'none' }} /> + <input type='submit' className='dispnone' /> </form> </Vierkant>; } @@ -109,15 +80,10 @@ export default function HomePage() { <NavBar /> <CenteredPage width={802}> <PageTitle>Zoeken</PageTitle> - <SearchBar searchFunction={getSearchResults} /> + <BigSearchBar searchFunction={getSearchResults} /> <SearchResults userList={results} /> {searched && results.length == 0 && <h1 - style={{ - opacity: .6, - color: 'var(--text)', - textAlign: 'center', - margin: '24px 32px', - }} + className='noresults center subtile' > Geen zoekresultaten gevonden </h1>} diff --git a/pages/settings.tsx b/pages/settings.tsx index 0f40a90..0ca2c30 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -1,7 +1,6 @@ import axios from 'axios'; import reduce from 'image-blob-reduce'; -import { CSSProperties, useContext } from 'react'; -import * as cookies from 'react-cookies'; +import { useContext } from 'react'; import { AccountAvatar } from '../components/account'; import { Footer } from '../components/footer'; @@ -16,11 +15,6 @@ import ExitToAppOutlinedIcon from '@material-ui/icons/ExitToAppOutlined'; import PublishOutlinedIcon from '@material-ui/icons/PublishOutlined'; import VisibilityOutlinedIcon from '@material-ui/icons/VisibilityOutlined'; -var SettingsSubsectionStyle: CSSProperties = { - marginTop: 24, - minHeight: 40, -}; - async function uploadNewProfileImage() { if (!this.result) return; @@ -54,9 +48,9 @@ export default function SettingsPage() { <NavBar /> <CenteredPage width={802}> <PageTitle>Instellingen</PageTitle> - <Vierkant fullwidth> + <Vierkant className='section account w100m2m pad-l bg-800'> <h2>Account</h2> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <AccountAvatar size={100} /> <label htmlFor='pfUpload'> <IconLabelButton text='Nieuwe profielfoto uploaden' icon={<PublishOutlinedIcon />} /> @@ -65,7 +59,7 @@ export default function SettingsPage() { type='file' id='pfUpload' accept='.png,.jpg,.jpeg' - style={{ display: 'none' }} + className='dispnone' onChange={event => { var file = event.target.files[0]; if (!file) return; @@ -76,45 +70,45 @@ export default function SettingsPage() { }} /> </div> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <IconLabelButton text='Bewerken' icon={<EditOutlinedIcon />} /> - <div style={{ display: 'block' }}> + <div className='dispbl'> <h3>Gebruikersnaam</h3> <p>Hier staat hij dan</p> </div> </div> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <IconLabelButton text='Bewerken' icon={<EditOutlinedIcon />} /> <IconLabelButton text='Onthullen' icon={<VisibilityOutlinedIcon />} /> - <div style={{ display: 'block' }}> + <div className='dispbl'> <h3>Email</h3> <p>******@example.com</p> </div> </div> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <IconLabelButton text='Bewerken' icon={<EditOutlinedIcon />} /> - <div style={{ display: 'block' }}> + <div className='dispbl'> <h3>Wachtwoord</h3> </div> </div> </Vierkant> - <Vierkant fullwidth> + <Vierkant className='section colors w100m2m pad-l bg-800'> <h2>Kleuren</h2> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <ColorPicker /> <ColorPicker /> - <div style={{ display: 'block' }}> + <div className='dispbl'> <h3>Schijfjes</h3> </div> </div> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <ColorPicker /> - <div style={{ display: 'block' }}> + <div className='dispbl'> <h3>Achtergrond</h3> </div> </div> - <div style={SettingsSubsectionStyle}> - <div style={{ float: 'right' }}> + <div className='subsection'> + <div className='floatr'> <CheckBox state={preferences?.darkMode} onclick={state => updatePreference({ 'darkMode': state })} @@ -123,31 +117,20 @@ export default function SettingsPage() { <h3>Donkere modus</h3> </div> </Vierkant> - <Vierkant fullwidth> + <Vierkant className='section gamerules w100m2m pad-l bg-800'> <h2>Standaard spelregels</h2> - <div style={SettingsSubsectionStyle}> + <div className='subsection'> <CurrentGameSettings /> </div> </Vierkant> - <Vierkant fullwidth> + <Vierkant className='section logout w100m2m pad-l bg-800'> <h2>Uitloggen</h2> - <div - style={{ - width: '100%', - textAlign: 'center', - }} - > + <div className='center'> <IconLabelButton + className='dispinbl' icon={<ExitToAppOutlinedIcon />} text='Uitloggen' - style={{ - float: 'none', - marginLeft: 0, - }} - onclick={() => { - cookies.remove('token'); - window.location.pathname = '/'; - }} + href='/logout' /> </div> </Vierkant> diff --git a/pages/user.tsx b/pages/user.tsx index 4f7331c..9741275 100644 --- a/pages/user.tsx +++ b/pages/user.tsx @@ -1,6 +1,6 @@ import Icon from '@mdi/react'; import axios from 'axios'; -import { Children, ReactNode, useContext, useEffect, useState } from 'react'; +import { ReactNode, useContext, useEffect, useState } from 'react'; import { userGames, userInfo } from '../api/api'; import { AccountAvatar } from '../components/account'; @@ -35,60 +35,18 @@ function InfoModule(props: { label: string; icon: ReactNode; }) { - return <div - style={{ - position: 'relative', - height: '100%', - }} - > - <div - style={{ - position: 'absolute', - left: '50%', - transform: 'translateX(-50%)', - }} - > + return <div className='infoModule posrel'> + <div className='iconWrapper posabs'> {props.icon} </div> - <div - style={{ - position: 'absolute', - top: 24 + 6, - left: 0, - right: 0, - bottom: 0, - }} - > - <span - style={{ - position: 'absolute', - top: '50%', - transform: 'translateY(-50%)', - width: '100%', - textAlign: 'center', - }} - > + <div className='labelWrapper posabs h0 b0'> + <span className='label posabs center fullwidth'> {props.label} </span> </div> </div>; } -function InfoSection(props: { children: ReactNode; }) { - return <Vierkant fullwidth> - <div - style={{ - display: 'grid', - gridTemplateColumns: `repeat(${Children.count(props.children)}, 1fr)`, - gridGap: 12, - height: 64, - }} - > - {props.children} - </div> - </Vierkant>; -} - export default function AccountPage() { var server = typeof window === 'undefined'; var loggedIn = !server && document.cookie.includes('token'); @@ -167,226 +125,222 @@ export default function AccountPage() { <NavBar /> <CenteredPage width={802}> <PageTitle>Profiel</PageTitle> - <Vierkant fullwidth> - <AccountAvatar size={128} id={user?.id || ''} /> - <div - style={{ - display: 'inline-block', - verticalAlign: 'top', - marginLeft: 12, - width: 'calc(100% - 128px - 12px)', - }} - > - <h2 style={{ fontSize: 32 }}>{user?.username}</h2> - <p - id='status' - contentEditable={editingStatus ? 'true' : 'false'} - style={{ - marginTop: 6, - transitionDuration: '.3s', - }} - suppressContentEditableWarning={true} - > - {user?.status} - </p> - </div> - <div - style={{ - position: 'absolute', - backgroundColor: 'var(--background)', - height: '40px', - bottom: 24, - left: 24 + 12 + 128, - right: 24, - }} - > - {loggedIn && <div> - {ownPage - ? <div> - <IconLabelButton icon={<SettingsOutlinedIcon />} href='/settings' text='Instellingen' /> - {!editingStatus - ? <IconLabelButton - icon={<EditOutlinedIcon />} - text='Status bewerken' - onclick={() => setEditingStatus(true)} + <Vierkant className='accountHeader w100m2m pad-l bg-800'> + <div className='inner posrel'> + <AccountAvatar size={128} id={user?.id || ''} /> + <div className='userInfo dispinbl valigntop'> + <h2 className='username'>{user?.username}</h2> + <p + id='status' + className='status round-t' + contentEditable={editingStatus ? 'true' : 'false'} + suppressContentEditableWarning={true} + > + {user?.status} + </p> + </div> + <div className='posabs b0 r0'> + {loggedIn && <div> + {ownPage + ? <div> + <IconLabelButton + icon={<SettingsOutlinedIcon />} + href='/settings' + text='Instellingen' /> - : <IconLabelButton - icon={<DoneOutlinedIcon />} - text='Status opslaan' - onclick={() => { - setEditingStatus(false); - axios.request({ - method: 'post', - url: `/api/user/status`, - headers: { 'content-type': 'application/json' }, - data: { 'status': document.getElementById('status').innerText }, - }); - }} - />} - </div> - : <div> - {(() => { - var icon = { - 'blocked': <Icon size={1} path={mdiAccountCancelOutline} />, - }[relation] || <Icon size={1} path={mdiAccountCancelOutline} />; + {!editingStatus + ? <IconLabelButton + icon={<EditOutlinedIcon />} + text='Status bewerken' + onclick={() => setEditingStatus(true)} + /> + : <IconLabelButton + icon={<DoneOutlinedIcon />} + text='Status opslaan' + onclick={() => { + setEditingStatus(false); + axios.request({ + method: 'post', + url: `/api/user/status`, + headers: { 'content-type': 'application/json' }, + data: { 'status': document.getElementById('status').innerText }, + }); + }} + />} + </div> + : <div> + {(() => { + var icon = { + 'blocked': <Icon size={1} path={mdiAccountCancelOutline} />, + }[relation] || <Icon size={1} path={mdiAccountCancelOutline} />; - var text = { - 'blocked': 'Deblokkeren', - }[relation] || 'Blokkeren'; + var text = { + 'blocked': 'Deblokkeren', + }[relation] || 'Blokkeren'; - return <IconLabelButton - icon={icon} - text={text} - onclick={() => { - var nextRelation = { - 'blocked': { - 'endpoint': '/api/social/unblock', - 'action': `${user.username} gedeblokkeerd`, - 'relation': 'none', + return <IconLabelButton + icon={icon} + text={text} + onclick={() => { + var nextRelation = { + 'blocked': { + 'endpoint': '/api/social/unblock', + 'action': `${user.username} gedeblokkeerd`, + 'relation': 'none', + 'icon': <Icon size={1} path={mdiAccountCancelOutline} />, + }, + }[relation] || { + 'endpoint': '/api/social/block', + 'action': `${user.username} geblokkeerd`, + 'relation': 'blocked', 'icon': <Icon size={1} path={mdiAccountCancelOutline} />, - }, - }[relation] || { - 'endpoint': '/api/social/block', - 'action': `${user.username} geblokkeerd`, - 'relation': 'blocked', - 'icon': <Icon size={1} path={mdiAccountCancelOutline} />, - }; + }; - axios.request({ - method: 'post', - url: nextRelation.endpoint, - headers: { 'content-type': 'application/json' }, - data: { 'id': user?.id }, - }) - .then(() => { - toast({ - message: nextRelation.action, - type: 'confirmation', - icon: nextRelation.icon, + axios.request({ + method: 'post', + url: nextRelation.endpoint, + headers: { 'content-type': 'application/json' }, + data: { 'id': user?.id }, + }) + .then(() => { + toast({ + message: nextRelation.action, + type: 'confirmation', + icon: nextRelation.icon, + }); + setRelation(nextRelation.relation); }); - setRelation(nextRelation.relation); - }); - }} - />; - })()} - {(() => { - var icon = { - 'friends': <Icon size={1} path={mdiAccountMinusOutline} />, - 'outgoing': <Icon size={1} path={mdiAccountRemoveOutline} />, - 'incoming': <PersonAddOutlinedIcon />, - }[relation] || <PersonAddOutlinedIcon />; + }} + />; + })()} + {(() => { + var icon = { + 'friends': <Icon size={1} path={mdiAccountMinusOutline} />, + 'outgoing': <Icon size={1} path={mdiAccountRemoveOutline} />, + 'incoming': <PersonAddOutlinedIcon />, + }[relation] || <PersonAddOutlinedIcon />; - var text = { - 'friends': 'Vriend verwijderen', - 'outgoing': 'Vriendschapsverzoek annuleren', - 'incoming': 'Vriendschapsverzoek accepteren', - }[relation] || 'Vriendschapsverzoek sturen'; + var text = { + 'friends': 'Vriend verwijderen', + 'outgoing': 'Vriendschapsverzoek annuleren', + 'incoming': 'Vriendschapsverzoek accepteren', + }[relation] || 'Vriendschapsverzoek sturen'; - return <IconLabelButton - icon={icon} - text={text} - onclick={() => { - var nextRelation = { - 'friends': { - 'endpoint': '/api/social/remove', - 'action': `${user.username} succesvol verwijderd als vriend`, - 'relation': 'none', - 'icon': <Icon size={1} path={mdiAccountMinusOutline} />, - }, - 'outgoing': { - 'endpoint': '/api/social/remove', - 'action': `Vriendschapsverzoek naar ${user.username} geannuleerd`, - 'relation': 'none', - 'icon': <Icon size={1} path={mdiAccountMinusOutline} />, - }, - 'incoming': { - 'endpoint': '/api/social/accept', - 'action': `Vriendschapsverzoek van ${user.username} geaccepteerd`, - 'relation': 'friends', + return <IconLabelButton + icon={icon} + text={text} + onclick={() => { + var nextRelation = { + 'friends': { + 'endpoint': '/api/social/remove', + 'action': `${user.username} succesvol verwijderd als vriend`, + 'relation': 'none', + 'icon': <Icon size={1} path={mdiAccountMinusOutline} />, + }, + 'outgoing': { + 'endpoint': '/api/social/remove', + 'action': + `Vriendschapsverzoek naar ${user.username} geannuleerd`, + 'relation': 'none', + 'icon': <Icon size={1} path={mdiAccountMinusOutline} />, + }, + 'incoming': { + 'endpoint': '/api/social/accept', + 'action': + `Vriendschapsverzoek van ${user.username} geaccepteerd`, + 'relation': 'friends', + 'icon': <PersonAddOutlinedIcon />, + }, + }[relation] || { + 'endpoint': '/api/social/request', + 'action': `Vriendschapsverzoek gestuurd naar ${user.username}`, + 'relation': 'outgoing', 'icon': <PersonAddOutlinedIcon />, - }, - }[relation] || { - 'endpoint': '/api/social/request', - 'action': `Vriendschapsverzoek gestuurd naar ${user.username}`, - 'relation': 'outgoing', - 'icon': <PersonAddOutlinedIcon />, - }; + }; - axios.request({ - method: 'post', - url: nextRelation.endpoint, - headers: { 'content-type': 'application/json' }, - data: { 'id': user?.id }, - }) - .then(() => { - toast({ - message: nextRelation.action, - type: 'confirmation', - icon: nextRelation.icon, + axios.request({ + method: 'post', + url: nextRelation.endpoint, + headers: { 'content-type': 'application/json' }, + data: { 'id': user?.id }, + }) + .then(() => { + toast({ + message: nextRelation.action, + type: 'confirmation', + icon: nextRelation.icon, + }); + setRelation(nextRelation.relation); }); - setRelation(nextRelation.relation); - }); - }} - />; - })()} - </div>} - </div>} + }} + />; + })()} + </div>} + </div>} + </div> </div> </Vierkant> - <InfoSection> - <InfoModule - icon={<Icon size={1} path={mdiCheckboxBlankCircle} color='var(--disk-b-text)' />} - label='Online' - /> - <InfoModule - icon={<AssignmentIndOutlinedIcon />} - label={(() => { - var memberSince = 'Lid sinds'; + <Vierkant className='infosection pad-l w100m2m bg-800'> + <div className='inner sidebyside'> + <InfoModule + icon={<Icon size={1} path={mdiCheckboxBlankCircle} className='outcome win' />} + label='Online' + /> + <InfoModule + icon={<AssignmentIndOutlinedIcon />} + label={(() => { + var memberSince = 'Lid sinds'; - var registered = new Date(user?.registered); - memberSince += ' ' + registered.toLocaleString('nl-nl', { month: 'long', day: 'numeric' }); + var registered = new Date(user?.registered); + memberSince += ' ' + registered.toLocaleString('nl-nl', { month: 'long', day: 'numeric' }); - var currentYear = new Date().getFullYear(); - var memberYear = registered.getFullYear(); - if (currentYear != memberYear) memberSince += ' ' + memberYear; + var currentYear = new Date().getFullYear(); + var memberYear = registered.getFullYear(); + if (currentYear != memberYear) memberSince += ' ' + memberYear; - return memberSince; - })()} - /> - <InfoModule - icon={<PeopleOutlineOutlinedIcon />} - label={(() => { - var label = user?.friends.toString() + ' '; - label += user?.friends == 1 ? 'vriend' : 'vrienden'; - return label; - })()} - /> - <InfoModule icon={<Icon size={1} path={mdiEarth} />} label='Nederland' /> - </InfoSection> - <InfoSection> - <InfoModule - icon={<ArrowUpwardOutlinedIcon style={{ color: 'var(--disk-b-text)' }} />} - label={gameInfo?.totals.win + ' keer gewonnen'} - /> - <InfoModule - icon={<Icon size={1} path={mdiEqual} />} - label={gameInfo?.totals.draw + ' keer gelijkspel'} - /> - <InfoModule - icon={<ArrowDownwardOutlinedIcon style={{ color: 'var(--disk-a-text)' }} />} - label={gameInfo?.totals.lose + ' keer verloren'} - /> - <InfoModule icon={<Icon size={1} path={mdiClipboardTextOutline} />} label={'Score: ' + user?.rating} /> - <InfoModule - icon={<Icon size={1} path={mdiGamepadSquareOutline} />} - label={(() => { - var label = gameInfo?.totals.games.toString() + ' '; - label += gameInfo?.totals.games == 1 ? 'potje' : 'potjes'; - return label; - })()} - /> - </InfoSection> - <Vierkant> + return memberSince; + })()} + /> + <InfoModule + icon={<PeopleOutlineOutlinedIcon />} + label={(() => { + var label = user?.friends.toString() + ' '; + label += user?.friends == 1 ? 'vriend' : 'vrienden'; + return label; + })()} + /> + <InfoModule icon={<Icon size={1} path={mdiEarth} />} label='Nederland' /> + </div> + </Vierkant> + <Vierkant className='infosection pad-l w100m2m sidebyside bg-800'> + <div className='inner sidebyside'> + <InfoModule + icon={<ArrowUpwardOutlinedIcon className='outcome win' />} + label={gameInfo?.totals.win + ' keer gewonnen'} + /> + <InfoModule + icon={<Icon size={1} path={mdiEqual} className='subtile' />} + label={gameInfo?.totals.draw + ' keer gelijkspel'} + /> + <InfoModule + icon={<ArrowDownwardOutlinedIcon className='outcome lose' />} + label={gameInfo?.totals.lose + ' keer verloren'} + /> + <InfoModule + icon={<Icon size={1} path={mdiClipboardTextOutline} />} + label={'Score: ' + user?.rating} + /> + <InfoModule + icon={<Icon size={1} path={mdiGamepadSquareOutline} />} + label={(() => { + var label = gameInfo?.totals.games.toString() + ' '; + label += gameInfo?.totals.games == 1 ? 'potje' : 'potjes'; + return label; + })()} + /> + </div> + </Vierkant> + <Vierkant className='pad-l bg-800'> <RecentGames games={gameInfo?.games} /> </Vierkant> </CenteredPage> diff --git a/public/favicon.png b/public/favicon.png Binary files differindex 15b2b22..e03870b 100644 --- a/public/favicon.png +++ b/public/favicon.png diff --git a/public/favicon.svg b/public/favicon.svg index c1f7520..63e5b7a 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,14 +1,16 @@ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <style> - .red { fill: #E16D82; } - .green { fill: #71D9CC; } + .red { fill: #A63A4D; } + .green { fill: #3AA699; } .outline { - stroke: #5D737E; + stroke: #141619; stroke-width: 2px; } @media (prefers-color-scheme: dark) { .outline { stroke: #FCFFFD; } + .red { fill: #FF4365; } + .green { fill: #00D9C0; } } </style> <circle class="green" cx="6.5" cy="6.5" r="4.5"/> @@ -42,11 +42,16 @@ A design prototype of the website can be found on [Figma](https://www.figma.com/file/rTciVQApAe6cwrH1Prl5Wn/4-op-een-rij?node-id=0%3A1). -An outdated version of the API documentation is still on Google Docs, but I'm -moving and updating it to be in api/readme.md. +The api documentation can be found in api/readme.md.  +## other readme's + +- [api](api/readme.md) +- [voerbak](voerbak/readme.md) +- [styles](styles/readme.md) + ## setup To set up this project you'll need to install npm and pip dependencies, pull all diff --git a/styles/dark.css b/styles/dark.css index 681116f..b44171b 100644 --- a/styles/dark.css +++ b/styles/dark.css @@ -1,9 +1,19 @@ html.dark { - --text: #FCFFFD; - --text-alt: var(--text); + --background: var(--gray-900); + --foreground: #FFFFF3; - --page-background: #11171a; - --background: #222d33; - --background-alt: #344047; -} + --accent: #AD34F3; + + --error: #FF4365; + --disk-b-alt: #F49BA1; + --confirm: #00D9C0; + --disk-a-alt: #86F3F3; + + --gray-100: #CED2DC; + --gray-200: #A9AFC0; + --gray-300: #757D92; + --gray-700: #293140; + --gray-800: #1F242D; + --gray-900: #141619; +} diff --git a/styles/disk.css b/styles/disk.css index d858335..2646fed 100644 --- a/styles/disk.css +++ b/styles/disk.css @@ -1,3 +1,3 @@ .voerBord .disk.state-0 { background-color: var(--page-background); } -.voerBord .disk.state-1 { background-color: var(--disk-a); } -.voerBord .disk.state-2 { background-color: var(--disk-b); } +.voerBord .disk.state-1 { background-color: var(--confirm); } +.voerBord .disk.state-2 { background-color: var(--error); } diff --git a/styles/game.css b/styles/game.css new file mode 100644 index 0000000..d482f9b --- /dev/null +++ b/styles/game.css @@ -0,0 +1,76 @@ +.outcomeDialog .inner > * { + margin-top: var(--spacing-medium); +} + +.outcomeDialog .inner .button { + float: unset; + padding: var(--spacing-medium) var(--spacing-large); +} + +.newGameDialog .inviteButton { height: 160px; } + +.newGameDialog .inviteButton.random { background-color: var(--confirm); } + +.newGameDialog .inviteButton.link { background-color: var(--error); } + +.newGameDialog .inviteButton .icon { + top: var(--spacing-medium); + font-size: 100px; +} + +.newGameDialog .inviteButton .label { bottom: var(--spacing-medium); } + +.newGameDialog > div { margin-top: var(--spacing-large); } + +.voerBord { border-spacing: var(--spacing-small); } + +.voerBord .cell.inner { border: 2px solid var(--gray-800); } + +.voerBord .square { margin-top: 100%; } + +.voerBord.active .cell.inner { cursor: pointer; } + +.voerBord .disk { + border-radius: 999999999px; + margin: 3px; +} + +.gameBar .gameBarButton { + margin: 0; + margin-left: var(--spacing-small); +} + +.gameBar .move { + width: 32px; + height: 32px; + border-radius: 16px; + + margin: 8px; +} + +.gameBar .move.move-a { background-color: var(--error); } +.gameBar .move.move-b { background-color: var(--confirm); } + +.gameBar .timer span { + margin: 0 4px; + font-size: 20px; +} + +.gameBar .score { + font-size: 20px; + z-index: 1; +} + +.gameBar .score.winning { color: var(--disk-a-alt); } +.gameBar .score.losing { color: var(--disk-b-alt); } + +.voerGame { + max-width: 100vh; + margin: 0 auto; +} + +html.dark .newGameDialog .inviteButton { background-color: var(--gray-800); } +html.dark .newGameDialog .inviteButton.random .icon { color: var(--confirm); } +html.dark .newGameDialog .inviteButton.link .icon { color: var(--error); } +html.dark .newGameDialog .searchBar .input { background-color: var(--gray-800); } + diff --git a/styles/gameSettings.css b/styles/gameSettings.css new file mode 100644 index 0000000..3c45fb4 --- /dev/null +++ b/styles/gameSettings.css @@ -0,0 +1,77 @@ +.editGameSettings { + height: 80px; + overflow: visible; + z-index: 1; +} + +.editGameSettings .currentRules { + top: 50%; + transform: translateY(-50%); +} + +.editGameSettings > .button { + width: 150px; + top: 50%; + transform: translateY(-50%); + text-align: left; +} + +.editGameSettings > .button .icon { + font-size: 48px; +} + +.editGameSettings > .button .text { + right: var(--spacing-medium); + width: 85px; + top: 50%; + vertical-align: middle; + transform: translateY(-50%); +} + +.editGameSettings .editableRules { + margin: var(--spacing-large) 0; + max-height: 500px; + overflow-y: scroll; +} + +.editGameSettings .editableRules .editableRulesSection { + width: 100%; + background-color: var(--gray-700); + margin: 0; + margin-bottom: var(--spacing-medium); +} +html.dark .editGameSettings .editableRules .editableRulesSection { + background-color: var(--gray-800); +} +.editGameSettings .editableRules .editableRulesSection:last-child { + margin-bottom: 0; +} + +.editGameSettings .button { line-height: normal; } + +.editGameSettings .checkboxWrapper { margin: -3px; } + +.editGameSettings .editableRules .editableRulesSection .timeControls { + margin: var(--spacing-medium); + margin-left: 0; + margin-right: 0; +} + +.editGameSettings .editableRules .editableRulesSection .timeControls input { + background-color: var(--background); + width: calc(100% - 2 * var(--spacing-medium)); +} + +.editGameSettings .editableRules .gamerule { + margin-top: var(--spacing-medium); +} + +.editGameSettings .editableRules .gamerule h1 { + color: var(--accent); + font-size: 2.5rem; +} + +#timelimit .valignsup { + margin-left: var(--spacing-small); +} + diff --git a/styles/global.css b/styles/global.css index 1e87524..79aa026 100644 --- a/styles/global.css +++ b/styles/global.css @@ -1,15 +1,36 @@ -html { - --text: #FCFFFD; - --page-background: var(--text); - - --background: #5D737E; - --text-alt: var(--background); - --background-alt: #81949E; - - --disk-a: #E16D82; - --disk-a-text: #FDC0C4; - --disk-b: #71D9CC; - --disk-b-text: #C0FDEB; +:root { + --background: var(--gray-900); + --foreground: var(--gray-100); + + --accent: #7E3AA6; + + --error: #A63A4D; + --disk-b-alt: #582D35; + + --confirm: #3AA699; + --disk-a-alt: #244743; + + /* shade */ + --gray-100: #141619; + --gray-200: #1F242D; + --gray-300: #293140; + --gray-600: #757D92; + --gray-700: #A9AFC0; + --gray-800: #CED2DC; + --gray-900: #E3E6EE; + + /* box-shadow */ + --drop-level-2: 0px 8px 32px 0px rgba(0, 0, 0, 0.3); + --drop-level-1: 0px 8px 12px -4px rgba(0, 0, 0, 0.15); + + /* border-radius */ + --tight-corner: 6px; + --loose-corner: 8px; + + /* margin/padding */ + --spacing-small: 6px; + --spacing-medium: 12px; + --spacing-large: 24px; } /* default margin */ @@ -18,6 +39,9 @@ html, body { padding: 0; } +/* section font size */ +h1 { font-size: 25px; } + /* subsection font size */ h2 { font-size: 20px; } @@ -27,17 +51,17 @@ h3 { padding-bottom: 6px; } -/* global font zize */ -p, b, i, span, td, th { font-size: 15px; } - /* navbar fix */ body { padding-left: 48px; } /* font */ -html { font-family: "Inter"; } +html { font-family: "Inter", sans-serif; } -/* background color */ -html, body { background-color: var(--page-background); } +/* color */ +:root, html, body { + background-color: var(--background); + color: var(--foreground); +} /* link fix */ a { @@ -59,14 +83,20 @@ h1, h2, h3, p, b, i, span, td, th { table { table-layout: fixed; } /* table styles */ -td, th { - padding: 4px; - font-size: 15px; +td, th { padding: 4px; } + +input { + color: var(--foreground); + background-color: transparent; + font-family: inherit; + border: 0; + font-size: 1rem; /* why? */ } input::placeholder { font-style: italic; - opacity: .8; + opacity: 1; + color: var(--gray-600); } /* remove chrome's ugly :focus outline */ @@ -78,19 +108,3 @@ input::placeholder { /* material-ui default state */ svg.MuiSvgIcon-root { transition: none !important; } -input::placeholder { opacity: .75; } -input.dark::placeholder { color: var(--text); } -input.light::placeholder { color: var(--text-alt); } - -/* editable field status */ -*[contenteditable] { border-color: var(--background-alt); } -*[contenteditable="true"]:focus { border-color: var(--disk-a); } -*[contenteditable="true"] { - background-color: var(--page-background); - color: var(--text-alt); - padding: 6px; - border-radius: 6px; - border-style: solid; - border-width: 2px; -} - diff --git a/styles/index.css b/styles/index.css new file mode 100644 index 0000000..fcda8e0 --- /dev/null +++ b/styles/index.css @@ -0,0 +1,71 @@ +.topbar * { + --squareSize: 90px; +} + +.loginOrRegisterBox { + height: calc(var(--squareSize) + 2 * var(--spacing-large)); + width: 100%; + max-width: calc(100% - var(--squareSize) - var(--spacing-medium) * 2 - var(--spacing-large) * 2); +} + +.loginOrRegisterBox .inner { + position: relative; + width: 100%; + height: 100%; +} + +.loginOrRegisterBox .inner .registerMessage { + user-select: none; + display: inline-block; + margin: 0 auto; + min-width: 240px; + max-width: 350px; + text-align: center; +} + +.loginOrRegisterBox .inner .button.register { + background-color: var(--gray-700); + color: var(--foreground); +} + +.gamemode .icon { + font-size: 64px; + font-size: 64px; + height: 64px; + display: inline-block; + position: absolute; + top: var(--spacing-large); + left: 50%; + transform: translateX(-50%); +} + +.gamemode .text { + white-space: nowrap; + display: inline-block; + position: absolute; + bottom: var(--spacing-large); + left: 50%; + transform: translateX(-50%); + user-select: none; + font-weight: 500; +} + +.topbar .gamemode { + --size: calc(var(--squareSize) + 2 * var(--spacing-large)); + width: var(--size); + height: var(--size); +} + +.topbar .profile .info { + left: calc(90px + 12px); + width: calc(100% - 90px - 12px); + height: 100%; +} + +.topbar .profile .info .username { max-width: 178px; } +.topbar .profile .info .score { margin-top: var(--spacing-small); } + +.topbar .profile .info .games .divider { + margin: 0 3px; +} + diff --git a/styles/loginregister.css b/styles/loginregister.css new file mode 100644 index 0000000..854aeb2 --- /dev/null +++ b/styles/loginregister.css @@ -0,0 +1,13 @@ +.centeredForm { + top: 50%; + transform: translateY(-50%); + margin: 0 auto; +} + +.centeredForm .input { + margin-bottom: var(--spacing-medium); +} + +.centeredForm .input:nth-last-of-type(2) { + margin-bottom: var(--spacing-large); +} diff --git a/styles/navbar.css b/styles/navbar.css new file mode 100644 index 0000000..baf9fd6 --- /dev/null +++ b/styles/navbar.css @@ -0,0 +1,36 @@ +.navbar .item { + margin: var(--spacing-medium); + margin-bottom: calc(var(--spacing-medium) + 4px); + display: block; +} + +.navbar .bottomArea { + position: absolute; + bottom: -4px; + left: 0; +} + +.navbar { + width: 48px; + line-height: 0; + overflow: visible; + white-space: nowrap; + z-index: 2; +} + +.navbar .bottomArea .item.notifications { + overflow: visible; +} + +.navbar .bottomArea .item.notifications > .iconWrapper { + cursor: pointer; +} + +.notifications .notificationDot { + background-color: var(--accent); + width: 8px; + height: 8px; + border-radius: 4px; + top: 2px; + right: 2px; +} diff --git a/styles/notifications.css b/styles/notifications.css new file mode 100644 index 0000000..db1598d --- /dev/null +++ b/styles/notifications.css @@ -0,0 +1,52 @@ +/* a tags are here because of css specificity */ +a.notificationsArea { + left: calc(48px + var(--spacing-medium)); + top: 92px; + transform: translateY(-100%); + text-align: left; + width: 400px; + height: 450px; +} + +a.notificationsArea .tuitje { + left: var(--spacing-medium); + bottom: 86px; + transform: translate(-100%, 100%) rotate(90deg); + fill: var(--gray-700); +} + +.notificationsArea .title { + margin-bottom: var(--spacing-large); +} + +.notificationsArea .inner { + overflow-y: scroll; + white-space: normal; + height: calc(450px - 4 * var(--spacing-large)); +} + +.notificationsArea .acceptable { + margin: 0; + margin-bottom: var(--spacing-medium); +} + +.notificationsArea .acceptable:last-child { + margin-bottom: 0; +} + +.notificationsArea .buttons .button { + margin: 0; +} + +.notificationsArea .buttons { + margin-top: var(--spacing-medium); +} + +.notificationsArea .noMsgsWrapper h1 { + white-space: nowrap; + user-select: none; +} + +.notificationsArea .acceptable .userInfo { + margin-left: var(--spacing-small); +} diff --git a/styles/readme.md b/styles/readme.md index 1d1e96f..58e6db0 100644 --- a/styles/readme.md +++ b/styles/readme.md @@ -1,103 +1,22 @@ -# Styles +# styles -This is the folder where all website css should be. Currently most of the css is -done using inline styles in the .tsx files, but they're being moved to seperate -.css files in here. A redesign has also been in the making and we now have a -style guide to follow while writing the website: +## dark -## Dark + - +## light -## Light - - + The new designs can be found on [Figma](https://www.figma.com/file/rTciVQApAe6cwrH1Prl5Wn/4-op-een-rij?node-id=0%3A1). The Figma designs are in dark mode, but the website will (for now) still have light mode as it's default theme. -After the move to .css files is done I'll also start working on the new theming -settings, and allow custom themes to be created with css that can easily be -selected in the settings page instead of having to copy-paste color codes -manually. - -## Show me the css - -These will eventually be moved to global.css when the move to .css files is -done. - -```css -:root { - --background: #E3E6EE; - --foreground: var(--gray-900); - - --accent: #7E3AA6; - - --error: #A63A4D; - --disk-b-alt: #582D35; - - --confirm: #3AA699; - --disk-a-alt: #244743; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: var(--gray-900); - --foreground: #FFFFF3; - - --accent: #AD34F3; - - --error: #FF4365; - --disk-b-alt: #F49BA1; - - --confirm: #00D9C0; - --disk-a-alt: #86F3F3; - } -} - -:root { - /* shade */ - --gray-100: #CED2DC; - --gray-200: #A9AFC0; - --gray-300: #757D92; - --gray-700: #293140; - --gray-800: #1F242D; - --gray-900: #141619; - - /* box-shadow */ - --drop-level-2: 0px 8px 32px 0px rgba(0, 0, 0, 0.3); - --drop-level-1: 0px 8px 12px -4px rgba(0, 0, 0, 0.15); - - /* border-radius */ - --tight-corner: 6px; - --loose-corner: 8px; - - /* margin/padding */ - --spacing-small: 6px; - --spacing-medium: 12px; - --spacing-large: 24px; -} - -html { - font-family: "Inter", sans-serif; - font-size: 14px; -} - -/* headings */ -h1, h2, h3 { font-weight: 700; } -h1 { font-size: 25px; } -h2 { font-size: 20px; } - -.subtile { - color: var(--gray-300); - font-style: italic; -} +I'm now working on the new theming settings, and allow custom themes to be +created with css that can easily be selected in the settings page instead of +having to copy-paste color codes manually. -/* no-select */ -.nosel { - user-select: none; - font-weight: 600; -} -``` +There are utility styles in ./utility.css. These are supposed to mimick +something like tailwind css. Most styles from the style guide are also defined +in ./global.css. diff --git a/styles/recentGames.css b/styles/recentGames.css new file mode 100644 index 0000000..c7150aa --- /dev/null +++ b/styles/recentGames.css @@ -0,0 +1,21 @@ +table .leftAlignedColumn { + text-align: left; + padding-left: var(--spacing-medium); +} + +table .rightAlignedColumn { + text-align: right; + padding-left: var(--spacing-medium); +} + +table.recentGames { + margin-top: var(--spacing-medium); +} + +table.recentGames a { + font-weight: 500; +} + +div.recentGames .nogames { + margin: 32px 64px; +} diff --git a/styles/search.css b/styles/search.css new file mode 100644 index 0000000..696294d --- /dev/null +++ b/styles/search.css @@ -0,0 +1,11 @@ +.bigSearchBar .input { + width: calc(100% - 2 * var(--spacing-medium) - 48px); +} + +.results .result .inner .userInfo { + left: calc(48px + var(--spacing-medium)); +} + +h1.noresults { + margin: 24px 32px; +} diff --git a/styles/settings.css b/styles/settings.css new file mode 100644 index 0000000..bcce5b5 --- /dev/null +++ b/styles/settings.css @@ -0,0 +1,8 @@ +.section.logout .button { + float: unset; +} + +.subsection { + margin-top: var(--spacing-large); + min-height: 40px; +} diff --git a/styles/toast.css b/styles/toast.css new file mode 100644 index 0000000..2ecbcfb --- /dev/null +++ b/styles/toast.css @@ -0,0 +1,66 @@ +.toast.error { + background-color: var(--error); +} +.toast.confirmation { + background-color: var(--confirm); +} +.toast.normal { + background-color: var(--gray-700); +} + +.toast.confirmation, +.toast.error { + color: var(--gray-900); +} + +html.dark .toast.confirmation, +html.dark .toast.error { + background-color: var(--gray-700); + color: var(--foreground); +} +html.dark .toast.error { + box-shadow: inset 0 0 0 2px var(--error); +} +html.dark .toast.confirmation { + box-shadow: inset 0 0 0 2px var(--confirm); +} + +.toast { + margin-bottom: var(--spacing-medium); +} + +#ToastArea { + white-space: nowrap; + bottom: var(--spacing-medium); + z-index: 1; + max-width: 600px; + width: calc(100% - 2 * 48px); + margin: 0 var(--spacing-large); +} + +.toast .inner { + line-height: 0; + min-height: 24px; +} + +.toast .inner.hasDescription { + min-height: 36px; +} + +.toast .inner .icon, +.toast .inner .content { + left: var(--spacing-medium); +} + +.toast .inner.hasIcon .content { + left: calc(36px + var(--spacing-medium)); +} + +.toast .inner .content h2 { + font-size: 1rem; +} + +.toast .inner .closeIcon { + right: var(--spacing-medium); + cursor: pointer; +} diff --git a/styles/ui.css b/styles/ui.css new file mode 100644 index 0000000..2736a2c --- /dev/null +++ b/styles/ui.css @@ -0,0 +1,118 @@ +.vierkant { + margin: var(--spacing-small); + display: inline-block; + position: relative; + box-sizing: border-box; +} + +html.dark .button { color: var(--foreground); } +.button { + background-color: var(--accent); + color: var(--gray-900); + text-align: center; + cursor: pointer; + position: relative; + text-decoration: none; + display: block; + user-select: none; + font-weight: 600; +} + +.button.iconlabel, +.button.colorpicker { + margin-left: var(--spacing-medium); +} + +.button.colorpicker { + --color: var(--gray-100); + --background: #ff00ff; + + background-color: var(--background); + color: var(--color); + border-width: 2px; + border-style: solid; + border-color: var(--color); +} + +.button.iconlabel .label { + margin: 3px; + margin-left: 8px; +} + +.button.colorpicker .labelwrapper { + width: 150px; + height: 24px; +} + +.button.colorpicker .labelwrapper .label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-feature-settings: "tnum", "ss01"; +} + +.bubble { + margin: 0; + overflow: visible; + left: 50%; + top: -24px; + transform: translateY(-100%) translateX(-50%); +} + +.bubble .tuitje { + bottom: -12px; + transform: translate(-50%, 0%) rotate(0deg); +} + +.dialogbox { width: 392px; } +.dialogbox > .title { margin-bottom: var(--spacing-large); } +.dialogbox .icon.close { + top: 25px; + right: 25px; + cursor: pointer; +} + +html.dark .dialogbox { background-color: var(--gray-700); } + +.searchBar { + overflow: hidden; +} + +.searchBar .button { + border-radius: 0; + padding: 10px; +} + +.searchBar .input { + width: calc(100% - 44px); + box-sizing: border-box; +} + +.checkbox { + cursor: pointer; +} + +.CenteredPageOuter { + margin: 0 auto; +} + +.CenteredPageInner { + margin: 0 var(--spacing-small); + line-height: 0; +} + +.pageTitle { + margin-left: var(--spacing-small); + margin-top: 32px; + margin-bottom: 64px; +} + +.accountAvatar { + background-size: cover; +} + +.accountAvatar.round { + border-radius: 9999999px; +} + diff --git a/styles/user.css b/styles/user.css new file mode 100644 index 0000000..d66fa94 --- /dev/null +++ b/styles/user.css @@ -0,0 +1,46 @@ +.infosection .inner { + height: 64px; +} + +/* editable field status */ +*[contenteditable] { + box-shadow: inset 0 0 0 0px var(--foreground); +} +*[contenteditable="true"]:focus { + box-shadow: inset 0 0 0 2px var(--accent); +} +*[contenteditable="true"] { + background-color: var(--background); + color: var(--text-alt); + padding: 8px; + box-shadow: inset 0 0 0 2px var(--foreground); +} + +.accountHeader .userInfo { + margin-left: var(--spacing-medium); + width: calc(100% - 128px - var(--spacing-medium)); +} + +.accountHeader .userInfo .username { + font-size: 2rem; +} + +.accountHeader .userInfo .status { + transition-duration: .3s; + margin-top: var(--spacing-small); +} + +.infoModule .iconWrapper { + left: 50%; + transform: translateX(-50%); +} + +.infoModule .labelWrapper { + top: calc(24px + var(--spacing-small)); +} + +.infoModule .labelWrapper .label { + top: 50%; + transform: translateY(-50%); +} + diff --git a/styles/utility.css b/styles/utility.css new file mode 100644 index 0000000..cc358e6 --- /dev/null +++ b/styles/utility.css @@ -0,0 +1,102 @@ +.drop-1 { box-shadow: var(--drop-level-1); } +.drop-2 { box-shadow: var(--drop-level-2); } + +.pad-s { padding: var(--spacing-small); } +.pad-m { padding: var(--spacing-medium); } +.pad-l { padding: var(--spacing-large); } + +.round-l { border-radius: var(--loose-corner); } +.round-t { border-radius: var(--tight-corner); } + +.bg-100 { background-color: var(--gray-100); } +.bg-200 { background-color: var(--gray-200); } +.bg-300 { background-color: var(--gray-300); } +.bg-700 { background-color: var(--gray-700); } +.bg-800 { background-color: var(--gray-800); } +.bg-900 { background-color: var(--gray-900); } + +.fg-100 { color: var(--gray-100); } +.fg-900 { color: var(--gray-900); } + +.outcome.win { color: var(--disk-a-alt); } +.outcome.lose { color: var(--disk-b-alt); } +.outcome.draw { color: var(--gray-600); } + +.posabs { position: absolute; } +.posrel { position: relative; } +.posfix { position: fixed; } + +.t0 { top: 0; } +.b0 { bottom: 0; } +.r0 { right: 0; } +.l0 { left: 0; } + +.h0 { left: 0; right: 0; } +.v0 { top: 0; bottom: 0; } + +.a0 { top: 0; bottom: 0; left: 0; right: 0; } + +.dispnone { display: none; } +.dispinbl { display: inline-block; } +.dispbl { display: block; } + +.valigntop { vertical-align: top; } +.valignsup { vertical-align: super; } + +.cpointer { cursor: pointer; } +.cdefault { cursor: default; } + +.center { text-align: center; } + +.floatr { float: right; } +.floatl { float: left; } + +.w100m2m { width: calc(100% - var(--spacing-medium)); } + +.w100vw { width: 100vw; } +.h100vh { height: 100vh; } + +.fullwidth { + width: 100%; + box-sizing: border-box; +} + +.subtile { + color: var(--gray-600); + font-style: italic; +} + +.nosel { + user-select: none; + font-weight: 600; +} + +.gg-l { grid-gap: var(--spacing-large) !important; } +.sidebyside { + display: grid; + grid-gap: var(--spacing-medium); + grid-auto-flow: column; +} + +.truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.abscenterh { + left: 50%; + transform: translateX(-50%); +} + +.abscenterv { + top: 50%; + transform: translateY(-50%); +} + +.abscenter { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + @@ -0,0 +1,43 @@ +pages: + +- 404 +- maintenance +- /logout +- privacy info + + + +unimplemented: + +- view friend/blocked/request list +- game stuff: + - rulesets + - timer / time limit + + + +website fixes: + +- unaccessible when not logged in: + - /user/* pages + - /settings +- unaccessible when logged in: + - /register + - /login + + + +backend fixes: + +- don't include unrated games in rating.py + + + +security measures: + +- repeated login prevention +- api rate limiting +- write unit tests +- bind tokens to ip adress (csrf) + + diff --git a/tsconfig.json b/tsconfig.json index 283a43f..6866dec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,12 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "preserve" + "jsx": "preserve", + "plugins": [ + { + "name": "typescript-plugin-css-modules" + } + ] }, "include": [ "src" @@ -3104,6 +3104,11 @@ bfj@^7.0.2: hoopy "^0.1.4" tryer "^1.0.1" +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3602,6 +3607,21 @@ chokidar@3.4.3, chokidar@^3.4.1: optionalDependencies: fsevents "~2.1.2" +"chokidar@>=3.0.0 <4.0.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.3.1" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3935,6 +3955,13 @@ cookie@^0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= +copy-anything@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.3.tgz#842407ba02466b0df844819bbe3baebbe5d45d87" + integrity sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ== + dependencies: + is-what "^3.12.0" + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -4145,6 +4172,13 @@ css-loader@4.3.0: schema-utils "^2.7.1" semver "^7.3.2" +css-parse@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" + integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= + dependencies: + css "^2.0.0" + css-prefers-color-scheme@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" @@ -4177,6 +4211,14 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" +css-selector-tokenizer@^0.7.0: + version "0.7.3" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" + integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== + dependencies: + cssesc "^3.0.0" + fastparse "^1.1.2" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -4428,6 +4470,13 @@ debug@^3.1.1, debug@^3.2.5: dependencies: ms "^2.1.1" +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4806,7 +4855,7 @@ dotenv-expand@5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== -dotenv@8.2.0: +dotenv@8.2.0, dotenv@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== @@ -4963,6 +5012,13 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +errno@^0.1.1: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -5546,6 +5602,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastparse@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" + integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== + fastq@^1.6.0: version "1.9.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" @@ -5846,6 +5907,11 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -5870,6 +5936,13 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +generic-names@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-1.0.3.tgz#2d786a121aee508876796939e8e3bff836c20917" + integrity sha1-LXhqEhruUIh2eWk56OO/+DbCCRc= + dependencies: + loader-utils "^0.2.16" + gensync@^1.0.0-beta.1: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -6379,6 +6452,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +icss-utils@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-3.0.1.tgz#ee70d3ae8cac38c6be5ed91e851b27eed343ad0f" + integrity sha1-7nDTroysOMa+XtkehRsn7tNDrQ8= + dependencies: + postcss "^6.0.2" + icss-utils@^4.0.0, icss-utils@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" @@ -6420,6 +6500,11 @@ image-blob-reduce@^2.2.2: dependencies: pica "^6.1.1" +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + immer@7.0.9: version "7.0.9" resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e" @@ -6904,6 +6989,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-what@^3.12.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" + integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7524,6 +7614,11 @@ json3@^3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -7702,6 +7797,22 @@ last-call-webpack-plugin@^3.0.0: lodash "^4.17.5" webpack-sources "^1.1.0" +less@^3.11.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" + integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== + dependencies: + copy-anything "^2.0.1" + tslib "^1.10.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + native-request "^1.0.5" + source-map "~0.6.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -7769,6 +7880,16 @@ loader-utils@2.0.0, loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" @@ -7806,6 +7927,11 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7841,6 +7967,11 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loglevel@^1.6.8: version "1.7.0" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" @@ -7886,7 +8017,7 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" -make-dir@^2.0.0: +make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -8045,7 +8176,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, dependencies: mime-db "1.44.0" -mime@1.6.0: +mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -8187,7 +8318,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -8277,6 +8408,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +native-request@^1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" + integrity sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag== + native-url@0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" @@ -9305,6 +9441,13 @@ postcss-env-function@^2.0.2: postcss "^7.0.2" postcss-values-parser "^2.0.0" +postcss-filter-plugins@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-3.0.1.tgz#9d226e946d56542ab7c26123053459a331df545d" + integrity sha512-tRKbW4wWBEkSSFuJtamV2wkiV9rj6Yy7P3Y13+zaynlPEEZt8EgYKn3y/RBpMeIhNmHXFlSdzofml65hD5OafA== + dependencies: + postcss "^6.0.14" + postcss-flexbugs-fixes@4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz#9218a65249f30897deab1033aced8578562a6690" @@ -9340,6 +9483,26 @@ postcss-gap-properties@^2.0.0: dependencies: postcss "^7.0.2" +postcss-icss-keyframes@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/postcss-icss-keyframes/-/postcss-icss-keyframes-0.2.1.tgz#80c4455e0112b0f2f9c3c05ac7515062bb9ff295" + integrity sha1-gMRFXgESsPL5w8Bax1FQYruf8pU= + dependencies: + icss-utils "^3.0.1" + postcss "^6.0.2" + postcss-value-parser "^3.3.0" + +postcss-icss-selectors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/postcss-icss-selectors/-/postcss-icss-selectors-2.0.3.tgz#27fa1afcaab6c602c866cbb298f3218e9bc1c9b3" + integrity sha1-J/oa/Kq2xgLIZsuymPMhjpvBybM= + dependencies: + css-selector-tokenizer "^0.7.0" + generic-names "^1.0.2" + icss-utils "^3.0.1" + lodash "^4.17.4" + postcss "^6.0.2" + postcss-image-set-function@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" @@ -9365,7 +9528,7 @@ postcss-lab-function@^2.0.1: postcss "^7.0.2" postcss-values-parser "^2.0.0" -postcss-load-config@^2.0.0: +postcss-load-config@^2.0.0, postcss-load-config@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== @@ -9777,7 +9940,7 @@ postcss-unique-selectors@^4.0.1: postcss "^7.0.0" uniqs "^2.0.0" -postcss-value-parser@^3.0.0: +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== @@ -9824,6 +9987,15 @@ postcss@8.1.7: nanoid "^3.1.16" source-map "^0.6.1" +postcss@^6.0.14, postcss@^6.0.2: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + postcss@^7, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" @@ -10559,6 +10731,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reserved-words@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" + integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE= + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -10766,7 +10943,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -10813,6 +10990,13 @@ sass-loader@8.0.2: schema-utils "^2.6.1" semver "^6.3.0" +sass@^1.26.5: + version "1.32.10" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.10.tgz#d40da4e20031b450359ee1c7e69bc8cc89569241" + integrity sha512-Nx0pcWoonAkn7CRp0aE/hket1UP97GiR1IFw3kcjV3pnenhWgZEWUf0ZcfPOV2fK52fnOcK3JdC/YYZ9E47DTQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -11653,7 +11837,21 @@ stylis@3.5.4: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== -supports-color@^5.3.0: +stylus@^0.54.7: + version "0.54.8" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" + integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== + dependencies: + css-parse "~2.0.0" + debug "~3.1.0" + glob "^7.1.6" + mkdirp "~1.0.4" + safer-buffer "^2.1.2" + sax "~1.2.4" + semver "^6.3.0" + source-map "^0.7.3" + +supports-color@^5.3.0, supports-color@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -12120,6 +12318,25 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-plugin-css-modules@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/typescript-plugin-css-modules/-/typescript-plugin-css-modules-3.2.0.tgz#44241064395d565f242689b96eb50bf413e68ddf" + integrity sha512-oMe5IDKuPvLBOeaYupAqqq8UMTu7Jo5e0InqkQSSKXuZCcRm0+LQfUVMyM62IFpgzxHLuN32a7YdBxAaSXZrRQ== + dependencies: + dotenv "^8.2.0" + icss-utils "^4.1.1" + less "^3.11.1" + lodash.camelcase "^4.3.0" + postcss "^7.0.27" + postcss-filter-plugins "^3.0.1" + postcss-icss-keyframes "^0.2.1" + postcss-icss-selectors "^2.0.3" + postcss-load-config "^2.1.0" + reserved-words "^0.1.2" + sass "^1.26.5" + stylus "^0.54.7" + tsconfig-paths "^3.9.0" + typescript@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" |