mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-09 15:54:47 -07:00
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:
parent
a0deb73df6
commit
1ddc9cc929
123 changed files with 424 additions and 228 deletions
0
webclient/src/components/CheckboxField/CheckboxField.css
Normal file
0
webclient/src/components/CheckboxField/CheckboxField.css
Normal file
25
webclient/src/components/CheckboxField/CheckboxField.tsx
Normal file
25
webclient/src/components/CheckboxField/CheckboxField.tsx
Normal 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;
|
||||
0
webclient/src/components/Decks/Decks.css
Normal file
0
webclient/src/components/Decks/Decks.css
Normal file
19
webclient/src/components/Decks/Decks.tsx
Normal file
19
webclient/src/components/Decks/Decks.tsx
Normal 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;
|
||||
0
webclient/src/components/Game/Game.css
Normal file
0
webclient/src/components/Game/Game.css
Normal file
19
webclient/src/components/Game/Game.tsx
Normal file
19
webclient/src/components/Game/Game.tsx
Normal 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;
|
||||
26
webclient/src/components/Guard/AuthGuard.tsx
Normal file
26
webclient/src/components/Guard/AuthGuard.tsx
Normal 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);
|
||||
27
webclient/src/components/Guard/ModGuard.tsx
Normal file
27
webclient/src/components/Guard/ModGuard.tsx
Normal 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);
|
||||
77
webclient/src/components/Header/Header.css
Normal file
77
webclient/src/components/Header/Header.css
Normal 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;
|
||||
}
|
||||
|
||||
118
webclient/src/components/Header/Header.tsx
Normal file
118
webclient/src/components/Header/Header.tsx
Normal 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));
|
||||
BIN
webclient/src/components/Header/logo.png
Normal file
BIN
webclient/src/components/Header/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
19
webclient/src/components/InputAction/InputAction.css
Normal file
19
webclient/src/components/InputAction/InputAction.css
Normal 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;
|
||||
}
|
||||
23
webclient/src/components/InputAction/InputAction.tsx
Normal file
23
webclient/src/components/InputAction/InputAction.tsx
Normal 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;
|
||||
17
webclient/src/components/InputField/InputField.tsx
Normal file
17
webclient/src/components/InputField/InputField.tsx
Normal 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;
|
||||
3
webclient/src/components/Logs/LogResults.css
Normal file
3
webclient/src/components/Logs/LogResults.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.log-results {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
122
webclient/src/components/Logs/LogResults.tsx
Normal file
122
webclient/src/components/Logs/LogResults.tsx
Normal 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;
|
||||
14
webclient/src/components/Logs/Logs.css
Normal file
14
webclient/src/components/Logs/Logs.css
Normal 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%;
|
||||
}
|
||||
101
webclient/src/components/Logs/Logs.tsx
Normal file
101
webclient/src/components/Logs/Logs.tsx
Normal 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));
|
||||
17
webclient/src/components/Player/Player.tsx
Normal file
17
webclient/src/components/Player/Player.tsx
Normal 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;
|
||||
30
webclient/src/components/Room/Games.css
Normal file
30
webclient/src/components/Room/Games.css
Normal 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%;
|
||||
}
|
||||
143
webclient/src/components/Room/Games.tsx
Normal file
143
webclient/src/components/Room/Games.tsx
Normal 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);
|
||||
17
webclient/src/components/Room/Messages.css
Normal file
17
webclient/src/components/Room/Messages.css
Normal 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;
|
||||
}
|
||||
31
webclient/src/components/Room/Messages.tsx
Normal file
31
webclient/src/components/Room/Messages.tsx
Normal 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;
|
||||
39
webclient/src/components/Room/Room.css
Normal file
39
webclient/src/components/Room/Room.css
Normal 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;
|
||||
}
|
||||
99
webclient/src/components/Room/Room.tsx
Normal file
99
webclient/src/components/Room/Room.tsx
Normal 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));
|
||||
18
webclient/src/components/Room/SayMessage.tsx
Normal file
18
webclient/src/components/Room/SayMessage.tsx
Normal 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));
|
||||
|
|
@ -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;
|
||||
4
webclient/src/components/SelectField/SelectField.css
Normal file
4
webclient/src/components/SelectField/SelectField.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.select-field label {
|
||||
background: white;
|
||||
padding: 0 5px;
|
||||
}
|
||||
30
webclient/src/components/SelectField/SelectField.tsx
Normal file
30
webclient/src/components/SelectField/SelectField.tsx
Normal 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;
|
||||
26
webclient/src/components/Server/Rooms.css
Normal file
26
webclient/src/components/Server/Rooms.css
Normal 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;
|
||||
}
|
||||
62
webclient/src/components/Server/Rooms.tsx
Normal file
62
webclient/src/components/Server/Rooms.tsx
Normal 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;
|
||||
61
webclient/src/components/Server/Server.css
Normal file
61
webclient/src/components/Server/Server.css
Normal 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;
|
||||
}
|
||||
157
webclient/src/components/Server/Server.tsx
Normal file
157
webclient/src/components/Server/Server.tsx
Normal 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));
|
||||
33
webclient/src/components/ThreePaneLayout/ThreePaneLayout.css
Normal file
33
webclient/src/components/ThreePaneLayout/ThreePaneLayout.css
Normal 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;
|
||||
}
|
||||
45
webclient/src/components/ThreePaneLayout/ThreePaneLayout.tsx
Normal file
45
webclient/src/components/ThreePaneLayout/ThreePaneLayout.tsx
Normal 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);
|
||||
11
webclient/src/components/UserDisplay/UserDisplay.css
Normal file
11
webclient/src/components/UserDisplay/UserDisplay.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.user-display,
|
||||
.user-display__link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-display__details {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
153
webclient/src/components/UserDisplay/UserDisplay.tsx
Normal file
153
webclient/src/components/UserDisplay/UserDisplay.tsx
Normal 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);
|
||||
3
webclient/src/components/VirtualList/VirtualList.css
Normal file
3
webclient/src/components/VirtualList/VirtualList.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.virtual-list {
|
||||
height: 100%;
|
||||
}
|
||||
35
webclient/src/components/VirtualList/VirtualList.tsx
Normal file
35
webclient/src/components/VirtualList/VirtualList.tsx
Normal 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;
|
||||
25
webclient/src/components/index.ts
Normal file
25
webclient/src/components/index.ts
Normal 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';
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue