1import { Button, createStyles, makeStyles, Theme, Typography } from '@material-ui/core';
2import { grey, orange, red } from '@material-ui/core/colors';
3import { Fireworks } from 'fireworks/lib/react';
4import * as React from 'react';
5import isEqual from 'react-fast-compare';
6
7import { isDefined, noop } from '../common';
8import { StateBoard, StateTile } from '../protocol';
9import { TeamHue, teamSpecs } from '../teams';
10import { AspectDiv } from './aspectDiv';
11
12function neutralStyle(revealed: boolean, spymaster: boolean): React.CSSProperties {
13 return {
14 color: revealed ? 'white' : 'black',
15 backgroundColor: grey[revealed ? 500 : 200],
16 fontWeight: spymaster ? 'bold' : undefined,
17 };
18}
19
20function bombStyle(revealed: boolean, spymaster: boolean): React.CSSProperties {
21 return {
22 color: revealed ? 'white' : grey[900],
23 backgroundColor: grey[revealed ? 900 : 700],
24 fontWeight: spymaster ? 'bold' : undefined,
25 };
26}
27
28function teamStyle(teamHue: TeamHue, revealed: boolean, spymaster: boolean): React.CSSProperties {
29 return {
30 color: revealed ? 'white' : teamHue[900],
31 backgroundColor: teamHue[revealed ? 600 : 200],
32 fontWeight: spymaster ? 'bold' : undefined,
33 };
34}
35
36function tileStyle(tile: StateTile, spymaster: boolean): React.CSSProperties {
37 if (!isDefined(tile.view) || tile.view.neutral) {
38 return neutralStyle(tile.revealed, spymaster);
39 }
40
41 if (tile.view.bomb) {
42 return bombStyle(tile.revealed, spymaster);
43 }
44
45 const teamHue = teamSpecs[tile.view.team].hue;
46 return teamStyle(teamHue, tile.revealed, spymaster);
47}
48
49const useTileStyles = makeStyles((theme: Theme) =>
50 createStyles({
51 button: {
52 position: 'absolute',
53 top: 0,
54 left: 0,
55 height: '100%',
56 width: '100%',
57 textAlign: 'center',
58 [theme.breakpoints.down('sm')]: {
59 padding: '6px',
60 },
61 },
62 typo: {
63 wordWrap: 'break-word',
64 width: '100%',
65 fontSize: theme.typography.h6.fontSize,
66 [theme.breakpoints.down('sm')]: {
67 fontSize: theme.typography.button.fontSize,
68 lineHeight: '1rem',
69 },
70 },
71 explosionWrapper: {
72 zIndex: 100,
73 position: 'absolute',
74 margin: 'auto',
75 height: 0,
76 width: 0,
77 top: 0,
78 left: 0,
79 bottom: 0,
80 right: 0,
81 },
82 explosion: {
83 transform: 'translate(-50%, -50%)',
84 pointerEvents: 'none',
85 },
86 })
87);
88
89const fireworksProps = {
90 interval: 0,
91 colors: [red[700], orange[800], grey[500]],
92 x: 0,
93 y: 0,
94};
95
96interface TileProps {
97 row: number;
98 col: number;
99 onClick: (row: number, col: number) => void;
100 tile: StateTile;
101 spymaster: boolean;
102 myTurn: boolean;
103 winner: boolean;
104}
105
106const Tile = React.memo(function Tile({ row, col, onClick, tile, spymaster, myTurn, winner }: TileProps) {
107 const classes = useTileStyles();
108
109 const bombRevealed = !!(tile.revealed && tile.view?.bomb);
110 const alreadyExploded = React.useRef(bombRevealed);
111 const explode = bombRevealed && !alreadyExploded.current;
112 const disabled = spymaster || !myTurn || winner || tile.revealed;
113
114 const reveal = React.useMemo(() => {
115 if (disabled) {
116 return noop;
117 }
118 return () => onClick(row, col);
119 }, [disabled, row, col, onClick]);
120
121 return (
122 <AspectDiv aspectRatio="75%">
123 <Button
124 type="button"
125 variant="contained"
126 className={classes.button}
127 onClick={reveal}
128 style={tileStyle(tile, spymaster)}
129 disabled={disabled}
130 >
131 <Typography variant="h6" className={classes.typo}>
132 {tile.word}
133 </Typography>
134 </Button>
135 {explode ? (
136 <div className={classes.explosionWrapper}>
137 <div className={classes.explosion}>
138 <Fireworks {...fireworksProps} />
139 </div>
140 </div>
141 ) : null}
142 </AspectDiv>
143 );
144}, isEqual);
145
146export interface BoardProps {
147 words: StateBoard;
148 spymaster: boolean;
149 myTurn: boolean;
150 winner: boolean;
151 onClick: (row: number, col: number) => void;
152}
153
154const useStyles = makeStyles((theme: Theme) =>
155 createStyles({
156 root: {
157 display: 'grid',
158 gridGap: theme.spacing(0.5),
159 [theme.breakpoints.up('lg')]: {
160 gridGap: theme.spacing(1),
161 },
162 gridTemplateRows: (props: BoardProps) => `repeat(${props.words.length}, 1fr)`,
163 gridTemplateColumns: (props: BoardProps) => `repeat(${props.words[0].length}, 1fr)`,
164 },
165 })
166);
167
168export const Board = React.memo(function Board(props: BoardProps) {
169 const classes = useStyles(props);
170
171 return (
172 <div className={classes.root}>
173 {props.words.map((arr, row) =>
174 arr.map((tile, col) => (
175 <div key={row * props.words.length + col}>
176 <Tile
177 row={row}
178 col={col}
179 onClick={props.onClick}
180 tile={tile}
181 spymaster={props.spymaster}
182 myTurn={props.myTurn}
183 winner={props.winner}
184 />
185 </div>
186 ))
187 )}
188 </div>
189 );
190}, isEqual);