Structure change (#4220)

* Structure change

* Remove duplicate folders from previous structure

* Cleanup websocket protocol

* Updating from based off PR

* Fixup - remove wrong files during conflict and get the websocket working

* renaming tsx to ts

Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
Joseph Chamish 2021-01-20 18:50:18 -05:00 committed by GitHub
parent a0deb73df6
commit 1ddc9cc929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 424 additions and 228 deletions

View file

@ -0,0 +1,25 @@
import React from "react";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
const CheckboxField = ({ input, label }) => {
const { value, onChange } = input;
// @TODO this isnt unchecking properly
return (
<FormControlLabel
className="checkbox-field"
label={label}
control={
<Checkbox
className="checkbox-field__box"
checked={!!value}
onChange={onChange}
color="primary"
/>
}
/>
);
};
export default CheckboxField;

View file

View file

@ -0,0 +1,19 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { AuthGuard } from "components/index";
import "./Decks.css";
class Decks extends Component {
render() {
return (
<div>
<AuthGuard />
<span>"Decks"</span>
</div>
)
}
}
export default Decks;

View file

View file

@ -0,0 +1,19 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { AuthGuard } from "../index";
import "./Game.css";
class Game extends Component {
render() {
return (
<div>
<AuthGuard />
<span>"Game"</span>
</div>
)
}
}
export default Game;

View file

@ -0,0 +1,26 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { ServerSelectors } from "store";
import { RouteEnum } from "types";
import { AuthenticationService } from "websocket";
class AuthGuard extends Component<AuthGuardProps> {
render() {
return !AuthenticationService.isConnected(this.props.state)
? <Redirect from="*" to={RouteEnum.SERVER} />
: "";
}
};
interface AuthGuardProps {
state: number;
}
const mapStateToProps = state => ({
state: ServerSelectors.getState(state),
});
export default connect(mapStateToProps)(AuthGuard);

View file

@ -0,0 +1,27 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { ServerSelectors } from "store";
import { User } from "types";
import { AuthenticationService } from "websocket";
import { RouteEnum } from "types";
class ModGuard extends Component<ModGuardProps> {
render() {
return !AuthenticationService.isModerator(this.props.user)
? <Redirect from="*" to={RouteEnum.SERVER} />
: "";
}
};
interface ModGuardProps {
user: User;
}
const mapStateToProps = state => ({
user: ServerSelectors.getUser(state),
});
export default connect(mapStateToProps)(ModGuard);

View file

@ -0,0 +1,77 @@
.Header {
}
.Header__logo {
display: flex;
}
.Header__logo img {
height: 40px;
}
.Header-content {
display: flex;
align-items: center;
width: 100%;
padding: 5px;
color: white;
}
.Header-serverDetails {
font-size: 12px;
}
.Header-nav {
width: 100%;
display: flex;
align-items: center;
margin-right: 10px;
}
.Header-nav__items {
width: 100%;
display: flex;
justify-content: flex-end;
}
.Header-nav__item {
list-style: none;
margin: 0 10px;
}
.Header-account {
display: flex;
align-items: center;
}
.Header-account__name {
margin-right: 10px;
font-weight: bold;
}
.Header-account__indicator {
display: inline-block;
height: 16px;
width: 16px;
background: red;
border: 2px solid;
border-radius: 50%;
}
.temp-subnav__rooms {
display: flex;
align-items: center;
font-size: 10px;
padding: 5px;
}
.temp-chip {
margin-left: 5px;
text-decoration: none;
}
.temp-chip > div {
cursor: inherit;
}

View file

@ -0,0 +1,118 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { NavLink, withRouter, generatePath } from "react-router-dom";
import AppBar from "@material-ui/core/AppBar";
import Chip from "@material-ui/core/Chip";
import Toolbar from "@material-ui/core/Toolbar";
import * as _ from "lodash";
import { RoomsSelectors, ServerSelectors } from "store";
import { Room, User } from "types";
import { AuthenticationService } from "../../websocket";
import { RouteEnum } from "../../types";
import "./Header.css";
import logo from "./logo.png";
class Header extends Component<HeaderProps> {
componentDidUpdate(prevProps) {
const currentRooms = this.props.joinedRooms;
const previousRooms = prevProps.joinedRooms;
if (currentRooms > previousRooms) {
const { roomId } = _.difference(currentRooms, previousRooms)[0];
this.props.history.push(generatePath(RouteEnum.ROOM, { roomId }));
}
}
render() {
const { joinedRooms, server, state, user } = this.props;
return (
<div>
{/*<header className="Header">*/}
<AppBar position="static">
<Toolbar variant="dense">
<NavLink to={RouteEnum.SERVER} className="Header__logo">
<img src={logo} alt="logo" />
</NavLink>
{ AuthenticationService.isConnected(state) && (
<div className="Header-content">
<nav className="Header-nav">
<ul className="Header-nav__items">
{
AuthenticationService.isModerator(user) && (
<li className="Header-nav__item">
<NavLink to={RouteEnum.LOGS}>
<button>Logs</button>
</NavLink>
</li>
)
}
<li className="Header-nav__item">
<NavLink to={RouteEnum.SERVER} className="plain-link">
Server ({server})
</NavLink>
</li>
<NavLink to={RouteEnum.ACCOUNT} className="plain-link">
<div className="Header-account">
<span className="Header-account__name">
{user.name}
</span>
<span className="Header-account__indicator"></span>
</div>
</NavLink>
</ul>
</nav>
</div>
) }
</Toolbar>
</AppBar>
<div className="temp-subnav">
{
!!joinedRooms.length && (
<Rooms rooms={joinedRooms} />
)
}
<div className="temp-subnav__games">
</div>
<div className="temp-subnav__chats">
</div>
</div>
</div>
)
}
}
const Rooms = props => (
<div className="temp-subnav__rooms">
<span>Rooms: </span>
{
_.reduce(props.rooms, (rooms, { name, roomId}) => {
rooms.push(
<NavLink to={generatePath(RouteEnum.ROOM, { roomId })} className="temp-chip" key={roomId}>
<Chip label={name} color="primary" />
</NavLink>
);
return rooms;
}, [])
}
</div>
)
interface HeaderProps {
state: number;
server: string;
user: User;
joinedRooms: Room[];
history: any;
}
const mapStateToProps = state => ({
state: ServerSelectors.getState(state),
server: ServerSelectors.getName(state),
user: ServerSelectors.getUser(state),
joinedRooms: RoomsSelectors.getJoinedRooms(state)
});
export default withRouter(connect(mapStateToProps)(Header));

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,19 @@
.input-action {
display: flex;
width: 100%;
align-items: center;
}
.input-action,
.input-action__item,
.input-action__submit {
padding: 5px;
}
.input-action__item {
width: 100%;
height: 100%;
}
.input-action__item > div {
margin: 0;
}

View file

@ -0,0 +1,23 @@
// eslint-disable-next-line
import React from "react";
import { Field } from "redux-form"
import Button from "@material-ui/core/Button";
import { InputField } from 'components';
import "./InputAction.css";
const InputAction = ({ action, label, name }) => (
<div className="input-action">
<div className="input-action__item">
<Field label={label} name={name} component={InputField} />
</div>
<div className="input-action__submit">
<Button color="primary" variant="contained" type="submit">
{action}
</Button>
</div>
</div>
);
export default InputAction;

View file

@ -0,0 +1,17 @@
import React from "react";
import TextField from "@material-ui/core/TextField";
const InputField = ({ input, label, name, autoComplete, type }) => (
<TextField
variant="outlined"
margin="dense"
fullWidth={true}
label={label}
name={name}
type={type}
autoComplete={autoComplete}
{ ...input }
/>
);
export default InputField;

View file

@ -0,0 +1,3 @@
.log-results {
margin-bottom: 20px;
}

View file

@ -0,0 +1,122 @@
import React from "react";
import * as _ from "lodash";
import AppBar from "@material-ui/core/AppBar";
import Box from "@material-ui/core/Box";
import Paper from "@material-ui/core/Paper";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import Typography from "@material-ui/core/Typography";
import "./LogResults.css";
const LogResults = (props) => {
const { logs } = props;
const hasRoomLogs = logs.room && logs.room.length;
const hasGameLogs = logs.game && logs.game.length;
const hasChatLogs = logs.chat && logs.chat.length;
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const headerCells = [
{
label: "Time"
},
{
label: "Sender Name"
},
{
label: "Sender IP"
},
{
label: "Message"
},
{
label: "Target ID"
},
{
label: "Target Name"
}
];
return (
<div>
<AppBar position="static">
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
<Tab label={"Rooms" + (hasRoomLogs ? ` [${logs.room.length}]` : "")} {...a11yProps(0)} />
<Tab label={"Games" + (hasGameLogs ? ` [${logs.game.length}]` : "")} {...a11yProps(1)} />
<Tab label={"Chats" + (hasChatLogs ? ` [${logs.chat.length}]` : "")} {...a11yProps(2)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
<Results logs={logs.room} headerCells={headerCells} />
</TabPanel>
<TabPanel value={value} index={1}>
<Results logs={logs.game} headerCells={headerCells} />
</TabPanel>
<TabPanel value={value} index={2}>
<Results logs={logs.chat} headerCells={headerCells} />
</TabPanel>
</div>
)
};
const a11yProps = index => {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
};
const TabPanel = ({ children, value, index, ...other }) => {
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
<Box>{children}</Box>
</Typography>
);
};
const Results = ({headerCells, logs}) => (
<Paper className="log-results">
<Table size="small">
<TableHead>
<TableRow>
{ _.map(headerCells, ({ label }) => (
<TableCell key={label}>{label}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{ _.map(logs, ({ time, senderName, senderIp, message, targetId, targetName }, index) => (
<TableRow key={index}>
<TableCell>{time}</TableCell>
<TableCell>{senderName}</TableCell>
<TableCell>{senderIp}</TableCell>
<TableCell>{message}</TableCell>
<TableCell>{targetId}</TableCell>
<TableCell>{targetName}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
);
export default LogResults;

View file

@ -0,0 +1,14 @@
.moderator-logs {
height: 100%;
display: flex;
padding: 20px;
}
.moderator-logs__form {
width: 40%;
margin-right: 20px;
}
.moderator-logs__results {
width: 100%;
}

View file

@ -0,0 +1,101 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import * as _ from "lodash";
import { ServerDispatch, ServerSelectors, ServerStateLogs } from "store";
import { ModeratorService } from "websocket";
import { AuthGuard, ModGuard} from "components";
import LogResults from "./LogResults";
import { SearchForm } from "forms";
import "./Logs.css";
class Logs extends Component<LogsTypes> {
MAXIMUM_RESULTS = 1000;
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
componentWillUnmount() {
ServerDispatch.clearLogs();
}
onSubmit(fields) {
const trimmedFields: any = this.trimFields(fields);
const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields;
const required = _.filter({
userName, ipAddress, gameName, gameId, message
}, field => field);
if (logLocation) {
trimmedFields.logLocation = this.flattenLogLocations(logLocation);
}
trimmedFields.maximumResults = this.MAXIMUM_RESULTS;
if (_.size(required)) {
ModeratorService.viewLogHistory(trimmedFields);
} else {
// @TODO use yet-to-be-implemented banner/alert
}
}
private trimFields(fields) {
return _.reduce(fields, (obj, field, key) => {
if (typeof field === "string") {
const trimmed = _.trim(field);
if (!!trimmed) {
obj[key] = trimmed;
}
} else {
obj[key] = field;
}
return obj;
}, {});
}
private flattenLogLocations(logLocations) {
return _.reduce(logLocations, (arr, loc, key) => {
arr.push(key);
return arr;
}, [])
}
render() {
return (
<div className="moderator-logs overflow-scroll">
<AuthGuard />
<ModGuard />
<div className="moderator-logs__form">
<SearchForm onSubmit={this.onSubmit} />
</div>
<div className="moderator-logs__results">
<LogResults logs={this.props.logs} />
</div>
</div>
)
}
}
interface LogsTypes {
logs: ServerStateLogs
}
const mapStateToProps = state => ({
logs: ServerSelectors.getLogs(state)
});
export default withRouter(connect(mapStateToProps)(Logs));

View file

@ -0,0 +1,17 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { AuthGuard } from "components";
class Player extends Component {
render() {
return (
<div>
<AuthGuard />
<span>"Player"</span>
</div>
)
}
}
export default Player;

View file

@ -0,0 +1,30 @@
.games {
}
.games-header,
.game {
display: flex;
padding: 10px;
border-bottom: 1px solid black;
}
.games-header__cell {
max-width: 200px;
}
.games-header__label,
.game__detail {
width: 10%;
flex-grow: 0;
}
.games-header__label.description,
.game__detail.description {
width: 20%;
flex-grow: 1;
}
.games-header__label.creator,
.game__detail.creator {
width: 20%;
}

View file

@ -0,0 +1,143 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { connect } from "react-redux";
import * as _ from "lodash";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Tooltip from "@material-ui/core/Tooltip";
// import { RoomsService } from "AppShell/common/services";
import { SortUtil, RoomsDispatch, RoomsSelectors } from "store";
import { UserDisplay } from "components";
import "./Games.css";
// @TODO run interval to update timeSinceCreated
class Games extends Component<GamesProps> {
private headerCells = [
{
label: "Age",
field: "startTime"
},
{
label: "Description",
field: "description"
},
{
label: "Creator",
field: "creatorInfo.name"
},
{
label: "Type",
field: "gameType"
},
{
label: "Restrictions",
// field: "?"
},
{
label: "Players",
// field: ["maxPlayers", "playerCount"]
},
{
label: "Spectators",
field: "spectatorsCount"
},
];
handleSort(sortByField) {
const { room: { roomId }, sortBy } = this.props;
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
RoomsDispatch.sortGames(roomId, field, order);
}
private isUnavailableGame({ started, maxPlayers, playerCount }) {
return !started && playerCount < maxPlayers;
}
private isPasswordProtectedGame({ withPassword }) {
return !withPassword;
}
private isBuddiesOnlyGame({ onlyBuddies }) {
return !onlyBuddies;
}
render() {
const { room, sortBy } = this.props;
const games = room.gameList.filter(game => (
this.isUnavailableGame(game) &&
this.isPasswordProtectedGame(game) &&
this.isBuddiesOnlyGame(game)
));
return (
<div className="games">
<Table size="small">
<TableHead>
<TableRow>
{ _.map(this.headerCells, ({ label, field }) => {
const active = field === sortBy.field;
const order = sortBy.order.toLowerCase();
const sortDirection = active ? order : false;
return (
<TableCell sortDirection={sortDirection} key={label}>
{!field ? label : (
<TableSortLabel
active={active}
direction={order}
onClick={() => this.handleSort(field)}
>
{label}
</TableSortLabel>
)}
</TableCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
<TableRow key={gameId}>
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
<TableCell className="games-header__cell">
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
<div className="single-line-ellipsis">
{description}
</div>
</Tooltip>
</TableCell>
<TableCell className="games-header__cell">
<UserDisplay user={ creatorInfo } />
</TableCell>
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
}
interface GamesProps {
room: any;
sortBy: any;
}
const mapStateToProps = state => ({
sortBy: RoomsSelectors.getSortGamesBy(state)
});
export default connect(mapStateToProps)(Games);

View file

@ -0,0 +1,17 @@
.messages {
height: 100%;
width: 100%;
padding: 10px;
font-size: 12px;
line-height: 1.3;
}
.message {
padding: 5px 0;
margin: 2px 0;
border-bottom: 1px dashed rgba(0, 0, 0, 0.25);
}
.message:last-of-type {
border: 0;
}

View file

@ -0,0 +1,31 @@
// eslint-disable-next-line
import React from "react";
import "./Messages.css";
const Messages = ({ messages }) => (
<div className="messages">
{
messages && messages.map(({ message, messageType, timeOf, timeReceived }) => (
<div className="message" key={timeReceived}>
<div className="message__detail">{ParsedMessage(message)}</div>
</div>
) )
}
</div>
);
const ParsedMessage = (message) => {
const name = message.match("^[^:]+:");
if (name && name.length) {
message = message.slice(name[0].length, message.length);
}
return <div>
<strong>{name}</strong>
{message}
</div>
};
export default Messages;

View file

@ -0,0 +1,39 @@
.room-view,
.room-view__games,
.room-view__messages,
.room-view__messages-content,
.room-view__side {
height: 100%;
}
.room-view__messages,
.room-view__side {
display: flex;
flex-direction: column;
}
.room-view__messages-sayMessage {
width: 100%;
margin: 10px auto 2px;
}
.room-view__side-label {
position: sticky;
top: 0;
padding: 10px;
background: white;
z-index: 1;
}
.room-view__side-list,
.room-view__side-list .room-view__side-list__item {
height: 100%;
}
.room-view__side-list .room-view__side-list__item {
padding: 0;
}
.room-view__side-list .room-view__side-list__item .user-display__details {
padding: 0 10px;
}

View file

@ -0,0 +1,99 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter /*, RouteComponentProps */ } from "react-router-dom";
import ListItem from "@material-ui/core/ListItem";
import Paper from "@material-ui/core/Paper";
import { RoomsStateMessages, RoomsStateRooms, RoomsSelectors } from "store";
import { RoomsService } from "websocket";
import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard} from "components";
import Games from "./Games";
import Messages from "./Messages";
import SayMessage from "./SayMessage";
import "./Room.css";
// @TODO (3)
class Room extends Component<any> {
constructor(props) {
super(props);
this.handleRoomSay = this.handleRoomSay.bind(this);
}
handleRoomSay({ message }) {
if (message) {
const { roomId } = this.props.match.params;
RoomsService.roomSay(roomId, message);
}
}
render() {
const { match, rooms} = this.props;
const { roomId } = match.params;
const room = rooms[roomId];
const messages = this.props.messages[roomId];
const users = room.userList;
return (
<div className="room-view">
<AuthGuard />
<ThreePaneLayout
fixedHeight
top={(
<Paper className="room-view__games overflow-scroll">
<Games room={room} />
</Paper>
)}
bottom={(
<div className="room-view__messages">
<Paper className="room-view__messages-content overflow-scroll">
<ScrollToBottomOnChanges changes={messages} content={(
<Messages messages={messages} />
)} />
</Paper>
<Paper className="room-view__messages-sayMessage">
<SayMessage onSubmit={this.handleRoomSay} />
</Paper>
</div>
)}
side={(
<Paper className="room-view__side overflow-scroll">
<div className="room-view__side-label">
Users in this room: {users.length}
</div>
<VirtualList
className="room-view__side-list"
itemKey={(index, data) => users[index].name }
items={ users.map(user => (
<ListItem button className="room-view__side-list__item">
<UserDisplay user={user} />
</ListItem>
) ) }
/>
</Paper>
)}
/>
</div>
);
}
}
interface RoomProps {
messages: RoomsStateMessages;
rooms: RoomsStateRooms;
}
const mapStateToProps = state => ({
messages: RoomsSelectors.getMessages(state),
rooms: RoomsSelectors.getRooms(state)
});
export default withRouter(connect(mapStateToProps)(Room));

View file

@ -0,0 +1,18 @@
// eslint-disable-next-line
import React from "react";
import { connect } from "react-redux";
import { Form, reduxForm } from "redux-form"
import { InputAction } from 'components';
const SayMessage = ({ handleSubmit }) => (
<Form onSubmit={handleSubmit}>
<InputAction action="Say" label="Chat" name="message" />
</Form>
);
const propsMap = {
form: "sayMessage"
};
export default connect()(reduxForm(propsMap)(SayMessage));

View file

@ -0,0 +1,25 @@
import React, { useEffect, useRef } from "react";
const ScrollToBottomOnChanges = ({ content, changes }) => {
const messagesEndRef = useRef(null);
// @TODO (2)
const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
}
useEffect(scrollToBottom, [changes]);
const styling = {
height: '100%'
};
return (
<div style={styling}>
{content}
<div ref={messagesEndRef} />
</div>
)
}
export default ScrollToBottomOnChanges;

View file

@ -0,0 +1,4 @@
.select-field label {
background: white;
padding: 0 5px;
}

View file

@ -0,0 +1,30 @@
import React from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import './SelectField.css';
const SelectField = ({ input, label, options, value }) => {
const id = label + "-select-field";
const labelId = id + "-label";
return (
<FormControl variant="outlined" margin="dense" className="select-field">
<InputLabel id={labelId}>{label}</InputLabel>
<Select
labelId={labelId}
id={id}
value={value}
{ ...input }
>{
options.map((option, index) => (
<MenuItem value={index} key={index}> { option } </MenuItem>
))
}</Select>
</FormControl>
);
};
export default SelectField;

View file

@ -0,0 +1,26 @@
.rooms {
}
.rooms-header,
.room {
display: flex;
padding: 10px;
border-bottom: 1px solid black;
}
.rooms-header__label,
.room__detail {
width: 10%;
flex-grow: 0;
}
.rooms-header__label.name,
.room__detail.name {
width: 20%;
}
.rooms-header__label.description,
.room__detail.description {
width: 30%;
flex-grow: 1;
}

View file

@ -0,0 +1,62 @@
// eslint-disable-next-line
import React from "react";
import { generatePath } from "react-router-dom";
import * as _ from "lodash";
import Button from "@material-ui/core/Button";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import { RoomsService } from "websocket";
import { RouteEnum } from "types";
import "./Rooms.css";
const Rooms = ({ rooms, joinedRooms, history }) => {
function onClick(roomId) {
if (_.find(joinedRooms, room => room.roomId === roomId)) {
history.push(generatePath(RouteEnum.ROOM, { roomId }));
} else {
RoomsService.joinRoom(roomId);
}
}
return (
<div className="rooms">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Permissions</TableCell>
<TableCell>Players</TableCell>
<TableCell>Games</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{ _.map(rooms, ({ description, gameCount, name, permissionlevel, playerCount, roomId }) => (
<TableRow key={roomId}>
<TableCell>{name}</TableCell>
<TableCell>{description}</TableCell>
<TableCell>{permissionlevel}</TableCell>
<TableCell>{playerCount}</TableCell>
<TableCell>{gameCount}</TableCell>
<TableCell>
<Button size="small" color="primary" variant="contained" onClick={() => onClick(roomId)}>
Join
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default Rooms;

View file

@ -0,0 +1,61 @@
.server,
.server-rooms,
.server-rooms__side {
height: 100%;
}
.server {
display: flex;
flex-direction: column;
align-items: center;
}
.server .form-wrapper {
display: flex;
flex-direction: column;
}
.server-connect {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.server-connect__form,
.server-connect__description {
width: 100%;
max-width: 300px;
}
.server-connect__form {
margin: 50px 0;
}
.server-connect__description {
text-align: center;
margin: 20px 0;
padding: 20px;
}
.serverRoomWrapper {
height: 100%;
}
.serverMessage {
height: 100%;
padding: 20px;
margin-bottom: 2px;
}
.server-rooms {
width: 100%;
}
.server-rooms__side-label {
position: sticky;
top: 0;
padding: 10px;
background: white;
z-index: 1;
}

View file

@ -0,0 +1,157 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import Button from "@material-ui/core/Button";
import ListItem from "@material-ui/core/ListItem";
import Paper from "@material-ui/core/Paper";
import { RoomsSelectors, ServerSelectors } from "store";
import { AuthenticationService } from "websocket";
import { ThreePaneLayout, UserDisplay, VirtualList } from "components";
import { ConnectForm, RegisterForm } from "forms";
import { Room, StatusEnum, User } from "types";
import Rooms from './Rooms';
import "./Server.css";
class Server extends Component<ServerProps, ServerState> {
constructor(props) {
super(props);
this.showDescription = this.showDescription.bind(this);
this.showRegisterForm = this.showRegisterForm.bind(this);
this.hideRegisterForm = this.hideRegisterForm.bind(this);
this.onRegister = this.onRegister.bind(this);
this.state = {
register: false
};
}
showDescription(state, description) {
const isDisconnected = state === StatusEnum.DISCONNECTED;
const hasDescription = description && !!description.length;
return isDisconnected && hasDescription;
}
showRegisterForm() {
this.setState({register: true});
}
hideRegisterForm() {
this.setState({register: false});
}
onRegister(fields) {
console.log("register", fields);
}
render() {
const { message, rooms, joinedRooms, history, state, description, users } = this.props;
const { register } = this.state;
const isConnected = AuthenticationService.isConnected(state);
return (
<div className="server">
{
isConnected
? ( <ServerRooms rooms={rooms} joinedRooms={joinedRooms} history={history} message={message} users={users} /> )
: (
<div className="server-connect">
<Paper className="server-connect__form">
{
register
? ( <Register connect={this.hideRegisterForm} onRegister={this.onRegister} /> )
: ( <Connect register={this.showRegisterForm} /> )
}
</Paper>
</div>
)
}
{
!isConnected && this.showDescription(state, description) && (
<Paper className="server-connect__description">
{description}
</Paper>
)
}
</div>
);
}
}
const ServerRooms = ({ rooms, joinedRooms, history, message, users}) => (
<div className="server-rooms">
<ThreePaneLayout
top={(
<Paper className="serverRoomWrapper overflow-scroll">
<Rooms rooms={rooms} joinedRooms={joinedRooms} history={history} />
</Paper>
)}
bottom={(
<Paper className="serverMessage overflow-scroll" dangerouslySetInnerHTML={{ __html: message }} />
)}
side={(
<Paper className="server-rooms__side overflow-scroll">
<div className="server-rooms__side-label">
Users connected to server: {users.length}
</div>
<VirtualList
itemKey={(index, data) => users[index].name }
items={ users.map(user => (
<ListItem button dense>
<UserDisplay user={user} />
</ListItem>
) ) }
/>
</Paper>
)}
/>
</div>
);
const Connect = ({register}) => (
<div className="form-wrapper">
<ConnectForm onSubmit={AuthenticationService.connect} />
{/*{<Button variant="outlined" onClick={register}>Register</Button>}*/}
</div>
);
const Register = ({ onRegister, connect }) => (
<div className="form-wrapper">
<RegisterForm onSubmit={event => onRegister(event)} />
<Button variant="outlined" onClick={connect}>Connect</Button>
</div>
);
interface ServerProps {
message: string;
state: number;
description: string;
rooms: Room[];
joinedRooms: Room[];
users: User[];
history: any;
}
interface ServerState {
register: boolean;
}
const mapStateToProps = state => ({
message: ServerSelectors.getMessage(state),
state: ServerSelectors.getState(state),
description: ServerSelectors.getDescription(state),
rooms: RoomsSelectors.getRooms(state),
joinedRooms: RoomsSelectors.getJoinedRooms(state),
users: ServerSelectors.getUsers(state)
});
export default withRouter(connect(mapStateToProps)(Server));

View file

@ -0,0 +1,33 @@
.three-pane-layout,
.three-pane-layout .grid {
width: 100%;
height: 100%;
margin: 0;
}
.three-pane-layout .grid-main,
.three-pane-layout .grid-side {
height: 100%;
}
.three-pane-layout .grid-main {
display: flex;
flex-direction: column;
}
.three-pane-layout .grid-main__top {
max-height: 50%;
width: 100%;
padding-bottom: 20px;
flex-shrink: 0;
}
.three-pane-layout .grid-main__top.fixedHeight {
height: 50%;
}
.three-pane-layout .grid-main__bottom {
height: 100%;
width: 100%;
flex-shrink: 1;
}

View file

@ -0,0 +1,45 @@
// eslint-disable-next-line
import React, { Component, CElement } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import Hidden from "@material-ui/core/Hidden";
import "./ThreePaneLayout.css";
class ThreePaneLayout extends Component<ThreePaneLayoutProps> {
render() {
return (
<div className="three-pane-layout">
<Grid container spacing={2} className="grid">
<Grid item xs={12} md={9} lg={10} className="grid-main">
<Grid item className={
"grid-main__top"
+ (this.props.fixedHeight ? " fixedHeight" : "")
}>
{this.props.top}
</Grid>
<Grid item className="grid-main__bottom">
{this.props.bottom}
</Grid>
</Grid>
<Hidden smDown>
<Grid item md={3} lg={2} className="grid-side">
{this.props.side}
</Grid>
</Hidden>
</Grid>
</div>
);
}
}
interface ThreePaneLayoutProps {
top: CElement<any, any>,
bottom: CElement<any, any>,
side?: CElement<any, any>,
fixedHeight?: boolean,
}
const mapStateToProps = state => ({});
export default connect(mapStateToProps)(ThreePaneLayout);

View file

@ -0,0 +1,11 @@
.user-display,
.user-display__link {
height: 100%;
width: 100%;
}
.user-display__details {
height: 100%;
display: flex;
align-items: center;
}

View file

@ -0,0 +1,153 @@
// eslint-disable-next-line
import React, { Component } from "react";
import { connect } from "react-redux";
import { NavLink, generatePath } from "react-router-dom";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import { SessionService } from "../../websocket";
import { ServerSelectors } from "../../store";
import { RouteEnum } from "../../types";
import { User } from "types";
import "./UserDisplay.css";
class UserDisplay extends Component<UserDisplayProps, UserDisplayState> {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
this.navigateToUserProfile = this.navigateToUserProfile.bind(this);
this.addToBuddyList = this.addToBuddyList.bind(this);
this.removeFromBuddyList = this.removeFromBuddyList.bind(this);
this.addToIgnoreList = this.addToIgnoreList.bind(this);
this.removeFromIgnoreList = this.removeFromIgnoreList.bind(this);
this.isABuddy = this.isABuddy.bind(this);
this.isIgnored = this.isIgnored.bind(this);
this.state = {
position: null
};
}
handleClick(event) {
event.preventDefault();
this.setState({
position: {
x: event.clientX + 2,
y: event.clientY + 4,
}
});
}
handleClose() {
this.setState({
position: null
});
}
navigateToUserProfile() {
this.handleClose();
}
addToBuddyList() {
SessionService.addToBuddyList(this.props.user.name);
this.handleClose();
}
removeFromBuddyList() {
SessionService.removeFromBuddyList(this.props.user.name);
this.handleClose();
}
addToIgnoreList() {
SessionService.addToIgnoreList(this.props.user.name);
this.handleClose();
}
removeFromIgnoreList() {
SessionService.removeFromIgnoreList(this.props.user.name);
this.handleClose();
}
isABuddy() {
return this.props.buddyList.filter(user => user.name === this.props.user.name).length;
}
isIgnored() {
return this.props.ignoreList.filter(user => user.name === this.props.user.name).length;
}
render() {
const { user } = this.props;
const { position } = this.state;
const { name } = user;
const isABuddy = this.isABuddy();
const isIgnored = this.isIgnored();
console.log('user', name, !!isABuddy, !!isIgnored);
return (
<div className="user-display">
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="plain-link">
<div className="user-display__details" onContextMenu={this.handleClick}>
<div className="user-display__country"></div>
<div className="user-display__name single-line-ellipsis">{name}</div>
</div>
</NavLink>
<div className="user-display__menu">
<Menu
open={Boolean(position)}
onClose={this.handleClose}
anchorReference='anchorPosition'
anchorPosition={
position !== null
? { top: position.y, left: position.x }
: undefined
}
>
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="user-display__link plain-link">
<MenuItem dense>Chat</MenuItem>
</NavLink>
{
!isABuddy
? ( <MenuItem dense onClick={this.addToBuddyList}>Add to Buddy List</MenuItem> )
: ( <MenuItem dense onClick={this.removeFromBuddyList}>Remove From Buddy List</MenuItem> )
}
{
!isIgnored
? ( <MenuItem dense onClick={this.addToIgnoreList}>Add to Ignore List</MenuItem> )
: ( <MenuItem dense onClick={this.removeFromIgnoreList}>Remove From Ignore List</MenuItem> )
}
</Menu>
</div>
</div>
);
}
}
interface UserDisplayProps {
user: User;
buddyList: User[];
ignoreList: User[];
}
interface UserDisplayState {
position: any;
}
const mapStateToProps = (state) => ({
buddyList: ServerSelectors.getBuddyList(state),
ignoreList: ServerSelectors.getIgnoreList(state)
});
export default connect(mapStateToProps)(UserDisplay);

View file

@ -0,0 +1,3 @@
.virtual-list {
height: 100%;
}

View file

@ -0,0 +1,35 @@
// eslint-disable-next-line
import React from "react";
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import './VirtualList.css';
const VirtualList = ({ items, itemKey, className = {}, size = 30 }) => (
<div className="virtual-list">
<AutoSizer>
{({ height, width }) => (
<List
className={`virtual-list__list ${className}`}
height={height}
width={width}
itemData={items}
itemCount={items.length}
itemSize={size}
itemKey={itemKey}
>
{Row}
</List>
)}
</AutoSizer>
</div>
);
const Row = ({ data, index, style }) => (
<div style={style}>
{data[index]}
</div>
);
export default VirtualList;

View file

@ -0,0 +1,25 @@
// Common components
export { default as Header } from './Header/Header';
export { default as InputField } from './InputField/InputField';
export { default as InputAction } from './InputAction/InputAction';
export { default as VirtualList } from './VirtualList/VirtualList';
export { default as UserDisplay} from './UserDisplay/UserDisplay';
export { default as ThreePaneLayout } from './ThreePaneLayout/ThreePaneLayout';
export { default as CheckboxField } from './CheckboxField/CheckboxField';
export { default as SelectField } from './SelectField/SelectField';
export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/ScrollToBottomOnChanges';
// Major components
export { default as Game } from './Game/Game';
export { default as Decks } from './Decks/Decks';
export { default as Room } from "./Room/Room";
export { default as Player } from "./Player/Player";
export { default as Server } from "./Server/Server";
export { default as Logs } from "./Logs/Logs";
// Guards
export { default as AuthGuard } from './Guard/AuthGuard';
export { default as ModGuard} from './Guard/ModGuard';