docker+upgrade

* Updated dependencies
  - React update needed transformation of React.createClass to ES2015
  classes
  - removed some deprecations/deprecated packages
  - added mulitple @types Dependencies to devDependencies for IDE code
  completion support
* added Docker related files
  - Dockerfile with build container
  - docker-compose.yml with mongodb and app
This commit is contained in:
Absurdon 2020-10-11 19:47:33 +02:00
parent 11c0168609
commit 79be2b8155
29 changed files with 4494 additions and 4230 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
node_modules
.snyk
*.md
db/data
Procfile
Dockerfile
docker-compose.yml
.env*
.nvmrc

12
.env.example Normal file
View file

@ -0,0 +1,12 @@
NODE_PORT=8080
RAILS_SECRET=secret
NEW_RELIC_LICENSE_KEY=secret
GATHER_STEAM_ACCOUNT=gatherbot
GATHER_STEAM_PASSWORD=secret
GATHER_DISCORD_HOOK_ID=someid
GATHER_DISCORD_HOOK_TOKEN=secret
NODE_ENV=production
MONGODB_USERNAME=ensl_gather
MONGODB_PASSWORD=supersecret
MONGODB_DATABASE=ensl_gather

4
.gitignore vendored
View file

@ -8,4 +8,6 @@ public/app.js.map
public/app.css
public/app.css.map
public/vendor.js
public/vendor.js.map
public/vendor.js.map
db/data
.env

28
Dockerfile Normal file
View file

@ -0,0 +1,28 @@
FROM bitnami/node:12 AS builder
COPY package*.json /app/
WORKDIR /app
RUN ["npm", "install"]
COPY . /app
RUN ["npm", "run", "compile_production" ]
RUN ["npm", "prune","--production"]
FROM bitnami/node:12-prod AS production
ENV NODE_ENV="production"
ENV PORT=8000
RUN ["adduser", "web", "--disabled-password"]
COPY --chown=web:web --from=builder /app /app
USER web
WORKDIR /app
RUN /bin/mkdir -p /home/web/tmp/public
RUN /bin/cp -r ./public /home/web/tmp/public
RUN /usr/bin/touch /home/web/tmp/.updatePublic
EXPOSE 8000
CMD ["node", "index.js"]

View file

@ -1,9 +1,10 @@
const React = require("react");
const ReactDOM = require("react-dom");
const App = require("javascripts/components/main");
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./components/main";
module.exports = function (mount) {
ReactDOM.render(<App />, mount);
ReactDOM.render(<App />, mount);
};
toastr.options = {

View file

@ -1,114 +1,119 @@
const React = require("react");
import {MenubarMixin} from "javascripts/components/menubar";
import React from "react";
import { object } from "prop-types"
import { MenubarMixin } from "javascripts/components/menubar";
const UserLogin = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired
},
class UserLogin extends React.Component {
static propTypes = {
socket: object.isRequired
}
getInitialState() {
return {
userId: null
};
},
state = {
userId: null
}
handleChange(e) {
const newId = e.target.value || null;
this.setState({ userId: newId });
},
handleChange = (e) => {
const newId = e.target.value || null;
this.setState({ userId: newId });
}
authorizeId(id) {
this.props.socket.emit("users:authorize", {
id: id
});
},
authorizeId = (id) => {
this.props.socket.emit("users:authorize", {
id: id
});
}
handleSubmit(e) {
e.preventDefault();
this.authorizeId(this.state.userId);
},
handleSubmit = (e) => {
e.preventDefault();
this.authorizeId(this.state.userId);
}
render() {
return (
<form>
<div className="input-group signin">
<input
id="btn-input"
type="text"
className="form-control"
vaue={this.state.userId}
onChange={this.handleChange}
placeholder="Change user (input ID)" />
<span className="input-group-btn">
<input
type="submit"
className="btn btn-primary"
onClick={this.handleSubmit}
value="Assume ID" />
</span>
</div>
</form>
);
}
});
render = () => {
return (
<form>
<div className="input-group signin">
<input
id="btn-input"
type="text"
className="form-control"
vaue={this.state.userId}
onChange={this.handleChange}
placeholder="Change user (input ID)" />
<span className="input-group-btn">
<input
type="submit"
className="btn btn-primary"
onClick={this.handleSubmit}
value="Assume ID" />
</span>
</div>
</form>
);
}
}
const ResetGatherButton = exports.ResetGatherButton = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired,
gather: React.PropTypes.object.isRequired
},
class ResetGatherButton extends React.Component {
static propTypes = {
socket: object.isRequired,
gather: object.isRequired
}
handleGatherReset() {
this.props.socket.emit("gather:reset", {
type: this.props.gather.type
});
},
handleGatherReset = () => {
this.props.socket.emit("gather:reset", {
type: this.props.gather.type
});
}
render() {
return (
<button
className="btn btn-danger max-width"
onClick={this.handleGatherReset}>
Reset {this.props.gather.name}</button>
);
}
});
render = () => {
return (
<button
className="btn btn-danger max-width"
onClick={this.handleGatherReset}>
Reset {this.props.gather.name}</button>
);
}
}
const AdminPanel = exports.AdminPanel = React.createClass({
mixins: [MenubarMixin],
class AdminPanel extends MenubarMixin(React.Component) {
propTypes: {
socket: React.PropTypes.object.isRequired,
gatherPool: React.PropTypes.object.isRequired
},
static propTypes = {
socket: object.isRequired,
gatherPool: object.isRequired
}
render() {
const gatherPool = this.props.gatherPool;
const resetButtons = [];
for (let attr in gatherPool) {
let gather = gatherPool[attr];
resetButtons.push(
<ResetGatherButton socket={this.props.socket}
gather={gather} key={gather.type} />
);
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>
<i className="fa fa-rebel"></i>
</a>
<ul className="dropdown-menu">
<li className="header">Admin</li>
<ul className="news-menu">
<h5>Swap Into a Different Account (Only works for admins)</h5>
<UserLogin socket={this.props.socket} />
<h5>Gather Options</h5>
<div>
{resetButtons}
</div>
</ul>
</ul>
</li>
);
}
});
constructor(props) {
super(props)
this.state = super.getInitialState();
}
render = () => {
const gatherPool = this.props.gatherPool;
const resetButtons = [];
for (let attr in gatherPool) {
let gather = gatherPool[attr];
resetButtons.push(
<ResetGatherButton socket={this.props.socket}
gather={gather} key={gather.type} />
);
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>
<i className="fa fa-rebel"></i>
</a>
<ul className="dropdown-menu">
<li className="header">Admin</li>
<ul className="news-menu">
<h5>Swap Into a Different Account (Only works for admins)</h5>
<UserLogin socket={this.props.socket} />
<h5>Gather Options</h5>
<div>
{resetButtons}
</div>
</ul>
</ul>
</li>
);
}
}
export { AdminPanel, ResetGatherButton }

View file

@ -1,71 +1,70 @@
const React = require("react");
import React from "react";
const discordDefaults = {
url: "https://discord.gg/Bvs3KjX",
alien: {
channel: "https://discord.gg/UcN724q",
},
marine: {
channel: "https://discord.gg/eGwfHXz",
}
url: "https://discord.gg/Bvs3KjX",
alien: {
channel: "https://discord.gg/UcN724q",
},
marine: {
channel: "https://discord.gg/eGwfHXz",
}
};
const DiscordButton = exports.DiscordButton = React.createClass({
getInitialState() {
return {
open: false
};
},
toggleOpen(e) {
e.preventDefault();
this.setState({ open: !this.state.open });
},
getDefaultProps() {
return discordDefaults
},
export class DiscordButton extends React.Component {
marineUrl() {
return discordDefaults.marine.channel;
},
static defaultProps = discordDefaults
alienUrl() {
return discordDefaults.alien.channel;
},
state = {
open: false
}
chevron() {
if (this.state.open) {
return <i className="fa fa-angle-down pull-right"></i>;
} else {
return <i className="fa fa-angle-right pull-right"></i>;
}
},
toggleOpen = (e) => {
e.preventDefault();
this.setState({ open: !this.state.open });
}
render() {
const open = this.state.open;
let componentClass = ["treeview"];
let dropdown;
if (open) {
componentClass.push("active");
dropdown = (
<ul className="treeview-menu menu-open" style={{display: "block"}}>
<li><a href={this.props.url}>Join Discord channel</a></li>
<li><a href={this.marineUrl()}>Join Marine channel</a></li>
<li><a href={this.alienUrl()}>Join Alien channel</a></li>
<li><p className="let-me-copy">Server: {discordDefaults.url}</p></li>
</ul>
);
}
return (
<li className={componentClass.join(" ")}>
<a href="#" onClick={this.toggleOpen}>
<i className="fa fa-microphone"></i><span>Discord</span>
{this.chevron()}
</a>
{dropdown}
</li>
);
}
});
marineUrl = () => discordDefaults.marine.channel;
alienUrl() {
return discordDefaults.alien.channel;
}
chevron = () => {
if (this.state.open) {
return <i className="fa fa-angle-down pull-right"></i>;
} else {
return <i className="fa fa-angle-right pull-right"></i>;
}
}
render = () => {
const open = this.state.open;
let componentClass = ["treeview"];
let dropdown;
if (open) {
componentClass.push("active");
dropdown = (
<ul className="treeview-menu menu-open" style={{ display: "block" }}>
<li><a href={this.props.url}>Join Discord channel</a></li>
<li><a href={this.marineUrl()}>Join Marine channel</a></li>
<li><a href={this.alienUrl()}>Join Alien channel</a></li>
<li><p className="let-me-copy">Server: {discordDefaults.url}</p></li>
</ul>
);
}
return (
<li className={componentClass.join(" ")}>
<a href="#" onClick={this.toggleOpen}>
<i className="fa fa-microphone"></i><span>Discord</span>
{this.chevron()}
</a>
{dropdown}
</li>
);
}
}

View file

