diff options
author | Joffrey BION <joffrey.bion@gmail.com> | 2019-05-16 23:48:38 +0200 |
---|---|---|
committer | Joffrey BION <joffrey.bion@gmail.com> | 2019-05-16 23:48:38 +0200 |
commit | 2382a452456e4bdef4584e1046925e372624cb79 (patch) | |
tree | 0e49b2e5d81facb55fb8b08228abeb218a27d466 /sw-ui/src/components/lobby | |
parent | Remove GRADLE_METADATA feature to avoid breaking frontend build (diff) | |
download | seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.tar.gz seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.tar.bz2 seven-wonders-2382a452456e4bdef4584e1046925e372624cb79.zip |
Rationalize module names
Diffstat (limited to 'sw-ui/src/components/lobby')
-rw-r--r-- | sw-ui/src/components/lobby/Lobby.tsx | 56 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/PlayerList.tsx | 41 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/RadialPlayerList.tsx | 69 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/radial-list/RadialList.css | 23 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/radial-list/RadialList.tsx | 64 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/radial-list/RadialListItem.css | 11 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/radial-list/RadialListItem.tsx | 18 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/radial-list/radial-math.ts | 48 | ||||
-rw-r--r-- | sw-ui/src/components/lobby/round-table.png | bin | 0 -> 18527 bytes |
9 files changed, 330 insertions, 0 deletions
diff --git a/sw-ui/src/components/lobby/Lobby.tsx b/sw-ui/src/components/lobby/Lobby.tsx new file mode 100644 index 00000000..3594af65 --- /dev/null +++ b/sw-ui/src/components/lobby/Lobby.tsx @@ -0,0 +1,56 @@ +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { List } from 'immutable'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { ApiLobby, ApiPlayer } from '../../api/model'; +import { GlobalState } from '../../reducers'; +import { actions } from '../../redux/actions/lobby'; +import { getCurrentGame } from '../../redux/games'; +import { getCurrentPlayer } from '../../redux/user'; +import { RadialPlayerList } from './RadialPlayerList'; + +export type LobbyStateProps = { + currentGame: ApiLobby | null, + currentPlayer: ApiPlayer | null, + players: List<ApiPlayer>, +} + +export type LobbyDispatchProps = { + startGame: () => void, +} + +export type LobbyProps = LobbyStateProps & LobbyDispatchProps + +class LobbyPresenter extends Component<LobbyProps> { + + render() { + const {currentGame, currentPlayer, players, startGame} = this.props; + if (!currentGame || !currentPlayer) { + return <div>Error: no current game.</div> + } + return ( + <div> + <h2>{currentGame.name + ' — Lobby'}</h2> + <RadialPlayerList players={players}/> + {currentPlayer.gameOwner && <Button text="START" className={Classes.LARGE} intent={Intent.PRIMARY} icon='play' + onClick={startGame} disabled={players.size < 3}/>} + </div> + ); + } +} + +function mapStateToProps(state: GlobalState): LobbyStateProps { + const game = getCurrentGame(state); + console.info(game); + return { + currentGame: game, + currentPlayer: getCurrentPlayer(state), + players: game ? List(game.players) : List(), + }; +} + +const mapDispatchToProps = { + startGame: actions.requestStartGame, +}; + +export const Lobby = connect(mapStateToProps, mapDispatchToProps)(LobbyPresenter); diff --git a/sw-ui/src/components/lobby/PlayerList.tsx b/sw-ui/src/components/lobby/PlayerList.tsx new file mode 100644 index 00000000..bfc3a56c --- /dev/null +++ b/sw-ui/src/components/lobby/PlayerList.tsx @@ -0,0 +1,41 @@ +import { Classes, Icon } from '@blueprintjs/core' +import { List } from 'immutable'; +import * as React from 'react'; +import { Flex } from 'reflexbox'; +import { ApiPlayer } from '../../api/model'; + +type PlayerListItemProps = { + player: ApiPlayer, + isOwner: boolean, + isUser: boolean, +}; + +const PlayerListItem = ({player, isOwner, isUser}: PlayerListItemProps) => ( + <tr> + <td> + <Flex align='center'> + {isOwner && <Icon icon='badge' title='Game owner'/>} + {isUser && <Icon icon='user' title='This is you'/>} + </Flex> + </td> + <td>{player.displayName}</td> + <td>{player.username}</td> + </tr> +); + +type PlayerListProps = { + players: List<ApiPlayer>, + owner: string, + currentPlayer: ApiPlayer, +}; + +export const PlayerList = ({players, owner, currentPlayer}: PlayerListProps) => ( + <table className={Classes.HTML_TABLE}> + <tbody> + {players.map((player: ApiPlayer) => <PlayerListItem key={player.username} + player={player} + isOwner={player.username === owner} + isUser={player.username === currentPlayer.username}/>)} + </tbody> + </table> +); diff --git a/sw-ui/src/components/lobby/RadialPlayerList.tsx b/sw-ui/src/components/lobby/RadialPlayerList.tsx new file mode 100644 index 00000000..88db55fc --- /dev/null +++ b/sw-ui/src/components/lobby/RadialPlayerList.tsx @@ -0,0 +1,69 @@ +import { Icon, IconName, Intent } from '@blueprintjs/core'; +import { List } from 'immutable'; +import * as React from 'react'; +import { ReactNode } from 'react'; +import { Flex } from 'reflexbox'; +import { ApiPlayer } from '../../api/model'; +import { RadialList } from './radial-list/RadialList'; +import roundTable from './round-table.png'; + +type PlayerItemProps = { + player: ApiPlayer +}; + +const PlayerItem = ({player}: PlayerItemProps) => ( + <Flex column align='center'> + <UserIcon isOwner={player.gameOwner} isUser={player.user} title={player.gameOwner ? 'Game owner' : null}/> + <h5 style={{margin: 0}}>{player.displayName}</h5> + </Flex> +); + +const PlayerPlaceholder = () => ( + <Flex column align='center' style={{opacity: 0.3}}> + <UserIcon isOwner={false} isUser={false} title='Waiting for player...'/> + <h5 style={{margin: 0}}>?</h5> + </Flex> +); + +type UserIconProps = { + isUser: boolean, + isOwner: boolean, + title: string | null, +}; + +const UserIcon = ({isUser, isOwner, title}: UserIconProps) => { + const icon: IconName = isOwner ? 'badge' : 'user'; + const intent: Intent = isUser ? Intent.WARNING : Intent.NONE; + return <Icon icon={icon} iconSize={50} intent={intent} title={title}/>; +}; + +type RadialPlayerListProps = { + players: List<ApiPlayer> +}; + +export const RadialPlayerList = ({players}: RadialPlayerListProps) => { + const orderedPlayers = placeUserFirst(players.toArray()); + const playerItems = orderedPlayers.map(player => <PlayerItem key={player.username} player={player}/>); + const tableImg = <img src={roundTable} alt='Round table' style={{width: 200, height: 200}}/>; + return <RadialList items={completeWithPlaceholders(playerItems)} + centerElement={tableImg} + radius={175} + offsetDegrees={180} + itemWidth={120} + itemHeight={100}/>; +}; + +function placeUserFirst(players: ApiPlayer[]): ApiPlayer[] { + while (!players[0].user) { + players.push(players.shift()!); + } + return players; +} + +function completeWithPlaceholders(playerItems: Array<ReactNode>): Array<ReactNode> { + while (playerItems.length < 3) { + playerItems.push(<PlayerPlaceholder/>); + } + return playerItems; +} + diff --git a/sw-ui/src/components/lobby/radial-list/RadialList.css b/sw-ui/src/components/lobby/radial-list/RadialList.css new file mode 100644 index 00000000..3b0f3a79 --- /dev/null +++ b/sw-ui/src/components/lobby/radial-list/RadialList.css @@ -0,0 +1,23 @@ +.radial-list-container { + margin: 0; + padding: 0; + position: relative; +} + +.radial-list { + margin: 0; + padding: 0; + transition: all 500ms ease-in-out; + z-index: 1; +} + +.radial-list-center { + z-index: 0; +} + +.absolute-center { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} diff --git a/sw-ui/src/components/lobby/radial-list/RadialList.tsx b/sw-ui/src/components/lobby/radial-list/RadialList.tsx new file mode 100644 index 00000000..806cdd08 --- /dev/null +++ b/sw-ui/src/components/lobby/radial-list/RadialList.tsx @@ -0,0 +1,64 @@ +import React, { ReactNode } from 'react'; +import { CartesianCoords, RadialConfig } from './radial-math'; +import { offsetsFromCenter, CLOCKWISE, COUNTERCLOCKWISE } from './radial-math'; +import './RadialList.css'; +import { RadialListItem } from './RadialListItem'; + +type RadialListProps = { + items: Array<ReactNode>, + centerElement?: ReactNode, + radius?: number, // 120px by default + offsetDegrees?: number, // defaults to 0 = 12 o'clock + arc?: number, // defaults to 360 (full circle) + clockwise?: boolean, // defaults to true + itemWidth?: number, + itemHeight?: number, +}; + +export const RadialList = ({items, centerElement, radius = 120, offsetDegrees = 0, arc = 360, clockwise = true, itemWidth = 20, itemHeight = 20}: RadialListProps) => { + const diameter = radius * 2; + const containerStyle = { + width: diameter + itemWidth, + height: diameter + itemHeight, + }; + const direction = clockwise ? CLOCKWISE : COUNTERCLOCKWISE; + const radialConfig: RadialConfig = {radius, arc, offsetDegrees, direction}; + + return <div className='radial-list-container' style={containerStyle}> + <RadialListItems items={items} radialConfig={radialConfig}/> + <RadialListCenter centerElement={centerElement}/> + </div>; +}; + +type RadialListItemsProps = { + items: Array<React.ReactNode>, + radialConfig: RadialConfig, +}; + +const RadialListItems = ({items, radialConfig}: RadialListItemsProps) => { + const diameter = radialConfig.radius * 2; + const ulStyle = { + width: diameter, + height: diameter, + }; + const itemOffsets: Array<CartesianCoords> = offsetsFromCenter(items.length, radialConfig); + + return <ul className='radial-list absolute-center' style={ulStyle}> + {items.map((item, i) => (<RadialListItem + key={i} + item={item} + offsets={itemOffsets[i]} + />))} + </ul>; +}; + +type RadialListCenterProps = { + centerElement?: ReactNode, +}; + +const RadialListCenter = ({centerElement}: RadialListCenterProps) => { + if (!centerElement) { + return null; + } + return <div className='radial-list-center absolute-center'>{centerElement}</div>; +}; diff --git a/sw-ui/src/components/lobby/radial-list/RadialListItem.css b/sw-ui/src/components/lobby/radial-list/RadialListItem.css new file mode 100644 index 00000000..65bb9661 --- /dev/null +++ b/sw-ui/src/components/lobby/radial-list/RadialListItem.css @@ -0,0 +1,11 @@ +.radial-list-item { + display: block; + position: absolute; + top: 50%; + left: 50%; + margin: 0; + padding: 0; + list-style: unset; + transition: all 500ms ease-in-out; + z-index: 1; +} diff --git a/sw-ui/src/components/lobby/radial-list/RadialListItem.tsx b/sw-ui/src/components/lobby/radial-list/RadialListItem.tsx new file mode 100644 index 00000000..19a27638 --- /dev/null +++ b/sw-ui/src/components/lobby/radial-list/RadialListItem.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { ReactNode } from 'react'; +import { CartesianCoords } from './radial-math'; +import './RadialListItem.css'; + +type RadialListItemProps = { + item: ReactNode, + offsets: CartesianCoords, +}; + +export const RadialListItem = ({item, offsets}: RadialListItemProps) => { + // Y-axis points down, hence the minus sign + const liStyle = { + transform: `translate(${offsets.x}px, ${-offsets.y}px) translate(-50%, -50%)`, + }; + + return <li className='radial-list-item' style={liStyle}>{item}</li>; +}; diff --git a/sw-ui/src/components/lobby/radial-list/radial-math.ts b/sw-ui/src/components/lobby/radial-list/radial-math.ts new file mode 100644 index 00000000..f0f411f5 --- /dev/null +++ b/sw-ui/src/components/lobby/radial-list/radial-math.ts @@ -0,0 +1,48 @@ +export type CartesianCoords = { + x: number, + y: number, +} +type PolarCoords = { + radius: number, + angleDeg: number, +} + +const toRad = (deg: number) => deg * (Math.PI / 180); +const roundedProjection = (radius: number, thetaRad: number, trigFn: (angle: number) => number) => Math.round(radius * trigFn(thetaRad)); +const xProjection = (radius: number, thetaRad: number) => roundedProjection(radius, thetaRad, Math.cos); +const yProjection = (radius: number, thetaRad: number) => roundedProjection(radius, thetaRad, Math.sin); + +const toCartesian = ({radius, angleDeg}: PolarCoords): CartesianCoords => ({ + x: xProjection(radius, toRad(angleDeg)), + y: yProjection(radius, toRad(angleDeg)), +}); + +export type Direction = -1 | 1; +export const CLOCKWISE: Direction = -1; +export const COUNTERCLOCKWISE: Direction = 1; + +export type RadialConfig = { + radius: number, + arc: number, + offsetDegrees: number, + direction: Direction, +} +const DEFAULT_CONFIG: RadialConfig = { + radius: 120, + arc: 360, + offsetDegrees: 0, + direction: CLOCKWISE, +}; + +const DEFAULT_START = 90; // Up + +export function offsetsFromCenter(nbItems: number, radialConfig: RadialConfig = DEFAULT_CONFIG): Array<CartesianCoords> { + return Array.from({length: nbItems}, (v, i) => itemCartesianOffsets(i, nbItems, radialConfig)); +} + +function itemCartesianOffsets(index: number, nbItems: number, {radius, arc, direction, offsetDegrees}: RadialConfig): CartesianCoords { + const startAngle = DEFAULT_START + direction * offsetDegrees; + const angleStep = arc / nbItems; + const itemAngle = startAngle + direction * angleStep * index; + return toCartesian({radius, angleDeg: itemAngle}); +} diff --git a/sw-ui/src/components/lobby/round-table.png b/sw-ui/src/components/lobby/round-table.png Binary files differnew file mode 100644 index 00000000..f277376d --- /dev/null +++ b/sw-ui/src/components/lobby/round-table.png |