Skip to content

Commit 2076a42

Browse files
author
Roman Babaev
committed
UI Improvements:
- Iphone compatibility - Zoom
1 parent fd0fb48 commit 2076a42

File tree

12 files changed

+183
-53
lines changed

12 files changed

+183
-53
lines changed

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<head>
55
<meta charset="utf-8">
6-
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no" />
77
<meta name="theme-color" content="#000000">
88
<!--
99
manifest.json provides metadata used when your web app is added to the

public/style.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
html {
2+
-webkit-text-size-adjust: none;
3+
}
4+
15
body {
26
margin: 0;
7+
overflow: hidden;
8+
}
9+
10+
#root {
11+
width: 100%;
12+
height: 100%;
313
}

src/App/index.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
import React from "react";
22
import { Arena } from "../Arena";
3+
import { FooterPanel } from "../FooterPanel";
34
import { Hat } from "../Hat";
45
import { Minesweeper } from "../core";
56
import { Options } from "../Options";
67
import "./styles.css";
78

9+
function preventZoom(event) {
10+
event = event.originalEvent || event;
11+
if(event.scale !== undefined && event.scale !== 1) event.preventDefault();
12+
}
13+
814
export default class App extends React.Component {
915
constructor(props) {
1016
super(props);
1117
this.state = {
12-
showOptions: false
18+
showOptions: false,
19+
zoom: 1
1320
};
1421

1522
this.game = new Minesweeper(null, null, this.forceUpdate.bind(this));
1623
}
1724

25+
componentDidMount() {
26+
document.addEventListener('touchmove', preventZoom, { passive: false });
27+
document.addEventListener('dblclick', preventZoom, { passive: false });
28+
}
29+
30+
componentWillUnmount() {
31+
document.removeEventListener('touchmove', preventZoom, { passive: false });
32+
document.removeEventListener('dblclick', preventZoom, { passive: false });
33+
}
34+
1835
toggleOptions = () =>
1936
this.setState(({ showOptions }) => ({ showOptions: !showOptions }));
2037

38+
handleZoomChange = zoom => zoom && this.setState({ zoom });
39+
2140
newGame = (arena = [10, 10], mines = 25) => this.game.configure(arena, mines);
2241

2342
render() {
2443
const { flaggingMode, gameState, minesCountTotal, arena, flagged, timerValue } = this.game.getStats();
25-
const { showOptions } = this.state;
44+
const { showOptions, zoom } = this.state;
2645

2746
return (
2847
<div className="App">
2948
<Hat
30-
flaggingMode={flaggingMode}
3149
gameState={gameState}
3250
timerValue={timerValue}
3351
minesLeftCount={minesCountTotal - flagged}
3452
onResetClick={this.game.reset}
3553
onOptionsClick={this.toggleOptions}
36-
onFlaggingSwitch={this.game.toggleFlaggingMode}
3754
/>
3855
<Arena
3956
gameState={gameState}
@@ -42,6 +59,13 @@ export default class App extends React.Component {
4259
onCellOpen={this.game.handleCellClick}
4360
onCellFlag={this.game.flagCell}
4461
onResetClick={this.game.reset}
62+
zoom={zoom}
63+
/>
64+
<FooterPanel
65+
flaggingMode={flaggingMode}
66+
zoom={zoom}
67+
onFlaggingSwitch={this.game.toggleFlaggingMode}
68+
onZoomChange={this.handleZoomChange}
4569
/>
4670
{showOptions && (
4771
<Options

src/App/styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
.App {
2+
position: absolute;
3+
top: 0;
4+
bottom: 0;
5+
left: 0;
6+
right: 0;
27
display: flex;
38
flex-direction: column;
49
align-items: center;
10+
overflow: hidden;
511
font-family: sans-serif;
612
text-align: center;
713
}

src/Arena/index.js

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,86 @@ import "./styles.css";
66
const b = b_.lock("Arena");
77
const cellSize = 28;
88

9-
export const Arena = ({
10-
cells = [],
11-
size,
12-
onCellFlag,
13-
onCellOpen,
14-
gameState,
15-
onResetClick
16-
}) => (
17-
<div
18-
className={b()}
19-
tabIndex={0}
20-
onKeyPress={e => e.charCode === 32 && onResetClick()}
21-
>
22-
<div
23-
className={b('inner')}
24-
style={{
25-
width: size[0] * cellSize,
26-
height: size[1] * cellSize
27-
}}
28-
>
29-
{cells.map((cell, i) => {
30-
return (
31-
<ArenaCell
32-
cell={cell}
33-
cellSize={cellSize}
34-
i={i}
35-
key={i}
36-
onCellOpen={onCellOpen}
37-
onCellFlag={onCellFlag}
38-
gameState={gameState}
39-
/>
40-
);
41-
})}
42-
</div>
43-
</div>
44-
);
9+
export class Arena extends React.Component {
10+
static defaultProps = { zoom: 1 };
11+
12+
state = {
13+
width: null,
14+
height: null
15+
};
16+
17+
innerRef = React.createRef();
18+
19+
componentDidMount() {
20+
const innerElement = this.innerRef && this.innerRef.current;
21+
22+
if (innerElement) {
23+
const { width, height } = innerElement.getBoundingClientRect();
24+
this.setState({ width, height });
25+
}
26+
}
27+
28+
componentDidUpdate(prevProps) {
29+
if (prevProps.zoom !== this.props.zoom) {
30+
const innerElement = this.innerRef && this.innerRef.current;
31+
32+
if (innerElement) {
33+
const { width, height } = innerElement.getBoundingClientRect();
34+
this.setState({ width, height });
35+
}
36+
}
37+
}
38+
39+
render() {
40+
const {
41+
cells = [],
42+
size,
43+
onCellFlag,
44+
onCellOpen,
45+
gameState,
46+
onResetClick,
47+
zoom
48+
} = this.props;
49+
50+
const {
51+
width,
52+
height
53+
} = this.state;
54+
55+
return (
56+
<div
57+
className={b()}
58+
tabIndex={0}
59+
onKeyPress={e => e.charCode === 32 && onResetClick()}
60+
>
61+
<div className={b('wrap')} style={{ width, height }}>
62+
<div
63+
className={b('inner')}
64+
style={{
65+
width: size[0] * cellSize,
66+
height: size[1] * cellSize,
67+
transform: `scale(${zoom})`
68+
}}
69+
ref={this.innerRef}
70+
>
71+
{cells.map((cell, i) => {
72+
return (
73+
<ArenaCell
74+
cell={cell}
75+
cellSize={cellSize}
76+
i={i}
77+
key={i}
78+
onCellOpen={onCellOpen}
79+
onCellFlag={onCellFlag}
80+
gameState={gameState}
81+
/>
82+
);
83+
})}
84+
</div>
85+
</div>
86+
</div>
87+
);
88+
}
89+
}
4590

4691
export const PureArena = memo(Arena);

src/Arena/styles.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
.Arena {
22
display: flex;
3-
/* justify-content: center; */
4-
max-width: 100%;
3+
flex-grow: 1;
4+
width: 100%;
55
overflow: auto;
66
outline: none;
7+
user-select: none;
8+
}
9+
10+
.Arena__wrap {
11+
margin: 12px auto;
712
}
813

914
.Arena__inner {
1015
display: flex;
1116
flex-wrap: wrap;
1217
flex-shrink: 0;
13-
border: solid 1px #ccc;
14-
margin: 12px;
15-
user-select: none;
18+
border: solid 12px #eee;
19+
transform-origin: 0 0;
1620
}

src/ArenaCell/index.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import b_ from "b_";
33
import "./styles.css";
44

@@ -12,6 +12,8 @@ export const ArenaCell = ({
1212
onCellOpen,
1313
gameState
1414
}) => {
15+
const [pressed, setPressed] = useState(false);
16+
1517
const opened = cell.opened || (gameState === "lost" && ((cell.mined && !cell.flagged) || (!cell.mined && cell.flagged)));
1618

1719
const content = opened
@@ -26,15 +28,24 @@ export const ArenaCell = ({
2628

2729
return (
2830
<div
29-
className={b({ opened: opened, closed: !opened })}
31+
className={b({
32+
opened: opened,
33+
closed: !opened,
34+
pressed: pressed && !opened
35+
})}
3036
style={{ width: cellSize, height: cellSize }}
3137
key={i}
3238
data-neighbor-mines={cell.neighborMines}
3339
onClick={() => onCellOpen(i)}
40+
onMouseDown={e => !pressed && (e.buttons & 1) && setPressed(true)}
41+
onMouseUp={() => pressed && setPressed(false)}
42+
onMouseLeave={e => pressed && setPressed(false)}
43+
onMouseEnter={e => (e.buttons & 1) && setPressed(true)}
3444
onContextMenu={e => {
3545
e.preventDefault();
3646
onCellFlag(i);
3747
}}
48+
// onTouchStart={() => onCellOpen(i)}
3849
>
3950
{content}
4051
</div>

src/ArenaCell/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
box-sizing: border-box;
66
cursor: default;
77
font-size: 12px;
8+
touch-action: manipulation;
89
}
910

1011
.ArenaCell_opened {
@@ -50,3 +51,7 @@
5051
border: outset 2px #fff;
5152
background-color: #ccc;
5253
}
54+
55+
.ArenaCell_pressed {
56+
border-color: transparent;
57+
}

src/FooterPanel/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from "react";
2+
import { Button, ControlsPanel, Switcher } from "@tarantool.io/ui-kit";
3+
import b_ from 'b_';
4+
import "./styles.css";
5+
6+
const b = b_.lock('FooterPanel');
7+
8+
export const FooterPanel = ({ flaggingMode, zoom, onFlaggingSwitch, onZoomChange }) => (
9+
<ControlsPanel
10+
className={b()}
11+
controls={[
12+
<Button className={b('zoomButton')} text='+' size='xs' onClick={() => onZoomChange(zoom + 0.5)} />,
13+
<Button className={b('zoomButton')} text='-' size='xs' onClick={() => onZoomChange(zoom - 0.5)} />,
14+
<Switcher onChange={onFlaggingSwitch} title="Flag" checked={flaggingMode}>Flag</Switcher>
15+
]}
16+
/>
17+
);

src/FooterPanel/styles.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.FooterPanel {
2+
flex-shrink: 0;
3+
margin: 6px;
4+
user-select: none;
5+
}
6+
7+
.FooterPanel__zoomButton {
8+
width: 35px;
9+
}

0 commit comments

Comments
 (0)