@ -1,30 +1,33 @@
const React = require("react");
const Events = exports.Events = React.createClass({
propTypes: {
events: React.PropTypes.array.isRequired
},
import React from "react";
import { array } from "prop-types";
class Events extends React.Component {
static propTypes = {
events: array.isRequired
}
getTime(timeString) {
return (new Date(timeString)).toTimeString().match(/^[\d:]*/)[0];
},
getTime(timeString) {
return (new Date(timeString)).toTimeString().match(/^[\d:]*/)[0];
}
render() {
let events;
if (this.props.events.length) {
events = this.props.events.map(event => {
return `${this.getTime(event.createdAt)} ${event.description}`;
}).join("\n");
return (
<pre className="events-panel">
{events}
</pre>
);
} else {
return (
<pre className="events-panel">
Listening for new events...
</pre>
);
}
}
});
render = () => {
let events;
if (this.props.events.length) {
events = this.props.events.map(event => {
return `${this.getTime(event.createdAt)} ${event.description}`;
}).join("\n");
return (
<pre className="events-panel">
{events}
</pre>
);
} else {
return (
<pre className="events-panel">
Listening for new events...
</pre>
);
}
}
}
export { Events }

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,35 @@
const React = require("react");
import {CompletedGather} from "javascripts/components/gather";
import React from "react";
import { array } from "prop-types";
import { CompletedGather } from "javascripts/components/gather";
const ArchivedGathers = exports.ArchivedGathers = React.createClass({
propTypes: {
archive: React.PropTypes.array.isRequired,
maps: React.PropTypes.array.isRequired
},
class ArchivedGathers extends React.Component {
static propTypes = {
archive: array.isRequired,
maps: array.isRequired
}
render() {
let archive = this.props.archive
.sort((a, b) => {
return new Date(b.createdAt) - new Date(a.createdAt);
})
.map((archivedGather, index) => {
return <CompletedGather
key={archivedGather.gather.done.time}
show={(index === 0) ? true : false}
gather={archivedGather.gather}
maps={this.props.maps} />
});
render = () => {
let archive = this.props.archive
.sort((a, b) => {
return new Date(b.createdAt) - new Date(a.createdAt);
})
.map((archivedGather, index) => {
return <CompletedGather
key={archivedGather.gather.done.time}
show={(index === 0) ? true : false}
gather={archivedGather.gather}
maps={this.props.maps} />
});
return (
<div className="panel panel-primary">
<div className="panel-heading">Archived Gathers</div>
<div className="panel-body">
{archive}
</div>
</div>
);
}
});
return (
<div className="panel panel-primary">
<div className="panel-heading">Archived Gathers</div>
<div className="panel-body">
{archive}
</div>
</div>
);
}
}
export { ArchivedGathers }

View file

