euchre-live

Euchre web-app for the socially distant family
git clone git://git.alexkarle.com/euchre-live.git
Log | Files | Refs | README | LICENSE

commit e190a2a9802ddcf0b23ffe9880e33426de3d14e2 (patch)
parent 7bc03463b1acdfd80b0fff360efe315bfd842f72
Author: Chris Karle <chriskarle@hotmail.com>
Date:   Fri, 29 May 2020 01:56:14 -0400

TableList, Lobby

Create the TableList widget with clickable-tile entries for
each table on the server.  Also presents the CreateTable modal
and will join user to a new table per modal details.  Lobby updates
to use TableList, dropping text input.

Diffstat:
Massets/app.js | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Massets/app.scss | 42++++++++++++++++++++++++++++++++++++++++++
Massets/components/Lobby.js | 32+++++++++++++++++++++++++-------
Aassets/components/TableList.js | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 310 insertions(+), 35 deletions(-)

diff --git a/assets/app.js b/assets/app.js @@ -7,6 +7,7 @@ import { w3cwebsocket as W3CWebSocket } from 'websocket'; // const client = new W3CWebSocket('ws://localhost:3000/play'); let client = null; +let socketAddr, tablesAddr; // additional fakeClient sockets, only used on tableDebug let fc1, fc2, fc3 = null; @@ -24,23 +25,52 @@ class App extends React.Component { tableName: initialTable, showTable: false, uniqueError: false, - firstMsg: null + firstMsg: null, + tableList: [] }; const host = window.location.host; - const clientAddr = 'ws://' + host + '/play'; - client = new W3CWebSocket(clientAddr); - client.onmessage = (event) => this.processResponse(event); + socketAddr = 'ws://' + host + '/play'; + tablesAddr = 'http://' + host + '/tables'; if (tableDebug) { + client = new W3CWebSocket(socketAddr); + client.onmessage = (event) => this.processResponse(event); + this.pingTimer = setInterval(() => { this.sendPing(); }, 5000); // on tableDebug send join plus add 3 fakeClient players that join+sit - fc1 = new W3CWebSocket(clientAddr); - fc2 = new W3CWebSocket(clientAddr); - fc3 = new W3CWebSocket(clientAddr); + fc1 = new W3CWebSocket(socketAddr); + fc2 = new W3CWebSocket(socketAddr); + fc3 = new W3CWebSocket(socketAddr); // wait 1 second so sockets establish connection setTimeout(()=>{ this.setFakeGame(initialName, initialTable); }, 1000); } - this.pingTimer = setInterval(() => { client.send(JSON.stringify({action:'ping'})) }, 5000); + } + + componentDidMount () { + this.fetchTables(); + } + + sendPing = () => { + if (client) { + client.send(JSON.stringify({action:'ping'})); + } + } + + fetchTables = () => { + fetch (tablesAddr).then ((response) => { + if (response.ok){ + response.json().then((data) => { + console.log(data); + this.setState({ + tableList: data.tables + }); + }); + } else { + console.log('BadResponse:', response); + } + }).catch ((error) => { + console.log('Caught error:', error); + }); } setPlayerName = name => { @@ -92,7 +122,8 @@ class App extends React.Component { client.onmessage = (event) => this.processResponse(event); this.setState({ tableName: '', - showTable: false + showTable: false, + firstMsg: null }, () => { client.send(JSON.stringify({ action:'leave_table' @@ -100,25 +131,33 @@ class App extends React.Component { }); } - chooseTable = tableName => { - if (!tableName || tableName == ''){ + joinTable = tableInfo => { + let clientConnectTimeout = 0; + if (client == null){ + client = new W3CWebSocket(socketAddr); client.onmessage = (event) => this.processResponse(event); - this.setState({ - firstMsg: null - }); + this.pingTimer = setInterval(() => { this.sendPing(); }, 5000); + clientConnectTimeout = 1000; + } + const tableName = tableInfo.table; + if (!tableName || tableName == ''){ + console.log('Empty table name!!'); }; - this.setState( { - tableName: tableName, - showTable: false - }, () => { - if (tableName && tableName != ''){ - client.send(JSON.stringify({ - action:'join_table', - player_name: this.state.playerName, - table: tableName - })); - } - }); + setTimeout(()=>{ + this.setState( { + tableName: tableName, + showTable: false + }, () => { + if (tableName && tableName != ''){ + tableInfo.action = 'join_table'; + client.send(JSON.stringify(tableInfo)); + } + }); + }, clientConnectTimeout); + } + + createTable = tableObj => { + console.log('App.createTable:', tableObj); } setFakeGame = (initialName, initialTable) => { @@ -132,15 +171,18 @@ class App extends React.Component { } render () { - const {showTable, playerName, tableName, firstMsg, uniqueError} = this.state; + const {showTable, playerName, tableName, firstMsg, uniqueError, tableList} = this.state; return ( <div id="top-app"> {!showTable && ( <Lobby setName={this.setPlayerName} - chooseTable={this.chooseTable} + joinTable={this.joinTable} name={playerName} uniqueError={uniqueError} + tableList={tableList} + refreshTables={this.fetchTables} + createTable={this.createTable} /> )} {showTable && ( diff --git a/assets/app.scss b/assets/app.scss @@ -319,6 +319,48 @@ .tb__left { padding-left: 2rem; } +.tlist__title { + font-size: 1.75rem; + font-weight: 400; + padding-right: 0.5rem; +} +.tlist__title__row { + display: flex; + align-items: center; +} +.tlist__main { + border: 3px solid darkgray; + margin-right: 4rem; +} +.tlist__cntl { + border-bottom: 1px solid; +} +.tlist__none { + margin: 1rem; +} +.tlist__holder { + height: 45vh; + overflow: hidden; +} +.tlist__list { + height: 45vh; + overflow: auto; + .bx--tile{ + margin-bottom: 5px; + } +} +.table__name { + font-size: 1.2rem; + font-weight: 600; +} +.table__name--locked { + color: red; +} +.table__conflict { + font-style: italic; + font-weight: 600; + color: red; +} // .hd__left { // background-color: rosybrown; diff --git a/assets/components/Lobby.js b/assets/components/Lobby.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Button, TextInput} from 'carbon-components-react'; import {Login32, CheckmarkOutline32} from '@carbon/icons-react'; +import TableList from './TableList'; export default class Lobby extends React.Component { @@ -24,7 +25,7 @@ export default class Lobby extends React.Component { componentDidUpdate (prevProps) { const { name } = this.props; if (name && (name != prevProps.name)){ - this.tableText.focus(); + this.setButton.blur(); } } @@ -52,7 +53,7 @@ export default class Lobby extends React.Component { } render () { - const {name, uniqueError} = this.props; + const {name, uniqueError, tableList} = this.props; const {nameIn, nameError, tableIn, tableError} = this.state; return ( <div id="lobby" className="lobby__outer"> @@ -66,6 +67,7 @@ export default class Lobby extends React.Component { className="lobby__name__input" placeholder="Name to display at table" size="xl" + labelText="" invalidText="Sorry, letters A-Z a-z and spaces only" invalid={nameError} onChange={this.handlePlayerIn} @@ -79,6 +81,7 @@ export default class Lobby extends React.Component { iconDescription="set name" tooltipPosition="bottom" disabled={nameError} + ref={(button) => {this.setButton = button;}} /> </div> <br/><br/> @@ -86,12 +89,23 @@ export default class Lobby extends React.Component { <div> <h3>Welcome, {name}!</h3> <p>You can change that name if you wish by entering a new one above.</p> - <p>Next tell us the name of the table you'd like to join -- use the same table name as the users you would like to play.</p> + <br/><br/> + <div> + <TableList + tables={tableList} + playerName={name} + joinTable={this.props.joinTable} + refresh={this.props.refreshTables} + /> + </div> + {/* <br/><br/> + <p>...or type a table name (deprecated, soon to be removed)</p> <div className="textRow"> <TextInput id="lobby__table" className="lobby__table__input" size="xl" + labelText="" placeholder="Table choice?" invalidText="Sorry, letters A-Z a-z and spaces only" invalid={tableError} @@ -101,14 +115,15 @@ export default class Lobby extends React.Component { <Button className="table__button" hasIconOnly - onClick={()=>this.props.chooseTable(tableIn)} + onClick={()=>this.props.joinTable({table: tableIn, player_name: name})} renderIcon={Login32} iconDescription="go!" tooltipPosition="bottom" disabled={tableError || !name || name==''} /> - </div> + </div> */} </div> + )} {uniqueError && ( <div className="unique__error"> @@ -123,7 +138,10 @@ export default class Lobby extends React.Component { Lobby.propTypes = { setName: PropTypes.func, - chooseTable: PropTypes.func, + joinTable: PropTypes.func, name: PropTypes.string, - uniqueError: PropTypes.bool + uniqueError: PropTypes.bool, + tableList: PropTypes.array, + refreshTables: PropTypes.func, + createTable: PropTypes.func } \ No newline at end of file diff --git a/assets/components/TableList.js b/assets/components/TableList.js @@ -0,0 +1,172 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ClickableTile, Button, ModalWrapper, TextInput, Checkbox } from 'carbon-components-react'; +import {Renew16, Locked16} from '@carbon/icons-react'; + +export default class TableList extends React.Component { + + conflictHandler = (tableName) => { + const {playerName} = this.props; + alert('The "' + tableName + '" table already has a player named ' + playerName + + ' -- you must change your name to join this table, or choose another table.'); + } + + handleJoin = (tableName, hasPwd) => { + console.log('handleJoin, hasPwd=', hasPwd); + const {playerName} = this.props; + let joinInfo = { + table: tableName, + player_name: playerName + }; + this.props.joinTable(joinInfo); + } + + handleCreate = () => { + const {playerName} = this.props; + console.log('handleCreate, optHpick:', this.optHpick.value); + const tableName = this.ctName.value; + if (!tableName || tableName == ''){ + alert('Unnamed tables are not permitted'); + } else { + const pwd = this.ctPwd.value; + let joinInfo = { + table: tableName, + player_name: playerName + } + if (pwd && pwd != ''){ + joinInfo.password = pwd; + } + this.props.joinTable(joinInfo); + } + } + + handleRefresh = () => { + this.refreshButton.blur(); + this.props.refresh(); + } + + renderCards = () => { + const {tables, playerName} = this.props; + let retVal = []; + if (tables){ + tables.forEach(table => { + const conflict = table.players.indexOf(playerName) > -1 + || table.spectators.indexOf(playerName) > -1; + const conflictWarning = conflict ? + (<div className="table__conflict">A user named &quot;{playerName}&quot; is at this table</div>) : null; + const clickHandler = conflict ? this.conflictHandler : this.handleJoin; + let seated = ''; + for (let ni = 0; ni < 4; ni++) { + if (table.players[ni] != 'Empty'){ + if (seated != ''){ + seated += ', '; + } + seated += table.players[ni]; + } + } + let specs = ''; + table.spectators.forEach(spName => { + if (specs != ''){ + specs += ', '; + } + specs += spName; + }) + const lockIcon = table.has_password ? (<Locked16 fill="red" description="locked table"/>) : null; + const nameClass = table.has_password ? "table__name table__name--locked" : "table__name"; + retVal.push( + <ClickableTile + key={table.name} + handleClick={() => clickHandler(table.name, table.has_password)} + > + <div className={nameClass}>{lockIcon}{table.name}</div> + {conflictWarning} + <div className="table__players">Seated: {seated}</div> + <div className="table__spectators">Spectators: {specs}</div> + </ClickableTile> + ) + }); + } + if (retVal.length == 0) { + retVal = ( + <div className="tlist__none"> + No tables yet -- create one or refresh if expecting one... + </div> + ); + } + return retVal; + } + + render () { + const {tables} = this.props; + const showCards = tables.length > 0; + const cards = this.renderCards(); + return ( + <div className="tlist__outer"> + <div className="tlist__header"> + <div className="tlist__title__row"> + <div className="tlist__title">Click a table to join it... or</div> + <ModalWrapper + className="create__modal" + buttonTriggerText="Create a New Table" + primaryButtonText="Create" + modalHeading="New Table" + handleSubmit={this.handleCreate} + shouldCloseAfterSubmit={true} + > + <TextInput + id="ct__name" + placeholder="name your table" + labelText="Table Name" + ref={(input) => {this.ctName = input;}} + /> + <br/> + <TextInput + id="ct__pwd" + placeholder="leave blank for no password" + labelText="Table Password (optional)" + ref={(input) => {this.ctPwd = input;}} + /> + <br/> + <fieldset className="ct__optSet"> + <legend className="ct__optLabel">Game Options</legend> + <Checkbox className="opt__hpick" id="opt__hpick" ref={(input) => {this.optHpick = input}} + defaultChecked labelText="Dealer must have suit to pick up"/> + <Checkbox className="opt__horder" id="opt__horder" ref={(input) => {this.optHorder = input}} + defaultChecked labelText="Must play alone if ordering partner"/> + <Checkbox className="opt__stick" id="opt__stick" ref={(input) => {this.optStick = input}} + labelText="Stick the dealer" /> + </fieldset> + </ModalWrapper> + </div> + {/* planned: filterByTableName, filterByPlayerName */} + </div> + <div className="tlist__main"> + <div className="tlist__cntl"> + <Button + className="tlist__refresh" + hasIconOnly + onClick={this.handleRefresh} + renderIcon={Renew16} + size="small" + iconDescription="Refresh List" + tooltipPosition="bottom" + ref={(button) => {this.refreshButton = button;}} + /> + </div> + <div className="tlist__holder"> + <div className="tlist__list"> + {cards} + </div> + </div> + </div> + </div> + ) + } + +} +TableList.propTypes = { + tables: PropTypes.array, + playerName: PropTypes.string, + joinTable: PropTypes.func, + refresh: PropTypes.func +} +\ No newline at end of file