import { Button, ButtonGroup, createStyles, Grid, IconButton, makeStyles, Paper, Slider, Theme, Typography, } from '@material-ui/core'; import { green, orange } from '@material-ui/core/colors'; import { Add, ArrowBack, Delete, Link, Person, Search, Timer, TimerOff } from '@material-ui/icons'; import { ok as assertTrue } from 'assert'; import isArray from 'lodash/isArray'; import range from 'lodash/range'; import { DropzoneDialog } from 'material-ui-dropzone'; import * as React from 'react'; import { isDefined } from '../common'; import { Board } from '../components/board'; import { ClipboardButton } from '../components/clipboard'; import { useServerTime } from '../hooks/useServerTime'; import { State, StatePlayer, StateTimer, WordPack } from '../protocol'; import { teamSpecs } from '../teams'; export interface Sender { reveal: (row: number, col: number) => void; newGame: () => void; endTurn: () => void; changeNickname: (nickname: string) => void; changeRole: (spymaster: boolean) => void; changeTeam: (team: number) => void; randomizeTeams: () => void; changePack: (num: number, enable: boolean) => void; changeTurnMode: (timed: boolean) => void; changeTurnTime: (seconds: number) => void; addPacks: (packs: { name: string; words: string[] }[]) => void; removePack: (num: number) => void; } export interface GameViewProps { roomID: string; leave: () => void; send: Sender; state: State; pState: StatePlayer; pTeam: number; } const useCenterStyles = makeStyles((_theme: Theme) => createStyles({ blink: { animation: '$blinker 0.5s cubic-bezier(.5, 0, 1, 1) infinite alternate', }, '@keyframes blinker': { to: { opacity: 0, }, }, }) ); const CenterText = ({ winner, timer, turn }: State) => { const classes = useCenterStyles(); const [countdown, setCountdown] = React.useState(); const { now } = useServerTime(); React.useEffect(() => { const updateCountdown = () => { if (isDefined(winner)) { setCountdown(undefined); return; } if (!isDefined(timer)) { if (countdown !== undefined) { setCountdown(undefined); } return; } const deadline = timer.turnEnd; const diff = deadline.getTime() - now(); const between = Math.floor(diff / 1000); if (between < 0) { if (countdown === 0) { return; } setCountdown(0); } else { setCountdown(between); } }; updateCountdown(); const interval = window.setInterval(() => { updateCountdown(); }, 200); return () => window.clearInterval(interval); }, [countdown, setCountdown, winner, timer, now]); const centerText = React.useMemo(() => { const text = isDefined(winner) ? `${teamSpecs[winner].name} wins!` : `${teamSpecs[turn].name}'s turn`; if (!isDefined(countdown) || isDefined(winner)) { return text; } return `${text} [${countdown}s]`; }, [winner, turn, countdown]); return (

{centerText}

); }; const Header = ({ send, state, pState, pTeam }: GameViewProps) => { const myTurn = state.turn === pTeam; return (

{state.wordsLeft.map((n, team) => { return ( {team !== 0 ? - : null} {n} ); })}

); }; const sliderMarks = range(30, 301, 30).map((v) => ({ value: v })); interface TimerSliderProps { id: string; timer: StateTimer; onCommit: (value: number) => void; } const TimerSlider = ({ timer, onCommit, id }: TimerSliderProps) => { // Keep around the original value when this component is created. // This prevents React from complaining about the defaultValue // changing when the overall state changes. const defaultValue = React.useRef(timer.turnTime); const [value, setValue] = React.useState(timer.turnTime); const valueStr = React.useMemo(() => { switch (value) { case 30: return '30 seconds'; case 60: return '60 seconds'; default: if (value % 60 === 0) { return `${value / 60} minutes`; } return `${(value / 60).toFixed(1)} minutes`; } }, [value]); return ( <> Timer: {valueStr} { assertTrue(!isArray(v)); if (v !== value) { setValue(v); } }} onChangeCommitted={(_e, v) => { assertTrue(!isArray(v)); onCommit(v); }} /> ); }; const useSidebarStyles = makeStyles((_theme: Theme) => createStyles({ dropzone: { backgroundColor: 'initial', }, previewGrid: { width: '100%', }, }) ); const Sidebar = ({ send, state, pState, pTeam }: GameViewProps) => { const classes = useSidebarStyles(); const teams = state.teams; const lists = state.lists; const wordCount = React.useMemo( () => lists.reduce((curr, l) => { if (l.enabled) { return curr + l.count; } return curr; }, 0), [lists] ); const [uploadOpen, setUploadOpen] = React.useState(false); return ( <>

Teams

{teams.map((team, i) => ( {team.map((member, j) => ( {member.spymaster ? `[${member.nickname}]` : member.nickname} ))} ))}

Packs

{wordCount} words in the selected packs.

{lists.map((pack, i) => (
{pack.custom && !pack.enabled ? ( send.removePack(i)}> ) : null}
))} {lists.length >= 10 ? null : ( <> setUploadOpen(false)} onSave={async (files) => { setUploadOpen(false); const packs: WordPack[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const name = file.name.substring(0, file.name.lastIndexOf('.')) || file.name; const text = (await file.text()).trim(); packs.push({ name, words: text.split('\n') }); } send.addPacks(packs); }} /> )}
{!isDefined(state.timer) ? null : (
)} ); }; const Board2 = ({ send, state, pState, pTeam }: GameViewProps) => { const myTurn = state.turn === pTeam; return ( myTurn && !pState.spymaster && send.reveal(row, col)} spymaster={pState.spymaster} myTurn={myTurn} winner={isDefined(state.winner)} /> ); }; const Footer = ({ send, state, pState }: GameViewProps) => { const end = isDefined(state.winner); return ( ); }; const useStyles = makeStyles((theme: Theme) => createStyles({ root: { height: '100vh', display: 'flex', }, wrapper: { width: '100%', textAlign: 'center', paddingLeft: theme.spacing(2), paddingRight: theme.spacing(2), // Emulate the MUI Container component. maxWidth: `1560px`, // TODO: Surely this shouldn't be hardcoded. margin: 'auto', // marginRight: 'auto', display: 'grid', gridGap: theme.spacing(2), gridTemplateAreas: '"header" "board" "footer" "sidebar"', [theme.breakpoints.down('lg')]: { paddingTop: theme.spacing(5), }, [theme.breakpoints.up('lg')]: { gridTemplateColumns: '1fr 4fr 1fr', gridTemplateRows: '1fr auto 1fr', gridTemplateAreas: '". header ." "sidebar board ." ". footer ."', }, }, header: { gridArea: 'header', }, board: { gridArea: 'board', }, footer: { gridArea: 'footer', }, sidebar: { gridArea: 'sidebar', }, }) ); export const GameView = (props: GameViewProps) => { const classes = useStyles(); return (
} />
); };