@ -1,26 +1,24 @@
const React = require("react");
import React from "react"
const InfoButton = exports.InfoButton = React.createClass({
getInitialState() {
return {
open: false
};
},
class InfoButton extends React.Component {
state = {
open: false
}
toggleOpen(e) {
toggleOpen = (e) => {
e.preventDefault();
this.setState({ open: !this.state.open });
},
}
chevron() {
chevron = () => {
if (this.state.open) {
return <i className="fa fa-angle-down pull-right"></i>;
} else {
return <i className="fa fa-angle-right pull-right"></i>;
}
},
}
render() {
render = () => {
const open = this.state.open;
let componentClass = ["treeview"];
let dropdown;
@ -62,5 +60,7 @@ const InfoButton = exports.InfoButton = React.createClass({
</li>
);
}
});
}
export { InfoButton }

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,20 @@
const MenubarMixin = exports.MenubarMixin = {
getInitialState() {
return {
show: false
}
},
var MenubarMixin = Base => class extends Base {
toggleShow() {
this.setState({ show: !this.state.show });
},
getInitialState() {
return {
show: false
}
}
componentClass() {
let componentClass = ["dropdown", "messages-menu"];
if (this.state.show) componentClass.push("open");
return componentClass.join(" ");
}
toggleShow = () => {
this.setState({ show: !this.state.show });
}
componentClass = () => {
let componentClass = ["dropdown", "messages-menu"];
if (this.state.show) componentClass.push("open");
return componentClass.join(" ");
}
};
export { MenubarMixin }

View file

@ -1,490 +1,484 @@
const React = require("react");
const moment = require("moment");
const ReactDOM = require("react-dom");
const Ps = require("perfect-scrollbar");
const ReactEmoji = require("react-emoji");
const ReactAutolink = require("react-autolink");
import React from "react";
import ReactDOM from "react-dom";
import { array, object } from "prop-types";
import ReactEmoji from "react-emoji";
import ReactAutolink from "react-autolink";
const MessageBrowser = React.createClass({
getInitialState() {
return {
browserState: "",
messages: [],
page: 0,
limit: 250,
search: ""
}
},
handleNextPage(e) {
e.preventDefault();
const page = this.state.page;
this.setState({ page: page + 1 });
this.loadMessages();
},
handlePreviousPage(e) {
e.preventDefault();
const page = this.state.page;
if (page < 1) return;
this.setState({ page: page - 1 });
this.loadMessages();
},
class MessageBrowser extends React.Component {
state = {
browserState: "",
messages: [],
page: 0,
limit: 250,
search: ""
}
pageHandlers() {
let previous;
if (this.state.page > 0) {
previous = (
<a className="btn btn-xs btn-primary add-right"
onClick={this.handlePreviousPage}>Prev</a>
);
}
let next;
if (this.state.messages.length === this.state.limit) {
next = (
<a className="btn btn-xs btn-primary"
onClick={this.handleNextPage}>Next</a>
);
}
return (
<div>
{previous}
<span className="add-right">
{this.state.page}
</span>
{next}
</div>
);
},
loadMessages() {
const limit = this.state.limit;
const page = this.state.page;
let data = {
limit: limit,
page: page
};
handleNextPage = (e) => {
e.preventDefault();
const page = this.state.page;
this.setState({ page: page + 1 });
this.loadMessages();
}
if (this.state.search.length) {
data.query = this.state.search;
}
handlePreviousPage = (e) => {
e.preventDefault();
const page = this.state.page;
if (page < 1) return;
this.setState({ page: page - 1 });
this.loadMessages();
}
this.setState({ browserState: "Retrieving messages"});
$.ajax({
url: "/api/messages",
data: data
})
.done(data => {
this.setState({
messages: data.messages,
browserState: ""
});
})
.fail(error => {
console.error(error);
this.setState({
browserState: `Unable to retrieve messages.`
});
})
},
pageHandlers = () => {
let previous;
if (this.state.page > 0) {
previous = (
<a className="btn btn-xs btn-primary add-right"
onClick={this.handlePreviousPage}>Prev</a>
);
}
let next;
if (this.state.messages.length === this.state.limit) {
next = (
<a className="btn btn-xs btn-primary"
onClick={this.handleNextPage}>Next</a>
);
}
return (
<div>
{previous}
<span className="add-right">
{this.state.page}
</span>
{next}
</div>
);
}
componentDidMount() {
this.loadMessages();
},
loadMessages = () => {
const limit = this.state.limit;
const page = this.state.page;
let data = {
limit: limit,
page: page
};
updateLimit(e) {
let newLimit = parseInt(e.target.value, 10);
if (isNaN(newLimit) || newLimit > 250) newLimit = 250;
this.setState({ limit: newLimit });
},
if (this.state.search.length) {
data.query = this.state.search;
}
updateSearch(e) {
this.setState({ search: e.target.value });
},
this.setState({ browserState: "Retrieving messages" });
$.ajax({
url: "/api/messages",
data: data
})
.done(data => {
this.setState({
messages: data.messages,
browserState: ""
});
})
.fail(error => {
console.error(error);
this.setState({
browserState: `Unable to retrieve messages.`
});
})
}
render() {
let browserState;
if (this.state.browserState.length) {
browserState = (
<div className="col-xs-7">
<div className="well">{this.state.browserState}</div>
</div>
);
}
const messages = this.state.messages.map(message => {
return (
<tr key={message._id}>
<td className="col-xs-2">{(new Date(message.createdAt)).toString()}</td>
<td className="col-xs-3">{message.author.username}</td>
<td className="col-xs-5">{message.content}</td>
<td className="col-xs-2">{message._id}</td>
</tr>
);
});
return (
<div className="row">
<div className="col-xs-5">
<div className="form-horizontal">
<div className="form-group">
<label className="col-sm-3 control-label">Max Results</label>
<div className="col-sm-9">
<input type="number" className="form-control"
onChange={this.updateLimit}
value={this.state.limit}></input>
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Search Filter</label>
<div className="col-sm-9">
<input type="text" className="form-control"
onChange={this.updateSearch}
value={this.state.search}></input>
</div>
</div>
<div className="form-group">
<div className="col-sm-offset-3 col-sm-9">
<button
className="btn btn-primary"
onClick={this.loadMessages}>Search</button>
</div>
</div>
<div className="row">
<div className="col-sm-offset-3 col-sm-9">
<p>Page Control</p>
{this.pageHandlers()}
</div>
</div>
</div>
</div>
{browserState}
<div className="col-xs-12">
<table className="table">
<thead>
<tr>
<th>Date</th>
<th>Author</th>
<th>Message</th>
<th>ID</th>
</tr>
</thead>
<tbody>
{messages}
</tbody>
</table>
</div>
</div>
);
}
});
componentDidMount = () => {
this.loadMessages();
}
const Chatroom = exports.Chatroom = React.createClass({
propTypes: {
messages: React.PropTypes.array.isRequired,
socket: React.PropTypes.object.isRequired,
user: React.PropTypes.object.isRequired
},
updateLimit = (e) => {
let newLimit = parseInt(e.target.value, 10);
if (isNaN(newLimit) || newLimit > 250) newLimit = 250;
this.setState({ limit: newLimit });
}
getInitialState() {
return {
autoScroll: true
};
},
updateSearch = (e) => {
this.setState({ search: e.target.value });
}
componentDidMount() {
let self = this;
render = () => {
let browserState;
if (this.state.browserState.length) {
browserState = (
<div className="col-xs-7">
<div className="well">{this.state.browserState}</div>
</div>
);
}
const messages = this.state.messages.map(message => {
return (
<tr key={message._id}>
<td className="col-xs-2">{(new Date(message.createdAt)).toString()}</td>
<td className="col-xs-3">{message.author.username}</td>
<td className="col-xs-5">{message.content}</td>
<td className="col-xs-2">{message._id}</td>
</tr>
);
});
return (
<div className="row">
<div className="col-xs-5">
<div className="form-horizontal">
<div className="form-group">
<label className="col-sm-3 control-label">Max Results</label>
<div className="col-sm-9">
<input type="number" className="form-control"
onChange={this.updateLimit}
value={this.state.limit}></input>
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Search Filter</label>
<div className="col-sm-9">
<input type="text" className="form-control"
onChange={this.updateSearch}
value={this.state.search}></input>
</div>
</div>
<div className="form-group">
<div className="col-sm-offset-3 col-sm-9">
<button
className="btn btn-primary"
onClick={this.loadMessages}>Search</button>
</div>
</div>
<div className="row">
<div className="col-sm-offset-3 col-sm-9">
<p>Page Control</p>
{this.pageHandlers()}
</div>
</div>
</div>
</div>
{browserState}
<div className="col-xs-12">
<table className="table">
<thead>
<tr>
<th>Date</th>
<th>Author</th>
<th>Message</th>
<th>ID</th>
</tr>
</thead>
<tbody>
{messages}
</tbody>
</table>
</div>
</div>
);
}
}
this.scrollListener = _.debounce((event) => {
self.temporarilyDisableAutoScroll(event);
}, 300, {
leading: false,
trailing: true
});
class Chatroom extends React.Component {
static propTypes = {
messages: array.isRequired,
socket: object.isRequired,
user: object.isRequired
}
state = {
autoScroll: true
}
let node = ReactDOM.findDOMNode(this.refs.messageContainer);
node.addEventListener('scroll', this.scrollListener);
componentDidMount = () => {
let self = this;
$(window).on("load", this.scrollToBottom);
},
this.scrollListener = _.debounce((event) => {
self.temporarilyDisableAutoScroll(event);
}, 300, {
leading: false,
trailing: true
});
componentWillUnmount() {
node.removeEventListener('scroll', this.scrollListener);
clearTimeout(this.disableScrollTimer);
},
let node = ReactDOM.findDOMNode(this.refs.messageContainer);
node.addEventListener('scroll', this.scrollListener);
loadMoreMessages() {
const earliestMessage = this.props.messages[0];
if (earliestMessage === undefined) return;
this.props.socket.emit("message:refresh", {
before: earliestMessage.createdAt
});
},
$(window).on("load", this.scrollToBottom);
}
sendMessage(message) {
this.props.socket.emit("newMessage", {message: message});
},
componentWillUnmount = () => {
node.removeEventListener('scroll', this.scrollListener);
clearTimeout(this.disableScrollTimer);
}
clearAutoScrollTimeout() {
if (this.disableScrollTimer) clearTimeout(this.disableScrollTimer);
},
loadMoreMessages = () => {
const earliestMessage = this.props.messages[0];
if (earliestMessage === undefined) return;
this.props.socket.emit("message:refresh", {
before: earliestMessage.createdAt
});
}
temporarilyDisableAutoScroll(event) {
let self = this;
let node = event.target;
if (node) {
if (node.scrollHeight - node.scrollTop === node.clientHeight) {
this.setState({ autoScroll: true });
this.clearAutoScrollTimeout();
}
if (node.scrollHeight - node.scrollTop - node.clientHeight < 50) return;
}
this.setState({ autoScroll: false });
this.clearAutoScrollTimeout();
this.disableScrollTimer = setTimeout(() => {
self.setState({
autoScroll: true
})
}, 10000);
},
sendMessage = (message) => {
this.props.socket.emit("newMessage", { message: message });
}
componentDidUpdate(prevProps) {
if (prevProps.messages.length < this.props.messages.length) {
this.scrollToBottom();
}
},
clearAutoScrollTimeout = () => {
if (this.disableScrollTimer) clearTimeout(this.disableScrollTimer);
}
scrollToBottom() {
if (!this.state.autoScroll) return;
let node = ReactDOM.findDOMNode(this.refs.messageContainer);
node.scrollTop = node.scrollHeight;
},
temporarilyDisableAutoScroll = (event) => {
let self = this;
let node = event.target;
if (node) {
if (node.scrollHeight - node.scrollTop === node.clientHeight) {
this.setState({ autoScroll: true });
this.clearAutoScrollTimeout();
}
if (node.scrollHeight - node.scrollTop - node.clientHeight < 50) return;
}
this.setState({ autoScroll: false });
this.clearAutoScrollTimeout();
this.disableScrollTimer = setTimeout(() => {
self.setState({
autoScroll: true
})
}, 10000);
}
render() {
const socket = this.props.socket;
const messages = this.props.messages.map(message => {
if (message) {
return <ChatMessage message={message}
key={message._id}
socket={socket}
user={this.props.user} />
}
});
return (
<div>
<ul className="chat" id="chatmessages" ref="messageContainer"
style={{height: this.props.containerHeight - 170}}>
<li className="text-center ">
<a href="#"
onClick={this.loadMoreMessages}
className="btn btn-primary btn-xs">
Load more messages
componentDidUpdate = (prevProps) => {
if (prevProps.messages.length < this.props.messages.length) {
this.scrollToBottom();
}
}
scrollToBottom = () => {
if (!this.state.autoScroll) return;
let node = ReactDOM.findDOMNode(this.refs.messageContainer);
node.scrollTop = node.scrollHeight;
}
render = () => {
const socket = this.props.socket;
const messages = this.props.messages.map(message => {
if (message) {
return <ChatMessage message={message}
key={message._id}
socket={socket}
user={this.props.user} />
}
});
return (
<div>
<ul className="chat" id="chatmessages" ref="messageContainer"
style={{ height: this.props.containerHeight - 170 }}>
<li className="text-center ">
<a href="#"
onClick={this.loadMoreMessages}
className="btn btn-primary btn-xs">
Load more messages
</a>
</li>
{messages}
</ul>
<div>
<MessageBar socket={socket}/>
</div>
</div>
);
}
});
</li>
{messages}
</ul>
<div>
<MessageBar socket={socket} />
</div>
</div>
);
}
}
const imgurRegex = /^(https?:\/\/i\.imgur\.com\/\S*\.(jpg|png))$/i;
const ChatMessage = React.createClass({
propTypes: {
user: React.PropTypes.object.isRequired,
socket: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired
},
class ChatMessage extends React.Component {
static propTypes = {
user: object.isRequired,
socket: object.isRequired,
message: object.isRequired
}
mixins: [
ReactAutolink,
ReactEmoji
],
state = {
createdAt: ""
}
getInitialState() {
return {
createdAt: ""
}
},
autolink = ReactAutolink.autolink
emojify = ReactEmoji.emojify
messageContent: function () {
let self = this;
let message = self.props.message.content
if (message.match(imgurRegex)) {
return (
<div className="imgur-container">
<a href={message} target="_blank">
<img className="imgur-chat" src={message} />
</a>
</div>
);
}
messageContent = () => {
let self = this;
let message = self.props.message.content
if (message.match(imgurRegex)) {
return (
<div className="imgur-container">
<a href={message} target="_blank">
<img className="imgur-chat" src={message} />
</a>
</div>
);
}
return (
<p className="wordwrap">
{
self.autolink(message, {
target: "_blank",
rel: "nofollow"
}).map((elem) => {
if (_.isString(elem)) {
return self.emojify(elem);
} else {
return elem;
}
})
}
</p>
);
},
return (
<p className="wordwrap">
{
self.autolink(message, {
target: "_blank",
rel: "nofollow"
}).map((elem) => {
if (_.isString(elem)) {
return self.emojify(elem);
} else {
return elem;
}
})
}
</p>
);
}
messageTime() {
let self = this;
let ts = new Date(self.props.message.createdAt);
let t = ts.toLocaleTimeString(undefined,{hour:"2-digit", minute:"2-digit"});
let d = ts.toLocaleDateString(undefined,{month: "2-digit", day:"2-digit"});
let r = "";
return r.concat(d," ", t);
},
messageTime = () => {
let self = this;
let ts = new Date(self.props.message.createdAt);
let t = ts.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
let d = ts.toLocaleDateString(undefined, { month: "2-digit", day: "2-digit" });
let r = "";
return r.concat(d, " ", t);
}
render() {
let deleteButton;
let user = this.props.user;
if (user && user.admin) {
deleteButton = <DeleteMessageButton messageId={this.props.message._id}
socket={this.props.socket}/>;
}
return (
<li className="left clearfix chat-message list-unstyled">
<span className="chat-img pull-left">
<img
src={this.props.message.author.avatar}
alt="User Avatar"
className="chat-avatar" />
</span>
<div className="chat-body clearfix">
<div className="header">
<strong className="primary-font">
{this.props.message.author.username}
</strong>
<small className="pull-right text-muted">
<span className="hidden-xs">
{this.messageTime()}
</span>
{deleteButton}
</small>
</div>
{this.messageContent()}
</div>
</li>
);
}
});
render = () => {
let deleteButton;
let user = this.props.user;
if (user && user.admin) {
deleteButton = <DeleteMessageButton messageId={this.props.message._id}
socket={this.props.socket} />;
}
return (
<li className="left clearfix chat-message list-unstyled">
<span className="chat-img pull-left">
<img
src={this.props.message.author.avatar}
alt="User Avatar"
className="chat-avatar" />
</span>
<div className="chat-body clearfix">
<div className="header">
<strong className="primary-font">
{this.props.message.author.username}
</strong>
<small className="pull-right text-muted">
<span className="hidden-xs">
{this.messageTime()}
</span>
{deleteButton}
</small>
</div>
{this.messageContent()}
</div>
</li>
);
}
}
const DeleteMessageButton = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired
},
class DeleteMessageButton extends React.Component {
static propTypes = {
socket: object.isRequired
}
handleClick (e) {
e.preventDefault();
this.props.socket.emit("message:delete", {
id: this.props.messageId
});
},
handleClick = (e) => {
e.preventDefault();
this.props.socket.emit("message:delete", {
id: this.props.messageId
});
}
render() {
return (
<a href="#" onClick={this.handleClick}>
<i className="fa fa-trash-o"></i>
</a>
);
}
})
render = () => {
return (
<a href="#" onClick={this.handleClick}>
<i className="fa fa-trash-o"></i>
</a>
);
}
}
const MessageBar = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired
},
class MessageBar extends React.Component {
static propTypes = {
socket: object.isRequired
}
sendMessage(content) {
this.props.socket.emit("message:new", {
content: content
});
},
state = {
statusMessage: null
}
getInitialState() {
return {
statusMessage: null
};
},
checkInputLength() {
const input = ReactDOM.findDOMNode(this.refs.content).value;
const currentStatusMessage = this.state.statusMessage;
if (input.length > 256) {
return this.setState({
statusMessage: "Maximum of 256 characters will be saved"
})
}
if (currentStatusMessage !== null) {
this.setState({
statusMessage: null
});
}
},
sendMessage = (content) => {
this.props.socket.emit("message:new", {
content: content
});
}
handleInputChange() {
// Noop, later assigned as debounced method in componentWillMount
},
checkInputLength = () => {
const input = ReactDOM.findDOMNode(this.refs.content).value;
const currentStatusMessage = this.state.statusMessage;
if (input.length > 256) {
return this.setState({
statusMessage: "Maximum of 256 characters will be saved"
})
}
if (currentStatusMessage !== null) {
this.setState({
statusMessage: null
});
}
}
handleSubmit(e) {
e.preventDefault();
let content = ReactDOM.findDOMNode(this.refs.content).value.trim();
if (!content) return;
ReactDOM.findDOMNode(this.refs.content).value = '';
this.sendMessage(content);
return;
},
handleInputChange() {
// Noop, later assigned as debounced method in componentWillMount
}
componentWillMount() {
this.handleInputChange = _.debounce(this.checkInputLength, {
leading: false,
trailing: true
});
},
handleSubmit = (e) => {
e.preventDefault();
let content = ReactDOM.findDOMNode(this.refs.content).value.trim();
if (!content) return;
ReactDOM.findDOMNode(this.refs.content).value = '';
this.sendMessage(content);
return;
}
render() {
let statusMessage;
if (this.state.statusMessage !== null) {
statusMessage = <div className="input-group">
<small>{this.state.statusMessage}</small>
</div>;
}
return (
componentWillMount = () => {
this.handleInputChange = _.debounce(this.checkInputLength, {
leading: false,
trailing: true
});
}
<form onSubmit={this.handleSubmit} autoComplete="off">
<div className="input-group">
<input
id="message-input"
type="text"
className="form-control message-input"
ref="content"
onChange={this.handleInputChange}
autoComplete="off"
placeholder="Be polite please..." />
<span className="input-group-btn">
<input
type="submit"
className="btn btn-primary"
id="btn-chat"
value="Send" />
</span>
</div>
{statusMessage}
</form>
);
}
});
render = () => {
let statusMessage;
if (this.state.statusMessage !== null) {
statusMessage = <div className="input-group">
<small>{this.state.statusMessage}</small>
</div>;
}
return (
<form onSubmit={this.handleSubmit} autoComplete="off">
<div className="input-group">
<input
id="message-input"
type="text"
className="form-control message-input"
ref="content"
onChange={this.handleInputChange}
autoComplete="off"
placeholder="Be polite please..." />
<span className="input-group-btn">
<input
type="submit"
className="btn btn-primary"
id="btn-chat"
value="Send" />
</span>
</div>
{statusMessage}
</form>
);
}
}
export { Chatroom }

