Skip to content

Commit

Permalink
[Feature] Gameboard (part 1) (#2)
Browse files Browse the repository at this point in the history
* [Feature] Add game board. Now we can to start and restart game. I use mock data yet

* [Feature][Modules] Add Ant Design for base stylization

* [Bugfix][Environment] Hot reload works incorrectly after version 2.24.9 ( gatsbyjs/gatsby#26192 )

* [Bugfix] Stub for 'Error: The result of this StaticQuery could not be fetched' (gatsbyjs/gatsby#24902)
  • Loading branch information
LyulyaevMaxim authored Aug 10, 2020
1 parent ea3fafb commit a4bc865
Show file tree
Hide file tree
Showing 21 changed files with 1,125 additions and 131 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
yarn-error.log
.pnp.*

/cypress
Expand Down
4 changes: 4 additions & 0 deletions configs/linters/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
'one-var': 0,
'spaced-comment': 0,
'no-param-reassign': 1,
'default-case': 1,
'no-use-before-define': 0,
'no-unused-vars': 1,
'no-shadow': 1,
Expand All @@ -46,7 +47,10 @@ module.exports = {
'react/destructuring-assignment': 0,
'react/no-children-prop': 0,
'react/jsx-props-no-spreading': 0,
'react/jsx-pascal-case': 0,
'react/button-has-type': 0,
'react/state-in-constructor': 0,
'react/static-property-placement': 0,
'react/require-default-props': 2,
'react/jsx-max-depth': [1, { max: 5 }],
'react-hooks/exhaustive-deps': 1,
Expand Down
5 changes: 3 additions & 2 deletions configs/linters/.stylelintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ module.exports = {
format: 'hsl',
},
'aditayvm/at-rule-no-children': [{ severity: 'warning' }],
'order/order': ['custom-properties', 'dollar-variables', 'declarations', 'at-rules', 'rules'],
'order/order': null, //['custom-properties', 'dollar-variables', 'declarations', 'at-rules', 'rules'],
// 'order/properties-alphabetical-order': true,
'selector-type-no-unknown': [true, { ignore: ['custom-elements'] }],
'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$', //lowerCamelCase
// 'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$', //lowerCamelCase
'block-no-empty': null,
'at-rule-no-unknown': null,
'max-nesting-depth': null,
'no-descending-specificity': null,
'no-missing-end-of-source-newline': null,
'comment-empty-line-before': null,
'comment-whitespace-inside': null,
'value-keyword-case': null,
...(withJS && { 'no-empty-source': null }),
},
}
1 change: 1 addition & 0 deletions gatsby-browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* https://www.gatsbyjs.org/docs/browser-apis/ */
import 'antd/dist/antd.css'
import 'modules/stylization/css-reset.css'
import { detectBrowser } from 'modules/polyfills'

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"babel-loader": "8.1.0",
"babel-preset-gatsby": "0.5.5",
"cross-env": "7.0.2",
"gatsby": "2.24.36",
"gatsby": "2.24.9",
"gatsby-plugin-manifest": "2.4.22",
"gatsby-plugin-offline": "3.2.22",
"gatsby-plugin-sharp": "2.6.25",
Expand Down
58 changes: 58 additions & 0 deletions src/components/page-root/GameBoard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react'
import { css } from 'styled-components'
import { useSelector } from 'react-redux'
import { flowRight } from 'lodash-es'
import { withErrorBoundaries } from 'helpers/decorators'
import { battleshipGameSelectors } from 'store/battleshipGame/selectors'
import { GameBoardCell } from './GameBoardCell'

function GameBoard() {
const gameBoardSize = useSelector(battleshipGameSelectors.getGameBoardSize),
gameBoardMap = useSelector(battleshipGameSelectors.getGameBoardData),
[rowsSize, columnsSize] = [gameBoardSize, gameBoardSize],
Cells = React.useMemo(() => {
const cells = []
for (let rowIndex = 0; rowIndex < rowsSize; rowIndex += 1) {
for (let columnIndex = 0; columnIndex < columnsSize; columnIndex += 1) {
const cell = gameBoardMap[rowIndex][columnIndex]
cells.push(
<GameBoardCell
rowIndex={rowIndex}
columnIndex={columnIndex}
shipId={cell.shipId}
isShot={cell.isShot}
key={`cell-${rowIndex}-${columnIndex}`}
/>
)
}
}
return cells
}, [gameBoardMap, rowsSize, columnsSize])

return (
<section css={boardStyles.wrapper as any} {...{ rowsSize, columnsSize }} data-testid="gameBoard-wrapper">
{Cells}
</section>
)
}

const boardStyles = {
wrapper: css<{ rowsSize: number; columnsSize: number }>`
display: grid;
grid-area: game-board;
${(props) => {
let dynamicStyles = ''
const cellSize = '2rem'
dynamicStyles += `
grid-template-columns: repeat(${props.columnsSize}, ${cellSize});
grid-template-rows: repeat(${props.rowsSize}, ${cellSize});
`
return dynamicStyles
}};
`,
}

const withDecorators = flowRight(withErrorBoundaries(), React.memo)(GameBoard)
export { withDecorators as GameBoard }
95 changes: 95 additions & 0 deletions src/components/page-root/GameBoardCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react'
import { css } from 'styled-components'
import { Checkbox } from 'antd'
import { useSelector, useDispatch } from 'react-redux'
import { battleshipGameSelectors } from 'store/battleshipGame/selectors'
import { battleshipGameAPI } from 'store/battleshipGame/actions'
import { NGameBoard } from 'store/battleshipGame/@types'

interface IGameBoardCell extends NGameBoard.ICell {
rowIndex: NGameBoard.rowIndex
columnIndex: NGameBoard.columnIndex
}

export const GameBoardCell: React.FC<IGameBoardCell> = React.memo((props) => {
const { rowIndex, columnIndex, shipId, isShot } = props

const dispatch = useDispatch(),
onCellClick = React.useCallback(
(event: any) => {
event.stopPropagation()
dispatch(battleshipGameAPI.shotCell({ payload: { rowIndex, columnIndex } }))
},
[dispatch, rowIndex, columnIndex]
)

const ship = useSelector(battleshipGameSelectors.getShip({ shipId })),
cell = useSelector(battleshipGameSelectors.getShipCell({ shipId, rowIndex, columnIndex })),
cellBorders = cell?.borders ?? Object.prototype,
isShowHints = useSelector(battleshipGameSelectors.isShowHints({ shipId }))

const styles = React.useMemo(
() => [
cellStyles.cell,
shipId &&
css`
--cellsAlive: ${ship?.cellsAlive};
`,
isShowHints && cellStyles.cellWithHints,
],
[shipId, isShowHints]
),
className = React.useMemo(
() =>
[
shipId &&
Object.keys(cellBorders)
.map((borderPosition) => `border-${borderPosition}`)
.join(' '),
]
.filter(Boolean)
.join(''),
[shipId, cellBorders]
)

return (
<Checkbox css={styles as any} className={className} onChange={onCellClick} disabled={isShot} checked={isShot} />
)
})

const cellStyles = {
cell: css`
&.ant-checkbox-wrapper {
margin: 0;
}
.ant-checkbox {
display: flex;
}
.ant-checkbox,
.ant-checkbox-inner {
width: 100%;
height: 100%;
}
.ant-checkbox-disabled .ant-checkbox-inner {
background-color: hsl(0 70% calc(var(--cellsAlive) * 20%));
}
&[class*='border-'] .ant-checkbox-disabled .ant-checkbox-inner {
border: initial !important;
}
${['left', 'right', 'top', 'bottom'].reduce((dynamicStyles, currentBorderPosition) => {
dynamicStyles += `
&.border-${currentBorderPosition} .ant-checkbox-disabled .ant-checkbox-inner {
border-${currentBorderPosition}: 2px solid hsl(0deg 0% 0%) !important;
}`
return dynamicStyles
}, '')}
`,
cellWithHints: css`
.ant-checkbox-inner {
background-color: hsl(271 76% 53% / 0.4);
}
`,
}
54 changes: 54 additions & 0 deletions src/components/page-root/GameInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import { css } from 'styled-components'
import { Modal, Button } from 'antd'
import { useSelector, useDispatch } from 'react-redux'
import { flowRight } from 'lodash-es'
import { withErrorBoundaries } from 'helpers/decorators'
import { battleshipGameSelectors } from 'store/battleshipGame/selectors'
import { battleshipGameAPI } from 'store/battleshipGame/actions'

function GameInfo() {
const dispatch = useDispatch(),
aliveShips = useSelector(battleshipGameSelectors.getAliveShips),
onShowHints = React.useCallback(() => {
dispatch(battleshipGameAPI.showHints())
setTimeout(() => dispatch(battleshipGameAPI.showHints()), 1000)
}, [dispatch])

const [isModalVisible, setModalVisible] = React.useState(false),
onClickCancel = React.useCallback(() => setModalVisible(false), []),
onClickOk = React.useCallback(() => {
setModalVisible(false)
dispatch(battleshipGameAPI.restartGame())
}, [dispatch])

React.useEffect(() => {
if (!aliveShips) setTimeout(() => setModalVisible(true))
}, [aliveShips])

return (
<aside css={infoStyles.wrapper}>
<p>
Alive: <span>{aliveShips}</span>
</p>
<Button children="Show hints" onClick={onShowHints} />
<Modal
title="You win!"
visible={isModalVisible}
onOk={onClickOk}
onCancel={onClickCancel}
centered
children="Do you want to start new game?"
/>
</aside>
)
}

const infoStyles = {
wrapper: css`
grid-area: info;
`,
}

const withDecorators = flowRight(withErrorBoundaries(), React.memo)(GameInfo)
export { withDecorators as GameInfo }
11 changes: 11 additions & 0 deletions src/components/page-root/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LoadableComponent } from '@loadable/component'
import { getModuleAsync } from 'modules/optimizations'

export const GameBoard: LoadableComponent<{}> = getModuleAsync({
moduleName: 'GameBoard',
moduleImport: () => import(/* webpackChunkName: "GameBoard", webpackPrefetch: true */ `./GameBoard`),
}),
GameInfo: LoadableComponent<{}> = getModuleAsync({
moduleName: 'GameInfo',
moduleImport: () => import(/* webpackChunkName: "GameInfo", webpackPrefetch: true */ `./GameInfo`),
})
57 changes: 57 additions & 0 deletions src/helpers/decorators/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'

interface IWithErrorBoundariesSettings {
isWrapperHandler?: boolean
customError?: null | ((errorInfo: IWrapperState) => React.ReactNode)
}

interface IWithErrorBoundaries {
stateKey: string
(settings?: IWithErrorBoundariesSettings): (Component: any) => React.ComponentType<any>
}

interface IWrapperState {
hasError: boolean
}

/** After error:
* withErrorBoundaries()(SomeComponent)
wrapper will replace children to renderDefaultError
* withErrorBoundaries({customError: OtherComp})(SomeComponent)
wrapper will replace children to OtherComp
* withErrorBoundaries({isWrapperHandler: null})(SomeComponent)
wrapper will ignore error, SomeComponent must use props[withErrorBoundaries.stateKey] for custom error handling
*/

export const withErrorBoundaries: IWithErrorBoundaries = (settings = Object.prototype) => (Component) =>
class Wrapper extends React.Component<any, IWrapperState> {
static displayName = `withErrorBoundaries(${displayNameCreate(Component)})`

state = { hasError: false }

static getDerivedStateFromError(error: any) {
return { hasError: true, error }
}

renderDefaultError = () => <h1>Something went wrong</h1>

/*componentDidCatch(error, errorInfo) {
showNotification()
sendErrorToLog(error, errorInfo);
}*/

render() {
const { isWrapperHandler = true, customError = this.renderDefaultError } = settings,
{ hasError } = this.state

if (isWrapperHandler && hasError) return customError ? customError(this.state) : null

return <Component {...this.props} {...{ [`${withErrorBoundaries.stateKey}`]: this.state }} />
}
}

withErrorBoundaries.stateKey = 'withErrorBoundaries'

const displayNameCreate = (Component: any) => Component.displayName || Component.name || 'Component'
7 changes: 2 additions & 5 deletions src/modules/optimizations/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import loadable from '@loadable/component'
import { timeout as pTimeout } from 'promise-timeout'
import { Spin } from 'antd'

interface IGetAsyncModule {
moduleImport: any
Expand Down Expand Up @@ -30,15 +31,11 @@ export function getModuleAsync({
})
},
{
fallback: <Loader />,
fallback: <Spin />,
}
)

if (withPreload) AsyncComponent.preload()

return AsyncComponent
}

export const Loader: React.FC = () => {
return <>Loading...</>
}
18 changes: 2 additions & 16 deletions src/modules/seo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'
import { siteMetadata } from '../../constants'

export function SEO({ locale, pageName }: any) {
const data = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
}
}
}
`
)

const { siteMetadata } = data.site,
{ title } = siteMetadata
const { title } = siteMetadata

return (
<Helmet
Expand Down
Loading

0 comments on commit a4bc865

Please sign in to comment.