summaryrefslogtreecommitdiff
path: root/sw-ui/src/components/lobby
diff options
context:
space:
mode:
authorJoffrey BION <joffrey.bion@gmail.com>2019-05-16 23:48:38 +0200
committerJoffrey BION <joffrey.bion@gmail.com>2019-05-16 23:48:38 +0200
commit2382a452456e4bdef4584e1046925e372624cb79 (patch)
tree0e49b2e5d81facb55fb8b08228abeb218a27d466 /sw-ui/src/components/lobby
parentRemove GRADLE_METADATA feature to avoid breaking frontend build (diff)
downloadseven-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.tsx56
-rw-r--r--sw-ui/src/components/lobby/PlayerList.tsx41
-rw-r--r--sw-ui/src/components/lobby/RadialPlayerList.tsx69
-rw-r--r--sw-ui/src/components/lobby/radial-list/RadialList.css23
-rw-r--r--sw-ui/src/components/lobby/radial-list/RadialList.tsx64
-rw-r--r--sw-ui/src/components/lobby/radial-list/RadialListItem.css11
-rw-r--r--sw-ui/src/components/lobby/radial-list/RadialListItem.tsx18
-rw-r--r--sw-ui/src/components/lobby/radial-list/radial-math.ts48
-rw-r--r--sw-ui/src/components/lobby/round-table.pngbin0 -> 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
new file mode 100644
index 00000000..f277376d
--- /dev/null
+++ b/sw-ui/src/components/lobby/round-table.png
Binary files differ
bgstack15