View file

@ -1,112 +1,119 @@
const $ = require("jquery");
const React = require("react");
const helper = require("javascripts/helper");
const storageAvailable = helper.storageAvailable;
import {MenubarMixin} from "javascripts/components/menubar";
import React from "react"
import $ from "jquery";
import { MenubarMixin } from "./menubar";
import { storageAvailable } from "../helper";
const READ_ARTICLES_STORAGE = "akuh098h209ufnw";
const HTML_ENTITY_REGEX = /&#\d+;/;
const News = exports.News = React.createClass({
mixins: [MenubarMixin],
getInitialState() {
let readArticles = {};
if (storageAvailable('localStorage')) {
const raw = localStorage.getItem(READ_ARTICLES_STORAGE) || {};
let rawJson;
try {
rawJson = JSON.parse(raw);
} catch (e) {
rawJson = {};
}
readArticles = rawJson;
}
class News extends MenubarMixin(React.Component) {
return {
posts: [],
readArticles: readArticles
};
},
constructor(props) {
super(props)
updatePosts(data) {
this.setState({
posts: data.posts.slice(0,5).map(post => {
return {
id: post.id,
url: post.url,
title: post.title
}
})
});
},
this.state = this.getInitialState();
}
renderTitle(title) {
return title.replace(HTML_ENTITY_REGEX, match => {
return String.fromCharCode(match.slice(2, match.length - 1))
});
},
getInitialState = () => {
let readArticles = {};
if (storageAvailable('localStorage')) {
const raw = localStorage.getItem(READ_ARTICLES_STORAGE) || {};
let rawJson;
try {
rawJson = JSON.parse(raw);
} catch (e) {
rawJson = {};
}
readArticles = rawJson;
}
componentDidMount() {
$.getJSON("http://ns2news.org/api-json/get_recent_posts?callback=?")
.done(this.updatePosts);
},
return Object.assign(super.getInitialState(), {
posts: [],
readArticles: readArticles
});
}
markAsRead(post) {
const self = this;
return function (e) {
let readArticles = self.state.readArticles;
readArticles[post.id] = (new Date()).toJSON();
self.setState({readArticles: readArticles});
updatePosts = (data) => {
this.setState({
posts: data.posts.slice(0, 5).map(post => {
return {
id: post.id,
url: post.url,
title: post.title
}
})
});
}
if (storageAvailable('localStorage')) {
localStorage.setItem(READ_ARTICLES_STORAGE, JSON.stringify(readArticles));
}
}
},
renderTitle = (title) => {
return title.replace(HTML_ENTITY_REGEX, match => {
return String.fromCharCode(match.slice(2, match.length - 1))
});
}
hasBeenRead(post) {
return (this.state.readArticles[post.id] !== undefined);
},
componentDidMount = () => {
$.getJSON("http://ns2news.org/api-json/get_recent_posts?callback=?")
.done(this.updatePosts);
}
render() {
const articles = this.state.posts.map(post => {
let postClass = "";
if (!this.hasBeenRead(post)) postClass += "unread";
return (
<li key={post.id}>
<a href={post.url} target="_blank" onClick={this.markAsRead(post)}
className={postClass}>{this.renderTitle(post.title)}</a>
</li>
);
});
markAsRead = (post) => {
const self = this;
return function (e) {
let readArticles = self.state.readArticles;
readArticles[post.id] = (new Date()).toJSON();
self.setState({ readArticles: readArticles });
const unreadArticles = this.state.posts.reduce((prev, post) => {
if (this.hasBeenRead(post)) {
return prev;
} else {
return prev + 1;
}
}, 0)
if (storageAvailable('localStorage')) {
localStorage.setItem(READ_ARTICLES_STORAGE, JSON.stringify(readArticles));
}
}
}
let tag;
if (unreadArticles > 0) {
tag = <span className="label label-success">{unreadArticles}</span>;
}
hasBeenRead = (post) => {
return (this.state.readArticles[post.id] !== undefined);
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>
<i className="fa fa-newspaper-o"></i>
{tag}
</a>
<ul className="dropdown-menu">
<li className="header">NS2News.org</li>
<ul className="news-menu">
{articles}
</ul>
</ul>
</li>
);
}
});
render = () => {
const articles = this.state.posts.map(post => {
let postClass = "";
if (!this.hasBeenRead(post)) postClass += "unread";
return (
<li key={post.id}>
<a href={post.url} target="_blank" onClick={this.markAsRead(post)}
className={postClass}>{this.renderTitle(post.title)}</a>
</li>
);
});
const unreadArticles = this.state.posts.reduce((prev, post) => {
if (this.hasBeenRead(post)) {
return prev;
} else {
return prev + 1;
}
}, 0)
let tag;
if (unreadArticles > 0) {
tag = <span className="label label-success">{unreadArticles}</span>;
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>
<i className="fa fa-newspaper-o"></i>
{tag}
</a>
<ul className="dropdown-menu">
<li className="header">NS2News.org</li>
<ul className="news-menu">
{articles}
</ul>
</ul>
</li>
);
}
}
export { News }

View file

@ -1,50 +1,53 @@
const React = require("react");
import React from "react";
import { func, bool } from "prop-types";
const SettingsPanel = exports.SettingsPanel = React.createClass({
propTypes: {
toggleUpdateTitle: React.PropTypes.func.isRequired,
updateTitle: React.PropTypes.bool.isRequired,
toggleEventsPanel: React.PropTypes.func.isRequired,
showEventsPanel: React.PropTypes.bool.isRequired
},
class SettingsPanel extends React.Component {
propTypes = {
toggleUpdateTitle: func.isRequired,
updateTitle: bool.isRequired,
toggleEventsPanel: func.isRequired,
showEventsPanel: bool.isRequired
}
render() {
return (
<div className="modal fade" id="settingsmodal">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">Settings</h4>
</div>
<div className="modal-body">
<div className="checkbox">
<label className="checkbox-inline">
<input type="checkbox"
onChange={this.props.toggleUpdateTitle}
checked={this.props.updateTitle}/> Update Gather Status in Title (Cabooble Mode) - May require refresh
render = () => {
return (
<div className="modal fade" id="settingsmodal">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">Settings</h4>
</div>
<div className="modal-body">
<div className="checkbox">
<label className="checkbox-inline">
<input type="checkbox"
onChange={this.props.toggleUpdateTitle}
checked={this.props.updateTitle} /> Update Gather Status in Title (Cabooble Mode) - May require refresh
</label>
</div>
</div>
<div className="modal-body">
<div className="checkbox">
<label className="checkbox-inline">
<input type="checkbox"
onChange={this.props.toggleEventsPanel}
checked={this.props.showEventsPanel}/> Show events panel
</div>
</div>
<div className="modal-body">
<div className="checkbox">
<label className="checkbox-inline">
<input type="checkbox"
onChange={this.props.toggleEventsPanel}
checked={this.props.showEventsPanel} /> Show events panel
</label>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default"
data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
}
});
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default"
data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
}
}
export { SettingsPanel }

View file

@ -1,36 +1,34 @@
const React = require("react");
import React from "react";
var SnowMachineMenu = React.createClass({
getInitialState() {
return {
snowMachine: null
}
},
class SnowMachineMenu extends React.Component {
state = {
snowMachine: null
}
componentDidMount() {
const snowMachine = new SnowMachine();
snowMachine.start();
this.setState({ snowMachine: snowMachine });
},
componentDidMount = () => {
const snowMachine = new SnowMachine();
snowMachine.start();
this.setState({ snowMachine: snowMachine });
}
toggle() {
const snowMachine = this.state.snowMachine;
if (snowMachine.timer) {
snowMachine.stop();
} else {
snowMachine.start();
}
},
toggle = () => {
const snowMachine = this.state.snowMachine;
if (snowMachine.timer) {
snowMachine.stop();
} else {
snowMachine.start();
}
}
render() {
return (
<ul className="nav navbar-top-links navbar-right">
<li>
<a href="#" onClick={this.toggle}>
Snow
render = () => {
return (
<ul className="nav navbar-top-links navbar-right">
<li>
<a href="#" onClick={this.toggle}>
Snow
</a>
</li>
</ul>
);
}
});
</li>
</ul>
);
}
}

View file

@ -1,292 +1,293 @@
const $ = require("jquery");
const React = require("react");
const Howl = require("howler").Howl;
const Howler = require("howler").Howler;
const helper = require("javascripts/helper");
import {MenubarMixin} from "javascripts/components/menubar";
const storageAvailable = helper.storageAvailable;
import $ from "jquery"
import React from "react";
import { Howl, Howler } from "howler";
import { storageAvailable } from "../helper";
import { MenubarMixin } from "./menubar";
class SoundController {
constructor () {
if (Howl === undefined) {
throw new Error("Howl.js required to created sound controller");
}
constructor() {
if (Howl === undefined) {
throw new Error("Howl.js required to created sound controller");
}
this.MINIMUM_PLAY_INTERVAL = 20000;
this.MINIMUM_PLAY_INTERVAL = 20000;
this.playGatherMusic = _.throttle(() => {
this.gather.music.play();
}, this.MINIMUM_PLAY_INTERVAL);
this.playGatherMusic = _.throttle(() => {
this.gather.music.play();
}, this.MINIMUM_PLAY_INTERVAL);
this.isMuted = Howler._muted;
let gatherMusic;
if (storageAvailable("localStorage")) {
let volume = localStorage.getItem("gatherVolume");
if (volume !== undefined) Howler.volume(volume);
gatherMusic = localStorage.getItem("gatherMusic");
}
this.isMuted = Howler._muted;
this.tunes = {
"classic": {
description: "Gathers Classic",
url: 'https://www.ensl.org/files/audio/gather-1.mp3'
},
"nights": {
description: "Nights",
url: 'https://www.ensl.org/files/audio/nights.mp3'
},
"robby": {
description: "Robby",
url: 'https://www.ensl.org/files/audio/robby.mp3'
},
"america": {
description: "Infamous",
url: 'https://www.ensl.org/files/audio/america.mp3'
},
"prommah": {
description: "Prommah",
url: 'https://www.ensl.org/files/audio/prommah.mp3'
},
"turts": {
description: "Gorges Rock your Ass",
url: 'https://www.ensl.org/files/audio/turts.mp3'
},
"skyice": {
description: "Skyice",
url: 'https://www.ensl.org/files/audio/skyice.mp3'
},
"justwannahavefun": {
description: "Gorges just want to have fun",
url: 'https://www.ensl.org/files/audio/justwannahavefun.mp3'
},
"eyeofthegorgie": {
description: "Eye of the Gorgie",
url: 'https://www.ensl.org/files/audio/eyeofthegorgie.mp3'
},
"boondock": {
description: "Boondock Marines",
url: 'https://www.ensl.org/files/audio/boondock.mp3'
},
"preclassic": {
description: "Old Gathers Classic",
url: 'https://www.ensl.org/files/audio/gather-5.mp3'
}
}
let gatherMusic;
if (storageAvailable("localStorage")) {
let volume = localStorage.getItem("gatherVolume");
if (volume !== undefined) Howler.volume(volume);
gatherMusic = localStorage.getItem("gatherMusic");
}
this.setupGatherMusic(gatherMusic);
}
this.tunes = {
"classic": {
description: "Gathers Classic",
url: 'https://www.ensl.org/files/audio/gather-1.mp3'
},
"nights": {
description: "Nights",
url: 'https://www.ensl.org/files/audio/nights.mp3'
},
"robby": {
description: "Robby",
url: 'https://www.ensl.org/files/audio/robby.mp3'
},
"america": {
description: "Infamous",
url: 'https://www.ensl.org/files/audio/america.mp3'
},
"prommah": {
description: "Prommah",
url: 'https://www.ensl.org/files/audio/prommah.mp3'
},
"turts": {
description: "Gorges Rock your Ass",
url: 'https://www.ensl.org/files/audio/turts.mp3'
},
"skyice": {
description: "Skyice",
url: 'https://www.ensl.org/files/audio/skyice.mp3'
},
"justwannahavefun": {
description: "Gorges just want to have fun",
url: 'https://www.ensl.org/files/audio/justwannahavefun.mp3'
},
"eyeofthegorgie": {
description: "Eye of the Gorgie",
url: 'https://www.ensl.org/files/audio/eyeofthegorgie.mp3'
},
"boondock": {
description: "Boondock Marines",
url: 'https://www.ensl.org/files/audio/boondock.mp3'
},
"preclassic": {
description: "Old Gathers Classic",
url: 'https://www.ensl.org/files/audio/gather-5.mp3'
}
}
mute() {
this.isMuted = true;
return Howler.mute();
}
this.setupGatherMusic(gatherMusic);
}
unMute() {
this.isMuted = false;
return Howler.unmute();
}
mute = () => {
this.isMuted = true;
return Howler.mute();
}
getVolume() {
return Howler.volume();
}
unMute = () => {
this.isMuted = false;
return Howler.unmute();
}
setVolume(val) {
if (val === undefined ||
typeof val !== 'number' ||
Math.abs(val) > 1) return;
if (storageAvailable("localStorage")) {
localStorage.setItem("gatherVolume", val);
}
return Howler.volume(val);
}
getVolume = () => {
return Howler.volume();
}
play(music) {
if (this.gather && this.gather.music) return this.gather.music.play();
}
setVolume = (val) => {
if (val === undefined ||
typeof val !== 'number' ||
Math.abs(val) > 1) return;
if (storageAvailable("localStorage")) {
localStorage.setItem("gatherVolume", val);
}
return Howler.volume(val);
}
stop(music) {
if (this.gather && this.gather.music) return this.gather.music.stop();
}
play = (music) => {
if (this.gather && this.gather.music) return this.gather.music.play();
}
defaultGatherMusic() {
return "classic";
}
stop = (music) => {
if (this.gather && this.gather.music) return this.gather.music.stop();
}
setupGatherMusic (musicName) {
let self = this;
let gatherMusic = self.tunes[musicName];
defaultGatherMusic() {
return "classic";
}
if (!gatherMusic) {
musicName = this.defaultGatherMusic();
gatherMusic = self.tunes[musicName];
}
setupGatherMusic = (musicName) => {
let self = this;
let gatherMusic = self.tunes[musicName];
if (self.gather && self.gather.name === musicName) return;
if (!gatherMusic) {
musicName = this.defaultGatherMusic();
gatherMusic = self.tunes[musicName];
}
// Stop if already playing
if (self.gather && self.gather.music) {
self.gather.music.stop();
}
if (self.gather && self.gather.name === musicName) return;
let tune = self.tunes[musicName];
self.gather = {
name: musicName,
description: tune.description,
url: tune.url,
music: new Howl({
urls: [tune.url]
})
};
}
// Stop if already playing
if (self.gather && self.gather.music) {
self.gather.music.stop();
}
let tune = self.tunes[musicName];
self.gather = {
name: musicName,
description: tune.description,
url: tune.url,
music: new Howl({
urls: [tune.url]
})
};
}
}
var MusicSelector = React.createClass({
getInitialState() {
return {
music: this.selectedMusic()
}
},
class MusicSelector extends React.Component {
selectedMusic() {
if (storageAvailable("localStorage")) {
return localStorage.getItem("gatherMusic")
|| this.props.soundController.defaultGatherMusic();
} else {
return this.props.soundController.defaultGatherMusic();
}
},
constructor(props) {
super(props)
this.state = {
music: this.selectedMusic()
}
}
setMusic(event) {
let name = event.target.value;
let soundController = this.props.soundController;
let selectedTune = soundController.tunes[name];
if (selectedTune === undefined) return;
this.setState({ music: name });
soundController.setupGatherMusic(name);
if (storageAvailable("localStorage")) {
localStorage.setItem("gatherMusic", name);
}
},
selectedMusic = () => {
if (storageAvailable("localStorage")) {
return localStorage.getItem("gatherMusic")
|| this.props.soundController.defaultGatherMusic();
} else {
return this.props.soundController.defaultGatherMusic();
}
}
render() {
let soundController = this.props.soundController;
let tunes = [];
for (var attr in soundController.tunes) {
let o = soundController.tunes[attr];
o.id = attr;
tunes.push(o);
}
let options = tunes.map(tune => {
return <option key={tune.id} value={tune.id}>{tune.description}</option>;
});
return (
<div className="form-group music-select">
<label>Music</label>
<select
className="form-control"
defaultValue={this.state.music}
onChange={this.setMusic}
value={this.state.music}>
{options}
</select>
</div>
);
}
})
setMusic = (event) => {
let name = event.target.value;
let soundController = this.props.soundController;
let selectedTune = soundController.tunes[name];
if (selectedTune === undefined) return;
this.setState({ music: name });
soundController.setupGatherMusic(name);
if (storageAvailable("localStorage")) {
localStorage.setItem("gatherMusic", name);
}
}
var SoundPanel = React.createClass({
mixins: [MenubarMixin],
render = () => {
let soundController = this.props.soundController;
let tunes = [];
for (var attr in soundController.tunes) {
let o = soundController.tunes[attr];
o.id = attr;
tunes.push(o);
}
let options = tunes.map(tune => {
return <option key={tune.id} value={tune.id}>{tune.description}</option>;
});
return (
<div className="form-group music-select">
<label>Music</label>
<select
className="form-control"
defaultValue={this.state.music}
onChange={this.setMusic}
value={this.state.music}>
{options}
</select>
</div>
);
}
}
componentDidMount() {
let soundController = this.props.soundController;
let scale = 10;
class SoundPanel extends MenubarMixin(React.Component) {
$('a#sound-dropdown').on('click', function (event) {
$(this).parent().toggleClass('open');
});
constructor(props) {
super(props)
this.state = this.getInitialState();
}
$("#volume-slide").slider({
min: 0,
max: scale,
step: 1
}).on("slideStop", ({value}) => {
soundController.setVolume(value / scale);
}).slider('setValue', soundController.getVolume() * scale);
},
componentDidMount = () => {
let soundController = this.props.soundController;
let scale = 10;
mute() {
this.props.soundController.mute();
this.forceUpdate();
},
$('a#sound-dropdown').on('click', function (event) {
$(this).parent().toggleClass('open');
});
unMute() {
this.props.soundController.unMute();
this.forceUpdate();
},
$("#volume-slide").slider({
min: 0,
max: scale,
step: 1
}).on("slideStop", ({ value }) => {
soundController.setVolume(value / scale);
}).slider('setValue', soundController.getVolume() * scale);
}
play() {
this.props.soundController.stop();
this.props.soundController.play();
},
mute = () => {
this.props.soundController.mute();
this.forceUpdate();
}
stop() {
this.props.soundController.stop();
},
unMute = () => {
this.props.soundController.unMute();
this.forceUpdate();
}
render() {
let soundController = this.props.soundController;
let mutedIcon, mutedButton;
if (soundController.isMuted) {
mutedIcon = <i className="fa fa-volume-off fa-fw"></i>;
mutedButton = <li>
<a href="#" onClick={this.unMute}>
{mutedIcon}&nbsp;Muted
play = () => {
this.props.soundController.stop();
this.props.soundController.play();
}
stop = () => {
this.props.soundController.stop();
}
render = () => {
let soundController = this.props.soundController;
let mutedIcon, mutedButton;
if (soundController.isMuted) {
mutedIcon = <i className="fa fa-volume-off fa-fw"></i>;
mutedButton = <li>
<a href="#" onClick={this.unMute}>
{mutedIcon}&nbsp;Muted
</a>
</li>;
} else {
mutedIcon = <i className="fa fa-volume-up fa-fw"></i>;
mutedButton = <li>
<a href="#" onClick={this.mute}>
{mutedIcon}&nbsp;Unmuted
</li>;
} else {
mutedIcon = <i className="fa fa-volume-up fa-fw"></i>;
mutedButton = <li>
<a href="#" onClick={this.mute}>
{mutedIcon}&nbsp;Unmuted
</a>
</li>;
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>{mutedIcon}</a>
<ul className="dropdown-menu">
<li className="header">Sound Settings</li>
<ul className="sound-menu">
{mutedButton}
<li>
<a href='#' onClick={this.play}>
<i className="fa fa-play"></i>&nbsp;Play
</li>;
}
return (
<li className={this.componentClass()}>
<a href="#" onClick={this.toggleShow}>{mutedIcon}</a>
<ul className="dropdown-menu">
<li className="header">Sound Settings</li>
<ul className="sound-menu">
{mutedButton}
<li>
<a href='#' onClick={this.play}>
<i className="fa fa-play"></i>&nbsp;Play
</a>
</li>
<li>
<a href='#' onClick={this.stop}>
<i className="fa fa-stop"></i>&nbsp;Stop
</li>
<li>
<a href='#' onClick={this.stop}>
<i className="fa fa-stop"></i>&nbsp;Stop
</a>
</li>
<hr />
<li>
<div className="volume-slide">
<label>Volume</label>
<div id="volume-slide"></div>
</div>
</li>
<li>
<MusicSelector soundController={soundController} />
</li>
</ul>
</ul>
</li>
);
}
});
</li>
<hr />
<li>
<div className="volume-slide">
<label>Volume</label>
<div id="volume-slide"></div>
</div>
</li>
<li>
<MusicSelector soundController={soundController} />
</li>
</ul>
</ul>
</li>
);
}
}
module.exports = {
SoundController: SoundController,
SoundPanel: SoundPanel
};
export { SoundController, SoundPanel }

View file

@ -1,389 +1,387 @@
import { LifeformIcons } from "javascripts/components/gather";
const React = require("react");
const helper = require("javascripts/helper");
const enslUrl = helper.enslUrl;
const modalId = helper.modalId;
const obsUrl = helper.observatoryUrl;
const Ps = require('perfect-scrollbar');
import React from "react";
import { object, number, func, array } from "prop-types";
import { enslUrl, observatoryUrl as obsUrl } from "../helper";
const DisconnectUserButton = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired,
id: React.PropTypes.number.isRequired
},
getDefaultProps() {
return {
id: null
};
},
class DisconnectUserButton extends React.Component {
static propTypes = {
socket: object.isRequired,
id: number.isRequired
}
state = {
id: null
}
disconnectUser() {
this.props.socket.emit("users:disconnect", {
id: this.props.id
});
},
disconnectUser = () => {
this.props.socket.emit("users:disconnect", {
id: this.props.id
});
}
render() {
return <button
className="btn btn-danger"
onClick={this.disconnectUser}>
Disconnect User</button>
}
});
render = () => {
return <button
className="btn btn-danger"
onClick={this.disconnectUser}>
Disconnect User</button>
}
}
const UserModal = React.createClass({
propTypes: {
user: React.PropTypes.object.isRequired,
socket: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
close: React.PropTypes.func.isRequired
},
class UserModal extends React.Component {
static propTypes = {
user: object.isRequired,
socket: object.isRequired,
currentUser: object.isRequired,
close: func.isRequired
}
render() {
const currentUser = this.props.currentUser;
const user = this.props.user;
let hiveStats;
if (user.hive.id) {
hiveStats = [
<tr key="stats"><td><strong>Hive Stats</strong></td><td></td></tr>,
<tr key="elo">
<td>ELO</td>
<td>{user.hive.skill}</td>
</tr>,
<tr key="hours">
<td>Play Time (Hours)</td>
<td>{Math.round(user.hive.playTime / 3600)}</td>
</tr>,
<tr key="losses">
<td>Marine Play Time (Hours)</td>
<td>{_.round(user.hive.marine_playtime / 3600, 1)}</td>
</tr>,
<tr key="kills">
<td>Alien Play Time (Hours)</td>
<td>{_.round(user.hive.alien_playtime / 3600, 1)}</td>
</tr>,
<tr key="assists">
<td>Commander Play Time (Hours)</td>
<td>{_.round(user.hive.commander_time / 3600, 1)}</td>
</tr>,
<tr key="wins">
<td>Player ID</td>
<td>{user.hive.pid}</td>
</tr>
]
}
let adminOptions;
if (currentUser.admin) {
adminOptions = <DisconnectUserButton id={user.id} socket={this.props.socket} />;
}
render = () => {
const currentUser = this.props.currentUser;
const user = this.props.user;
let hiveStats;
if (user.hive.id) {
hiveStats = [
<tr key="stats"><td><strong>Hive Stats</strong></td><td></td></tr>,
<tr key="elo">
<td>ELO</td>
<td>{user.hive.skill}</td>
</tr>,
<tr key="hours">
<td>Play Time (Hours)</td>
<td>{Math.round(user.hive.playTime / 3600)}</td>
</tr>,
<tr key="losses">
<td>Marine Play Time (Hours)</td>
<td>{_.round(user.hive.marine_playtime / 3600, 1)}</td>
</tr>,
<tr key="kills">
<td>Alien Play Time (Hours)</td>
<td>{_.round(user.hive.alien_playtime / 3600, 1)}</td>
</tr>,
<tr key="assists">
<td>Commander Play Time (Hours)</td>
<td>{_.round(user.hive.commander_time / 3600, 1)}</td>
</tr>,
<tr key="wins">
<td>Player ID</td>
<td>{user.hive.pid}</td>
</tr>
]
}
let adminOptions;
if (currentUser.admin) {
adminOptions = <DisconnectUserButton id={user.id} socket={this.props.socket} />;
}
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={this.props.close}
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">
<img src="blank.gif"
className={"flag flag-" + ((user.country === null) ? "eu" :
user.country.toLowerCase())}
alt={user.country} />&nbsp;
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={this.props.close}
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">
<img src="blank.gif"
className={"flag flag-" + ((user.country === null) ? "eu" :
user.country.toLowerCase())}
alt={user.country} />&nbsp;
{user.username}
</h4>
</div>
<div className="modal-body">
<div className="text-center">
<img
src={user.avatar}
alt="User Avatar" />
</div>
<table className="table borderless">
<tbody>
<tr>
<td>Lifeforms</td>
<td><LifeformIcons gatherer={{ user: user }} /></td>
</tr>
<tr>
<td>Links</td>
<td>
<a href={enslUrl(user)}
className="btn btn-xs btn-primary"
target="_blank">ENSL Profile</a>&nbsp;
</h4>
</div>
<div className="modal-body">
<div className="text-center">
<img
src={user.avatar}
alt="User Avatar" />
</div>
<table className="table borderless">
<tbody>
<tr>
<td>Lifeforms</td>
<td><LifeformIcons gatherer={{ user: user }} /></td>
</tr>
<tr>
<td>Links</td>
<td>
<a href={enslUrl(user)}
className="btn btn-xs btn-primary"
target="_blank">ENSL Profile</a>&nbsp;
<a href={obsUrl({ user: user })}
className="btn btn-xs btn-primary"
target="_blank">Observatory Profile</a>
</td>
</tr>
{hiveStats}
</tbody>
</table>
</div>
<div className="modal-footer">
{adminOptions}
<button type="button"
className="btn btn-default"
onClick={this.props.close}
>Close</button>
</div>
</div>
</div>
);
}
})
className="btn btn-xs btn-primary"
target="_blank">Observatory Profile</a>
</td>
</tr>
{hiveStats}
</tbody>
</table>
</div>
<div className="modal-footer">
{adminOptions}
<button type="button"
className="btn btn-default"
onClick={this.props.close}
>Close</button>
</div>
</div>
</div>
);
}
}
const UserItem = React.createClass({
propTypes: {
user: React.PropTypes.object.isRequired,
socket: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
mountModal: React.PropTypes.func.isRequired
},
class UserItem extends React.Component {
static propTypes = {
user: object.isRequired,
socket: object.isRequired,
currentUser: object.isRequired,
mountModal: func.isRequired
}
openModal(e) {
e.preventDefault();
this.props.mountModal({
component: UserModal,
props: {
user: this.props.user,
currentUser: this.props.currentUser,
socket: this.props.socket
}
});
},
openModal = (e) => {
e.preventDefault();
this.props.mountModal({
component: UserModal,
props: {
user: this.props.user,
currentUser: this.props.currentUser,
socket: this.props.socket
}
});
}
render() {
const user = this.props.user;
const currentUser = this.props.currentUser;
return (
<li className="users-list-group-item">
<a href="#" onClick={this.openModal}>{user.username}</a>
</li>
);
}
});
render = () => {
const user = this.props.user;
const currentUser = this.props.currentUser;
return (
<li className="users-list-group-item">
<a href="#" onClick={this.openModal}>{user.username}</a>
</li>
);
}
}
const UserMenu = exports.UserMenu = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired,
users: React.PropTypes.array.isRequired,
mountModal: React.PropTypes.func.isRequired
},
class UserMenu extends React.Component {
static propTypes = {
socket: object.isRequired,
users: array.isRequired,
mountModal: func.isRequired
}
componentDidMount() {
render = () => {
const users = this.props.users
.sort((a, b) => (a.username.toLowerCase() > b.username.toLowerCase()) ? 1 : -1)
.map(user => {
return <UserItem user={user} key={user.id}
currentUser={this.props.user} socket={this.props.socket}
mountModal={this.props.mountModal} />
});
return (
<div>
<ul className="users-list-group" id="user-list">
{users}
</ul>
</div>
);
}
}
},
class ProfileModal extends React.Component {
static propTypes = {
user: object.isRequired,
socket: object.isRequired,
close: func.isRequired
}
render() {
const users = this.props.users
.sort((a, b) => (a.username.toLowerCase() > b.username.toLowerCase()) ? 1 : -1)
.map(user => {
return <UserItem user={user} key={user.id}
currentUser={this.props.user} socket={this.props.socket}
mountModal={this.props.mountModal} />
});
return (
<div>
<ul className="users-list-group" id="user-list">
{users}
</ul>
</div>
);
}
});
constructor(props) {
super(props);
this.state = this.getInitialState();
}
const ProfileModal = exports.ProfileModal = React.createClass({
propTypes: {
user: React.PropTypes.object.isRequired,
socket: React.PropTypes.object.isRequired,
close: React.PropTypes.func.isRequired
},
getInitialState = () => {
const user = this.props.user;
console.log(user.profile);
return {
abilities: {
skulk: user.profile.abilities.skulk,
lerk: user.profile.abilities.lerk,
gorge: user.profile.abilities.gorge,
fade: user.profile.abilities.fade,
onos: user.profile.abilities.onos,
commander: user.profile.abilities.commander
},
skill: user.profile.skill
};
}
getInitialState() {
const user = this.props.user;
console.log(user.profile);
return {
abilities: {
skulk: user.profile.abilities.skulk,
lerk: user.profile.abilities.lerk,
gorge: user.profile.abilities.gorge,
fade: user.profile.abilities.fade,
onos: user.profile.abilities.onos,
commander: user.profile.abilities.commander
},
skill: user.profile.skill
};
},
handleUserUpdate = (e) => {
e.preventDefault();
this.props.socket.emit("users:update:profile", {
id: this.props.user.id,
profile: {
abilities: this.state.abilities,
skill: this.state.skill
}
});
this.props.close();
}
handleUserUpdate(e) {
e.preventDefault();
this.props.socket.emit("users:update:profile", {
id: this.props.user.id,
profile: {
abilities: this.state.abilities,
skill: this.state.skill
}
});
this.props.close();
},
handleAbilityChange = (e) => {
let abilities = this.state.abilities;
abilities[e.target.name] = e.target.checked;
this.setState({ abilities: abilities });
}
handleAbilityChange(e) {
let abilities = this.state.abilities;
abilities[e.target.name] = e.target.checked;
this.setState({ abilities: abilities });
},
handleSkillChange = (e) => {
this.setState({ skill: e.target.value });
}
handleSkillChange(e) {
this.setState({ skill: e.target.value });
},
render = () => {
const user = this.props.user;
if (!user) return false;
render() {
const user = this.props.user;
if (!user) return false;
const abilities = this.state.abilities;
const abilities = this.state.abilities;
let abilitiesForm = [];
for (let lifeform in abilities) {
abilitiesForm.push(
<div key={lifeform} className="checkbox">
<label className="checkbox-inline">
<input type="checkbox" name={lifeform}
checked={abilities[lifeform]} onChange={this.handleAbilityChange} />
{_.capitalize(lifeform)}
</label>
</div>
);
}
let abilitiesForm = [];
for (let lifeform in abilities) {
abilitiesForm.push(
<div key={lifeform} className="checkbox">
<label className="checkbox-inline">
<input type="checkbox" name={lifeform}
checked={abilities[lifeform]} onChange={this.handleAbilityChange} />
{_.capitalize(lifeform)}
</label>
</div>
);
}
let skillLevel = user.profile.skill;
let skillLevels = _.uniq(["Low Skill", "Medium Skill", "High Skill", skillLevel])
.filter(skill => { return typeof skill === 'string' })
.map(skill => { return <option key={skill} value={skill}>{skill}</option> });
let skillLevel = user.profile.skill;
let skillLevels = _.uniq(["Low Skill", "Medium Skill", "High Skill", skillLevel])
.filter(skill => { return typeof skill === 'string' })
.map(skill => { return <option key={skill} value={skill}>{skill}</option> });
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" aria-label="Close"
onClick={this.props.close}>
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">Profile</h4>
</div>
<div className="modal-body" id="profile-panel">
<form>
<div className="form-group">
<label>Player Skill</label><br />
<select
value={skillLevel}
className="form-control"
onChange={this.handleSkillChange}>
{skillLevels}
</select>
<p className="add-top"><small>
Try to give an accurate representation of your skill to raise
the quality of your gathers
return (
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" aria-label="Close"
onClick={this.props.close}>
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">Profile</h4>
</div>
<div className="modal-body" id="profile-panel">
<form>
<div className="form-group">
<label>Player Skill</label><br />
<select
value={skillLevel}
className="form-control"
onChange={this.handleSkillChange}>
{skillLevels}
</select>
<p className="add-top"><small>
Try to give an accurate representation of your skill to raise
the quality of your gathers
</small></p>
</div>
<hr />
<div className="form-group">
<label>Preferred Lifeforms</label><br />
{abilitiesForm}
<p><small>
Specify which lifeforms you'd like to play in the gather
</div>
<hr />
<div className="form-group">
<label>Preferred Lifeforms</label><br />
{abilitiesForm}
<p><small>
Specify which lifeforms you'd like to play in the gather
</small></p>
</div>
<hr />
<p className="small">
You will need to rejoin the gather to see your updated profile
</div>
<hr />
<p className="small">
You will need to rejoin the gather to see your updated profile
</p>
<div className="form-group">
<button
type="submit"
className="btn btn-primary"
onClick={this.handleUserUpdate}>
Update &amp; Close</button>
</div>
</form>
</div>
</div>
</div>
);
}
});
<div className="form-group">
<button
type="submit"
className="btn btn-primary"
onClick={this.handleUserUpdate}>
Update &amp; Close</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
const CurrentUser = exports.CurrentUser = React.createClass({
render() {
if (this.props.user) {
let adminOptions;
if (this.props.user.admin || this.props.user.moderator) {
adminOptions = (
<li>
<a href="#" data-toggle="modal" data-target="#adminmodal">
<i className="fa fa-magic fa-fw"></i> Administration
class CurrentUser extends React.Component {
render = () => {
if (this.props.user) {
let adminOptions;
if (this.props.user.admin || this.props.user.moderator) {
adminOptions = (
<li>
<a href="#" data-toggle="modal" data-target="#adminmodal">
<i className="fa fa-magic fa-fw"></i> Administration
</a>
</li>
)
}
return (
<li className="dropdown">
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{this.props.user.username} &nbsp;<img src={this.props.user.avatar}
alt="User Avatar"
height="20"
width="20" /> <i className="fa fa-caret-down"></i>
</a>
<ul className="dropdown-menu dropdown-user">
<li>
<a data-toggle="modal"
data-target="#profilemodal"
href="#"><i className="fa fa-user fa-fw"></i> Profile</a>
</li>
<li>
<a data-toggle="modal"
data-target="#settingsmodal"
href="#"><i className="fa fa-gear fa-fw"></i> Settings</a>
</li>
{adminOptions}
</ul>
</li>
);
} else {
return false;
}
}
});
</li>
)
}
return (
<li className="dropdown">
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{this.props.user.username} &nbsp;<img src={this.props.user.avatar}
alt="User Avatar"
height="20"
width="20" /> <i className="fa fa-caret-down"></i>
</a>
<ul className="dropdown-menu dropdown-user">
<li>
<a data-toggle="modal"
data-target="#profilemodal"
href="#"><i className="fa fa-user fa-fw"></i> Profile</a>
</li>
<li>
<a data-toggle="modal"
data-target="#settingsmodal"
href="#"><i className="fa fa-gear fa-fw"></i> Settings</a>
</li>
{adminOptions}
</ul>
</li>
);
} else {
return false;
}
}
}
var AssumeUserIdButton = exports.AssumeUserIdButton = React.createClass({
propTypes: {
socket: React.PropTypes.object.isRequired,
gatherer: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
},
class AssumeUserIdButton extends React.Component {
static propTypes = {
socket: object.isRequired,
gatherer: object.isRequired,
currentUser: object.isRequired,
}
assumeId(e) {
e.preventDefault();
if (this.props.gatherer) {
this.props.socket.emit("users:authorize", {
id: this.props.gatherer.id
});
// Refresh Gather list
setTimeout(() => {
this.props.socket.emit("gather:refresh");
}, 5000);
}
},
assumeId = (e) => {
e.preventDefault();
if (this.props.gatherer) {
this.props.socket.emit("users:authorize", {
id: this.props.gatherer.id
});
// Refresh Gather list
setTimeout(() => {
this.props.socket.emit("gather:refresh");
}, 5000);
}
}
render() {
let currentUser = this.props.currentUser;
let gatherer = this.props.gatherer;
if (currentUser && gatherer) {
return <button
className="btn btn-xs btn-danger"
onClick={this.assumeId}>Assume User ID</button>
}
}
});
render = () => {
let currentUser = this.props.currentUser;
let gatherer = this.props.gatherer;
if (currentUser && gatherer) {
return <button
className="btn btn-xs btn-danger"
onClick={this.assumeId}>Assume User ID</button>
}
}
}
export { UserMenu, ProfileModal, CurrentUser, AssumeUserIdButton }

View file

@ -4,81 +4,83 @@
// 2. Increments ID vote tally for every vote
// 3. Sorts
const rankVotes = exports.rankVotes = function (votes, candidates) {
var initial = candidates.reduce(function (acc, candidate) {
acc[candidate.id] = 0;
return acc;
}, {});
const rankVotes = function (votes, candidates) {
var initial = candidates.reduce(function (acc, candidate) {
acc[candidate.id] = 0;
return acc;
}, {});
var scores = votes.reduce(function (acc, id) {
if (acc[id] !== undefined) {
acc[id]++;
}
return acc;
}, initial);
var scores = votes.reduce(function (acc, id) {
if (acc[id] !== undefined) {
acc[id]++;
}
return acc;
}, initial);
var rank = [];
var rank = [];
for (var id in scores) {
if (scores.hasOwnProperty(id)) {
rank.push({
id: parseInt(id, 10),
count: scores[id]
});
}
}
for (var id in scores) {
if (scores.hasOwnProperty(id)) {
rank.push({
id: parseInt(id, 10),
count: scores[id]
});
}
}
return rank.sort(function (a, b) {
if (b.count === a.count) {
return b.id - a.id;
} else {
return b.count - a.count;
}
}).map(function (tally) {
return tally.id
}).map(function (id) {
return candidates.reduce(function (acc, candidate) {
if (candidate.id === id) return candidate;
return acc;
});
});
return rank.sort(function (a, b) {
if (b.count === a.count) {
return b.id - a.id;
} else {
return b.count - a.count;
}
}).map(function (tally) {
return tally.id
}).map(function (id) {
return candidates.reduce(function (acc, candidate) {
if (candidate.id === id) return candidate;
return acc;
});
});
};
const enslUrl = exports.enslUrl = (gatherer) => {
return `https://www.ensl.org/users/${gatherer.id}`
const enslUrl = (gatherer) => {
return `https://www.ensl.org/users/${gatherer.id}`
};
const hiveUrl = exports.hiveUrl = (gatherer) => {
const hiveId = gatherer.user.hive.id;
if (hiveId) {
return `http://hive.naturalselection2.com/profile/${hiveId}`;
} else {
return null;
}
const hiveUrl = (gatherer) => {
const hiveId = gatherer.user.hive.id;
if (hiveId) {
return `http://hive.naturalselection2.com/profile/${hiveId}`;
} else {
return null;
}
};
const modalId = exports.modalId = (user) => {
return `user-modal-${user.id}`;
const modalId = (user) => {
return `user-modal-${user.id}`;
};
const storageAvailable = exports.storageAvailable = (type) => {
try {
var storage = window[type],
x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch (e) {
return false;
}
const storageAvailable = (type) => {
try {
var storage = window[type],
x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch (e) {
return false;
}
};
const observatoryUrl = exports.observatoryUrl = (gatherer) => {
const steamId = gatherer.user.steam.id;
if (steamId) {
return `https://observatory.morrolan.ch/player?steam_id=STEAM_${steamId}`;
} else {
return null;
}
const observatoryUrl = (gatherer) => {
const steamId = gatherer.user.steam.id;
if (steamId) {
return `https://observatory.morrolan.ch/player?steam_id=STEAM_${steamId}`;
} else {
return null;
}
};
export { enslUrl, hiveUrl, modalId, observatoryUrl, rankVotes, storageAvailable }

11
bin/entry.sh Normal file
View file

@ -0,0 +1,11 @@
#!/bin/bash
cd /app
if [ -f "/home/web/tmp/.updatePublic" ]; then
cp -r /home/web/tmp/public /app/public
rm -rf /home/web/tmp/public
rm /home/web/tmp/.updatePublic
fi
node index.js

View file

@ -34,7 +34,12 @@ exports.config = {
// Configure your plugins
plugins: {
babel: {
presets: ["es2015", "react"],
presets: [
["@babel/preset-env", {
"bugfixes": true,
"shippedProposals": true
}],
"@babel/preset-react"],
// Do not use ES6 compiler in vendor code
ignore: [/vendor/]
}
@ -46,16 +51,16 @@ exports.config = {
// 'app.js': ['app']
// }
// },
npm: {
enabled: true,
styles: {
"bootstrap-solarized": ["bootstrap-solarized-dark.css"],
"toastr": ["build/toastr.min.css"]
},
whitelist: ["react", "react-dom", "jquery", "lodash",
whitelist: ["react", "react-dom", "jquery", "lodash",
"react-autolink", "react-dom", "react-emoji", "howler",
"bootstrap", "perfect-scrollbar", "moment", "toastr",
"bootstrap", "perfect-scrollbar", "moment", "toastr",
"socket.io-client"],
globals: {
"_": "lodash",
@ -63,7 +68,5 @@ exports.config = {
"$": "jquery",
"toastr": "toastr"
}
},
notifications: true
}
};

View file

@ -19,7 +19,7 @@ module.exports = app => {
});
// Enforce HTTPS in production
if (env === 'production') {
app.use((req,res,next) => {
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=2592000; includeSubdomains'); // Enforce usage of HTTPS; max-age = 30 days
next();
});
@ -28,27 +28,27 @@ module.exports = app => {
app.use(cookieParser());
app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
// Use winston on production
var log;
if (env !== 'development') {
log = {
stream: {
write: (message, encoding) => {
winston.info(message);
}
}
};
} else {
log = 'dev';
}
// // Use winston on production
// var log;
// if (env !== 'development') {
// log = {
// stream: {
// write: (message, encoding) => {
// winston.info(message);
// }
// }
// };
// } else {
// log = 'dev';
// }
// if (env !== 'test') app.use(morgan(log));
if (env !== 'test') app.use(morgan(log));
var hbs = exphbs({
defaultLayout: 'main',
defaultLayout: 'main',
extname: '.hbs'
});
app.engine('.hbs', hbs);
app.set('view engine', '.hbs');
app.engine('.hbs', hbs);
app.set('view engine', '.hbs');
};

View file

@ -6,25 +6,22 @@ var mongoose = require("mongoose");
var config = require(path.join(__dirname, "../config/config.js"));
var connect = function () {
mongoose.connect(config.mongo.uri, {
server: {
socketOptions: {
keepAlive: 1,
connectTimeoutMS: 30000
}
}
});
mongoose.connect(config.mongo.uri, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(
() => winston.info("MongoDB: Connection established"),
error => winston.error(error)
);
};
connect();
mongoose.connection.on("error", function (error) {
winston.error(error);
});
mongoose.connection.on("error", (error) => winston.error(error));
mongoose.connection.on("disconnected", () => winston.error("MongoDB: Was disconnected."));
mongoose.connection.on("reconnectFailed", () => winston.error("MongoDB: Reconnect Failed!"));
mongoose.connection.on("disconnected", function () {
winston.error("MongoDB: Was disconnected.");
});
mongoose.connection.on("reconnected", () => winston.info("MongoDB: Connection established"));
// Load models
require(path.join(__dirname, "/models/event"));

38
docker-compose.yml Normal file
View file

@ -0,0 +1,38 @@
version: "3.8"
services:
production:
container_name: ensl_gather_production
build:
context: ./
target: production
dockerfile: Dockerfile
depends_on:
- mongodb
command: ["/app/bin/entry.sh"]
user: web:web
environment:
- NODE_ENV=production
- PORT=$NODE_PORT
- "MONGOLAB_URI=mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@mongodb/${MONGODB_DATABASE}"
- RAILS_SECRET
- NEW_RELIC_LICENSE_KEY
- GATHER_STEAM_ACCOUNT
- GATHER_STEAM_PASSWORD
- GATHER_DISCORD_HOOK_ID
- GATHER_DISCORD_HOOK_TOKEN
- RANDOM_USER
- FIXED_USER
ports:
- "${NODE_PORT}:${NODE_PORT}"
volumes:
- "./public:/app/public"
init: true
mongodb:
image: "bitnami/mongodb:latest"
container_name: ensl_gather_mongodb
volumes:
- "./db/data:/bitnami/mongodb"
environment:
- MONGODB_USERNAME
- MONGODB_PASSWORD
- MONGODB_DATABASE

View file

@ -7,118 +7,117 @@ var logger = require("winston");
var querystring = require('querystring');
var config = require(path.join(__dirname, "../../config/config"));
const SECRET_TOKEN = config.secret_token;
var childProcess = require("child_process").exec;
const Marshal = require('marshal');
const MAP_CATEGORY = 45;
const SERVER_CATEGORY = 45;
function EnslClient (options) {
if (!(this instanceof EnslClient)) {
return new EnslClient(options);
}
function EnslClient(options) {
if (!(this instanceof EnslClient)) {
return new EnslClient(options);
}
this.baseUrl = config.ensl_url;
this.baseUrl = config.ensl_url;
}
EnslClient.prototype.getUserById = function (options, callback) {
var id = options.id;
var url = this.baseUrl + "api/v1/users/" + id;
return request({
url: url,
json: true
}, callback);
var id = options.id;
var url = this.baseUrl + "api/v1/users/" + id;
return request({
url: url,
json: true
}, callback);
};
EnslClient.prototype.getTeamById = function (options, callback) {
const id = options.id;
const url = `${this.baseUrl}api/v1/teams/${id}`;
return request({
url: url,
json: true
}, callback);
const id = options.id;
const url = `${this.baseUrl}api/v1/teams/${id}`;
return request({
url: url,
json: true
}, callback);
};
EnslClient.prototype.getServers = function (callback) {
const url = this.baseUrl + "api/v1/servers";
return request({
url: url,
json: true
}, (error, response, data) => {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error("Non-200 status code received"));
return callback(null, {
servers: data.servers.filter(function (server) {
return server.category_id === SERVER_CATEGORY;
})
});
});
const url = this.baseUrl + "api/v1/servers";
return request({
url: url,
json: true
}, (error, response, data) => {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error("Non-200 status code received"));
return callback(null, {
servers: data.servers.filter(function (server) {
return server.category_id === SERVER_CATEGORY;
})
});
});
};
EnslClient.prototype.getMaps = function (callback) {
const url = this.baseUrl + "api/v1/maps";
return request({
url: url,
json: true
}, (error, response, data) => {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error("Non-200 status code received"));
return callback(null, {
maps: data.maps.filter(map => {
return map.category_id === MAP_CATEGORY;
})
});
});
const url = this.baseUrl + "api/v1/maps";
return request({
url: url,
json: true
}, (error, response, data) => {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error("Non-200 status code received"));
return callback(null, {
maps: data.maps.filter(map => {
return map.category_id === MAP_CATEGORY;
})
});
});
};
EnslClient.prototype.getFullAvatarUri = function (url) {
return this.baseUrl + url.replace(/^\//, "");
return this.baseUrl + url.replace(/^\//, "");
};
EnslClient.parseCookies = function (socket) {
let cookieString = socket.request.headers.cookie;
if (typeof cookieString !== 'string') return null;
let cookies = socket.request.headers.cookie.split(";")
.map(cookie => cookie.trim())
.reduce((acc, cookie) => {
let values = cookie.split("=");
let attr = values[0];
let val = values[1];
if (attr && val) acc[attr] = val;
return acc;
}, {})
return cookies;
let cookieString = socket.request.headers.cookie;
if (typeof cookieString !== 'string') return null;
let cookies = socket.request.headers.cookie.split(";")
.map(cookie => cookie.trim())
.reduce((acc, cookie) => {
let values = cookie.split("=");
let attr = values[0];
let val = values[1];
if (attr && val) acc[attr] = val;
return acc;
}, {})
return cookies;
};
EnslClient.decodeSession = function (sessionCookie, callback) {
if (typeof sessionCookie !== 'string') {
return callback(new Error("Invalid cookie"), null);
}
if (typeof sessionCookie !== 'string') {
return callback(new Error("Invalid cookie"), null);
}
var session = sessionCookie.split("--");
if (session.length !== 2) {
return callback(new Error("Invalid cookie: No signature provided"), null);
}
var session = sessionCookie.split("--");
if (session.length !== 2) {
return callback(new Error("Invalid cookie: No signature provided"), null);
}
// Separate text and signature
var text = querystring.unescape(session[0]);
var signature = session[1];
// Separate text and signature
var text = querystring.unescape(session[0]);
var signature = session[1];
// Verify signature
if (crypto.createHmac("sha1", SECRET_TOKEN).update(text).digest('hex') !== signature) {
return callback(new Error("Invalid cookie signature"), null);
}
// Verify signature
if (crypto.createHmac("sha1", SECRET_TOKEN).update(text).digest('hex') !== signature) {
return callback(new Error("Invalid cookie signature"), null);
}
var cb = callback;
childProcess("ruby unmarshal.rb " + text, {
cwd: path.join(__dirname, "../../scripts")
}, function (err, stdout, stderr) {
var userId = parseInt(stdout, 10);
if (isNaN(userId)) {
return callback(new Error("Invalid cookie: User ID not found"), null);
} else {
return callback(null, userId);
}
});
let railsSession = new Marshal(text, 'base64').toJSON();
if (isNaN(railsSession.user)) {
return callback(new Error("Invalid cookie: User ID not found"), null);
} else {
return callback(null, railsSession.user);
}
};
module.exports = EnslClient;

2158
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -31,10 +31,6 @@
"homepage": "https://github.com/cblanc/sws_gathers",
"dependencies": {
"async": "~1.4.0",
"babel": "~6.0.0",
"babel-brunch": "~7.0.0",
"babel-preset-es2015": "~6.3.13",
"babel-preset-react": "~6.3.13",
"bootstrap": "~4.0.0",
"bootstrap-solarized": "~1.0.2",
"brunch": "~3.0.0",
@ -51,31 +47,45 @@
"javascript-state-machine": "~2.3.5",
"jquery": "~3.5.0",
"lodash": "~4.17.20",
"marshal": "^0.5.2",
"moment": "~2.11.2",
"mongoose": "~5.7.5",
"morgan": "~1.9.1",
"newrelic": "~5.13.1",
"perfect-scrollbar": "~0.6.10",
"react": "~0.14.6",
"react": "^16.13.1",
"react-autolink": "~0.2.1",
"react-dom": "~16.0.1",
"react-emoji": "~0.4.1",
"request": "~2.88.0",
"serve-favicon": "~2.4.5",
"snyk": "^1.316.1",
"socket.io": "~2.1.1",
"socket.io-client": "~2.1.1",
"steam": "~1.4.0",
"steam": "1.4.0",
"steamidconvert": "~0.2.4",
"toastr": "~2.1.4",
"uglify-js-brunch": ">= 2.0.1",
"winston": "~1.0.1",
"snyk": "^1.316.1"
"winston": "~1.0.1"
},
"devDependencies": {
"@babel/cli": "^7.11.6",
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4",
"@types/jquery": "^3.5.2",
"@types/mongoose": "^5.7.36",
"@types/node": "^12.12.67",
"@types/react": "^16.9.51",
"@types/react-dom": "^16.9.8",
"@types/request": "^2.48.5",
"@types/socket.io-client": "^1.4.34",
"@types/winston": "^2.4.4",
"babel-brunch": "^7.0.1",
"chai": "~3.1.0",
"mocha": "~2.2.5",
"nodemon": "~1.4.0",
"supertest": "~1.0.1"
"supertest": "~1.0.1",
"terser-brunch": "^4.0.0"
},
"engines": {
"node": "^10.13.0"

View file

@ -1,13 +0,0 @@
# Reads in raw Rails 3 session store, returns user id
require 'base64'
session_store = ARGV[0]
deserialised_store = Marshal.load(Base64.decode64(session_store))
user_id = deserialised_store['user']
STDOUT.write user_id
exit 0