Initial commit.

master
Icedream 2018-04-24 12:13:48 +02:00
commit 9ab0432a5b
Signed by: icedream
GPG Key ID: C1D30A06E6490C14
41 changed files with 20533 additions and 0 deletions

16
.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"presets": [
["babel-preset-env", {
"targets": {
"node": true,
"uglify": false
}
}]
],
"plugins": [
"babel-plugin-transform-class-properties",
"babel-plugin-transform-runtime",
"babel-plugin-transform-object-rest-spread",
"babel-plugin-dynamic-import-node"
]
}

33
.eslintignore Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
**/node_modules/**
!**/node_modules/.gitkeep
# Webpack output
packages/*/dist
packages/*/lib
# Intermediate build files (cache, etc.)
/build

30
.eslintrc.yml Normal file
View File

@ -0,0 +1,30 @@
extends: airbnb
parser: babel-eslint
plugins:
- babel
- json
env:
browser: true
node: true
es6: true
rules:
no-console: off
no-plusplus:
- error
- allowForLoopAfterthoughts: true
no-underscore-dangle: 'off'
react/no-array-index-key: 0
overrides:
- files:
- "config/**"
- "**/webpack.config*.js"
- "**/.babelrc*"
rules:
import/no-extraneous-dependencies:
- error
- devDependencies: true

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
**/node_modules/**
!**/node_modules/.gitkeep
# Webpack output
packages/*/dist
packages/*/lib
# Intermediate build files (cache, etc.)
/build

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
package-lock=false
prune=false

4
config/.eslintrc.yml Normal file
View File

@ -0,0 +1,4 @@
rules:
import/no-extraneous-dependencies:
- error
- devDependencies: true

17
config/tools/lerna.js Normal file
View File

@ -0,0 +1,17 @@
import { argv, exit } from 'process';
import { packageManager, execute } from './tool_env';
const args = [];
if (packageManager !== 'npm') {
args.push(`--npm-client=${packageManager}`);
}
args.push(...argv.splice(2));
execute('lerna', args)
.then(exit)
.catch((err) => {
console.error(err);
exit(-1);
});

38
config/tools/tool_env.js Normal file
View File

@ -0,0 +1,38 @@
import spawn from 'execa';
function detectPackageManager() {
// @TODO - use a better data source than the user-configurable user agent
const userAgent = process.env.npm_config_user_agent;
// Yarn?
if (/\byarn\/[\d.]+\b/.test(userAgent)) {
return 'yarn';
}
return 'npm';
}
export const packageManager = detectPackageManager();
/**
* Executes a given command with given argument array and then resolves the
* promise with whatever exit code the process returns.
*/
export function execute(name, args, options = {}) {
return new Promise((resolve, reject) => {
// console.log('Environment:', env);
console.log('Executing:', [name, ...args]
.map(v => (v.includes(' ') ? JSON.stringify(v) : v))
.join(' '));
const proc = spawn(name, args, {
stdio: 'inherit',
// cwd: undefined,
// env,
...options,
});
proc.on('error', reject);
proc.on('close', resolve);
});
}

View File

@ -0,0 +1,130 @@
import autoprefixer from 'autoprefixer';
export default class Environment {
constructor(options) {
this.development = true;
this.production = false;
this.server = false;
this.autoprefixerTargets = [
'chrome >= 58',
];
this.locales = ['en'];
if (options !== undefined && options !== null) {
this.input(options);
}
}
input(options) {
if (options) {
switch (true) {
case typeof (options) === 'string': // string
this.inputString(options);
break;
case Array.isArray(options): // array
options.forEach((arg) => { this.input(arg); });
break;
default: // object
Object.keys(options).forEach((k) => {
this[k] = options[k] || this[k];
});
break;
}
} else if (process.env.NODE_ENV) {
this.inputString(process.env.NODE_ENV);
}
}
inputString(env) {
switch (env.toLowerCase()) {
case 'development':
this.development = true;
this.production = false;
break;
case 'production':
this.development = false;
this.production = true;
break;
case 'server':
this.server = true;
break;
default:
console.warn('Unknown environment:', env);
break;
}
}
styleLoaders(...preprocessingLoaders) {
const {
production,
autoprefixerTargets,
server,
ExtractTextPlugin, // @HACK
} = this;
if (!ExtractTextPlugin) {
throw new Error('Need a valid ExtractTextPlugin fed into the environment object.');
}
let cssLoaders = [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: true,
localIdentName: production
? '[name]__[local]--[hash:base64:5]'
: '[name]__[local]--[hash:base64:5]',
},
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
autoprefixer({
browsers: autoprefixerTargets,
grid: false,
}),
],
sourceMap: true,
},
},
].filter(loader => loader !== false);
if (preprocessingLoaders && preprocessingLoaders.length > 0) {
cssLoaders.push(
{
loader: 'resolve-url-loader',
options: {
fail: true,
silent: false,
},
},
...preprocessingLoaders.map(loader => Object.assign({}, loader, {
options: Object.assign({}, loader.options || {}, {
sourceMap: true,
}),
})),
);
}
if (!server) {
const fallback = cssLoaders.shift();
cssLoaders = ExtractTextPlugin.extract({
fallback,
use: cssLoaders,
});
}
return cssLoaders;
}
}

7
lerna.json Normal file
View File

@ -0,0 +1,7 @@
{
"lerna": "2.1.0",
"packages": [
"packages/*"
],
"version": "0.0.1"
}

63
package.json Normal file
View File

@ -0,0 +1,63 @@
{
"name": "@eldewrito-menu/root",
"version": "0.0.1",
"private": true,
"scripts": {
"bootstrap": "yon-lerna bootstrap --hoist",
"build": "yon-lerna run build",
"clean": "yon-lerna run clean",
"lerna": "yon-lerna.js",
"lint": "eslint .",
"postinstall": "yon run -s bootstrap",
"start": "yon-lerna run start --stream --parallel",
"test:coverage": "yon-lerna run test:coverage",
"test:watch": "yon-lerna run test:watch",
"test": "yon-lerna run test"
},
"repository": {
"type": "git",
"url": "ssh://git@git.icedream.tech:2222/icedream/eldewrito-menu.git"
},
"keywords": [
"website",
"vizon",
"rizon",
"draw",
"countdown"
],
"author": "Carl Kittelberger <icedream@icedream.pw>",
"license": "MIT",
"devDependencies": {
"autoprefixer": "^7.1.4",
"babel-cli": "^6.26.0",
"babel-eslint": "^7.2.3",
"babel-plugin-dynamic-import-node": "^1.0.2",
"babel-plugin-dynamic-import-webpack": "^1.0.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"chalk": "^2.1.0",
"debug": "^3.0.1",
"eslint": "^4.6.1",
"eslint-config-airbnb": "^15.1.0",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-jsx-a11y": "^5.1.1",
"eslint-plugin-react": "^7.3.0",
"execa": "^0.8.0",
"lerna": "^2.1.2",
"nodemon": "^1.12.0",
"rimraf": "^2.6.1",
"slash": "^1.0.0",
"yon-tools": "^0.0.2"
},
"dependencies": {
"performance-now": "^2.1.0"
},
"workspaces": [
"packages/*"
]
}

View File

@ -0,0 +1,18 @@
{
"presets": [
["babel-preset-env", {
"targets": {
"node": true,
"uglify": false
}
}]
],
"plugins": [
"babel-plugin-dynamic-import-node",
"babel-plugin-syntax-export-extensions",
"babel-plugin-transform-class-properties",
"babel-plugin-transform-export-extensions",
"babel-plugin-transform-object-rest-spread",
"babel-plugin-transform-runtime"
]
}

View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules/**
!node_modules/.gitkeep
# Webpack/Babel output
/dist
/lib
# Intermediate build files (cache, etc.)
/build

View File

@ -0,0 +1,12 @@
overrides:
- files:
- src/website/**/*
rules:
import/no-extraneous-dependencies:
- error
- devDependencies: true
- files:
- src/**/*
rules:
no-console: error

2
packages/frontend/.npmrc Normal file
View File

@ -0,0 +1,2 @@
package-lock=false
prune=false

View File

@ -0,0 +1,116 @@
{
"name": "@eldewrito-menu/frontend",
"version": "0.0.1",
"private": true,
"browser": "./lib/website/index.js",
"scripts": {
"build:website:development": "yon run -s build:website -- --env development",
"build:website:production": "yon run -s build:website -- --env production",
"build:website:watch": "run-p build:website -- --watch",
"build:website": "webpack",
"build:production": "bnr build:production",
"build": "run-p build:website",
"clean": "rimraf build lib",
"lint": "eslint .",
"prepublishOnly": "yon run -s build:production",
"start": "webpack-dev-server --mode development --env development --hot --inline"
},
"betterScripts": {
"build:production": {
"command": "yon run -s build",
"env": {
"NODE_ENV": "production"
}
}
},
"repository": {
"type": "git",
"url": "ssh://git@git.icedream.tech:2222/icedream/eldewrito-menu.git"
},
"keywords": [
"website"
],
"files": [
"lib",
"README.md"
],
"author": "Carl Kittelberger <icedream@icedream.pw>",
"license": "UNLICENSED",
"devDependencies": {
"autoprefixer": "^8.3.0",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-dynamic-import-webpack": "^1.0.2",
"babel-plugin-syntax-export-extensions": "^6.13.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-export-extensions": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"better-npm-run": "^0.1.0",
"browserslist": "^3.2.4",
"case-sensitive-paths-webpack-plugin": "^2.1.2",
"chalk": "^2.4.0",
"check-types": "^7.3.0",
"css-loader": "^0.28.11",
"debug": "^3.1.0",
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-babel": "^5.1.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.7.0",
"eventsource-polyfill": "^0.9.6",
"extract-text-webpack-plugin": "4.0.0-beta.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"json-loader": "^0.5.7",
"node-fetch": "^2.1.2",
"node-sass": "^4.8.3",
"nodemon": "^1.17.3",
"normalize-scss": "^7.0.1",
"npm-run-all": "^4.1.2",
"obop": "^0.0.12",
"offline-plugin": "^4.9.0",
"performance-now": "^2.1.0",
"postcss-loader": "^2.1.4",
"progress-bar-webpack-plugin": "^1.11.0",
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.1.1",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"redux-actions": "^2.3.0",
"redux-promise": "^0.5.3",
"redux-promises": "^1.0.0",
"redux-saga": "^0.16.0",
"reselect": "^3.0.1",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2",
"sass-loader": "^7.0.1",
"shortid": "^2.2.8",
"slash": "^2.0.0",
"spinkit": "^1.2.5",
"style-loader": "^0.21.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"uglifyjs-webpack-plugin": "^1.2.5",
"url-loader": "^1.0.1",
"webfontloader": "^1.6.28",
"webpack": "^4.6.0",
"webpack-cli": "^2.0.15",
"webpack-dev-server": "^3.1.3",
"webpack-hot-middleware": "^2.22.1",
"webpack-merge": "^4.1.2",
"yon-tools": "^0.0.2"
}
}

View File

@ -0,0 +1,24 @@
{
"presets": [
["babel-preset-env", {
"targets": {
"browsers": ">1%",
"uglify": false
},
"modules": false
}],
"babel-preset-react"
],
"plugins": [
"babel-plugin-dynamic-import-node",
"babel-plugin-syntax-export-extensions",
"babel-plugin-transform-class-properties",
"babel-plugin-transform-export-extensions",
"babel-plugin-transform-react-constant-elements",
["babel-plugin-transform-runtime", {
"helpers": false,
"polyfill": false,
"regenerator": true
}]
]
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Provider } from 'react-redux';
import store from './redux';
import MessageBoxes from './containers/MessageBoxes';
const App = () => (
<Provider store={store}>
<MessageBoxes />
</Provider>
);
export default App;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { getTypeName } from '../redux/ui';
import style from './MessageBox.styl';
import MessageBoxButton from './MessageBoxButton';
const MessageBox = ({
children,
type,
progress,
buttons,
dispatchClose,
}) => (
<div className={[
style.message,
style[`message-${getTypeName(type)}`],
].join(' ')}
>
<div className={style['message-body']}>
{children}
</div>
{
buttons
? (
<div className={style.messageButtons}>
{
buttons.map(({ modifiers, close, text }, index) => (
<MessageBoxButton
modifiers={modifiers}
onClick={
close
? dispatchClose
: null
}
key={index}
>
{text}
</MessageBoxButton>
))
}
</div>
)
: null
}
</div>
);
export default MessageBox;

View File

@ -0,0 +1,7 @@
.message,
.message-error,
.message-warning,
.message-info,
.message-status,
.message-question
display: block

View File

@ -0,0 +1,23 @@
import React from 'react';
import { getTypeName } from '../redux/ui';
import style from './MessageBoxButton.styl';
const MessageBoxButton = ({
children,
modifiers = [],
onClick,
}) => (
<button
className={[
style['message-button'],
...modifiers.map(m => style[`message-button--${m}`]),
]}
onClick={onClick}
>
{children}
</button>
);
export default MessageBoxButton;

View File

@ -0,0 +1,6 @@
.message-button
display: inline-block
.message-button--primary
color: white
background: #113388

View File

@ -0,0 +1,13 @@
import React from 'react';
import MessageBox from '../containers/MessageBox';
const MessageBoxCollection = ({
messageIds,
}) => ([
...messageIds.map(id => (
<MessageBox key={id} messageId={id} />
)),
]);
export default MessageBoxCollection;

View File

@ -0,0 +1,2 @@
.messageBoxes
display: block

View File

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { selectors, actions } from '../redux/ui';
import Component from '../components/MessageBox';
const mapStateToProps = (state, { messageId }) => {
const { text, ...props } = selectors.getMessage(state, messageId);
return ({
children: text,
...props,
});
};
const mapDispatchToProps = (dispatch, { messageId }) => ({
dispatchClose: () => {
dispatch(actions.closeMessage({ id: messageId }));
},
});
const ConnectedMessageBoxes = connect(
mapStateToProps,
mapDispatchToProps,
)(Component);
export default ConnectedMessageBoxes;

View File

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { selectors } from '../redux/ui';
import Component from '../components/MessageBoxes';
const mapStateToProps = state => ({
messageIds: selectors.getMessages(state).map(m => m.id),
});
const ConnectedMessageBoxes = connect(
mapStateToProps,
)(Component);
export default ConnectedMessageBoxes;

View File

@ -0,0 +1,21 @@
$debug: false
@if ($debug)
*
background-color: rgba(255, 0, 255, 0.1)
border: red 1px solid
box-sizing: border-box
position: relative
&:after
content: attr(class)
z-index: 99999
box-shadow: 0 0 10px black
position: absolute
left: 0
top: 0
background: white
border: black 1px solid
padding: 4px
font-size: 12px
font-weight: bold
font-family: monospace

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Icedream's ElDewrito Menu</title>
</head>
<body>
<noscript>This website requires JavaScript, please enable it.</noscript>
</body>
</html>

View File

@ -0,0 +1,50 @@
import React from 'react';
import { render } from 'react-dom';
import store from './redux';
import * as ui from './redux/ui';
import * as serverlist_loader from './redux/serverlist_loader';
// import * as OfflinePluginRuntime from 'offline-plugin/runtime';
import App from './App';
import './index.sass';
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(
`%cThis is a ${process.env.NODE_ENV} build!`,
'font-family: sans-serif; color: red; font-size: 2.5em; font-weight: bold',
);
// eslint-disable-next-line no-console
console.log(
'%cThis code MUST not be deployed in production!',
'font-family: sans-serif; color: red; font-weight: bold; font-size: 1.5em',
);
}
// Offline caching runtime
// OfflinePluginRuntime.install();
// Create div node to render app in
const rootContainer = document.createElement('div');
document.body.appendChild(rootContainer);
window.addEventListener('load', () => {
render((
<App />
), rootContainer);
setTimeout(() => {
store.dispatch(serverlist_loader.actions.updateGameServers());
}, 500);
// setTimeout(() => {
// store.dispatch(ui.actions.add({
// text: 'Hello world!',
// type: ui.messageTypes.INFO,
// }));
// }, 1000);
});

View File

@ -0,0 +1,3 @@
@import ~normalize-scss/sass/normalize
@import debug

View File

@ -0,0 +1,202 @@
import { combineReducers } from 'redux';
const REDUX_PATH_SEPARATOR = '/';
export default class ReduxGenerator {
constructor(...id) {
this._id = id;
}
sub(name) {
return new ReduxGenerator(...this._id, name);
}
actionType(name) {
return [...this._id, name]
.join(REDUX_PATH_SEPARATOR);
}
actionObject(name, payload) {
return {
type: this.actionType(name),
payload,
};
}
selector(selector) {
return (state, ...params) => {
const selectedState = this._id
.slice(1) // ignore root id
.reduce((current, id) => current[id], state);
return selector(selectedState, ...params);
};
}
all({
actions = {},
selectors = {},
reducers = {},
initialState = null,
}) {
/*
Descriptor = {
someName: {
params: {
param1: (v) => typeof(value) === "string",
},
reducer(currentState, payload) {
// ... do stuff
return {
...currentState,
...newStateDiff,
};
},
},
// ...
}
*/
const retval = Object.entries(actions)
.reduce((current, [
name,
{
params,
reducer,
},
]) => {
const id = this.actionType(name);
const target = {
...current,
actions: {
...current.actions,
[name]: (payload = {}) => {
// Validate all payload values
const err = Object.entries(payload)
.map(([key, value]) => {
const validator = params[key];
if (!validator) {
console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`);
return undefined;
}
let result = validator(value);
console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result);
switch (typeof result) {
case 'boolean':
if (!result) {
result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`);
} else {
result = null;
}
break;
default:
if (result instanceof Error) {
result.message = `${JSON.stringify(key)}: ${result.message}`;
} else {
result = new Error(`${JSON.stringify(key)}: ${result}`);
}
}
Object.freeze(result);
return result;
})
.find(v => !!v);
if (err) {
throw err;
}
// Create action object
return {
type: id,
payload,
};
},
},
actionTypes: {
...current.actionTypes,
[name]: id,
},
};
if (reducer) {
target.reducers = {
...current.reducers,
[name]: (state, { type, payload }) => {
// Ignore non-matching actions
if (type !== id) {
console.debug(`${type}: skipping action-mismatching reducer`, { state, payload });
return state;
}
// Call original reducer with our payload
console.debug(`${type}: action-matched reducer called`, { state, payload });
const result = reducer(state, payload);
console.debug(' =>', result);
return result;
},
};
}
return target;
}, {
actions: {},
actionTypes: {},
reducers: Object.entries(reducers)
.reduce((current, [name, reducer]) => ({
[name]: (state, ...args) => {
console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args });
const result = reducer(state, ...args);
console.debug(' =>', result);
return result;
},
}), {}),
selectors: {},
});
retval.selectors = Object.entries(selectors)
.reduce((current, [name, selector]) => ({
...current,
[name]: (state, ...args) => {
const namespaceState = this._id
.slice(1) // ignore root id
.reduce(
(currentState, namespaceName) =>
currentState[namespaceName], state);
console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args });
const result = selector(namespaceState, ...args);
console.debug(' =>', result);
return result;
},
}), selectors);
retval.reducer = (state, ...args) => {
console.debug(`${this._id.join('/')}: combined reducer called`, { state, args });
if (state === undefined) {
console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args });
const result = initialState;
console.debug(' =>', result);
return result;
}
// not using combineReducers here as we want all reducers to act on the same namespace
if (retval.reducers) {
return Object.values(retval.reducers)
.filter(value => typeof (value) === 'function')
.reduce(
(current, reducer) => reducer(current, ...args),
state,
);
}
console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`);
return state;
};
// if (initialState) {
// retval.initialState = initialState;
// }
console.debug('generated redux module:', retval);
return retval;
}
}

View File

@ -0,0 +1,33 @@
import check from 'check-types';
function ipv4(v) {
if (!check.string(v)) {
return new Error('not a string');
}
const segments = v.split('.');
if (segments.length !== 4) {
return new Error('not a valid IPv4');
}
return segments
.map(segmentValue => parseInt(segmentValue, 10))
.every(segmentValue =>
check.integer(segmentValue) && check.inRange(segmentValue, 0x00, 0xff));
}
function port(v) {
return check.integer(v) &&
check.inRange(v, 0x0001, 0xffff);
}
function positiveInteger(v) {
return check.integer(v) && check.positive(v);
}
export default {
...check,
ipv4,
port,
positiveInteger,
};

View File

@ -0,0 +1,81 @@
import {
createStore,
applyMiddleware,
combineReducers,
compose,
} from 'redux';
import createSagaMiddleware from 'redux-saga';
import {
all,
fork,
} from 'redux-saga/effects';
import * as serverlist from './serverlist';
import * as serverlistLoader from './serverlist_loader';
import * as ui from './ui';
import * as mastersync from './mastersync';
const reduxModules = Object.entries({
serverlist,
serverlistLoader,
ui,
mastersync,
});
console.debug('all redux modules', reduxModules);
const reducers =
reduxModules
.reduce((current, [name, m]) => {
if (!m.reducer) { return current; }
return {
...current,
[name]: m.reducer,
};
}, {});
console.debug('combined reducers:', reducers);
const reducer = combineReducers(reducers);
// const initialState =
// reduxModules
// .reduce((current, [name, m]) => {
// if (!m.initialState) { return current; }
// return {
// ...current,
// [name]: m.initialState,
// };
// }, {});
// console.debug('combined initial state:', initialState);
// create the saga middleware
function* rootSaga() {
const sagas = Object.values(reduxModules)
.reduce((current, [name, m]) => {
if (!m.sagas) {
console.debug(`Module ${name} does not have any sagas`);
return current;
}
return [
...current,
...m.sagas,
];
}, []);
console.debug('Will run these sagas from root:', sagas);
yield all(sagas);
console.debug('Root saga done');
}
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default createStore(
reducer,
// initialState,
composeEnhancers(
applyMiddleware(
sagaMiddleware,
),
),
);
sagaMiddleware.run(rootSaga);

View File

@ -0,0 +1,139 @@
import {
takeEvery,
put,
call,
} from 'redux-saga/effects';
import fetch from 'node-fetch';
import * as ui from '../ui';
import check from '../check';
import root from '../root';
let myRedux;
export const states = {
IDLE: 0,
LOADING: 1,
};
function* doDewritoJsonUpdateRequest() {
console.debug('mastersync doDewritoJsonUpdateRequest saga called');
try {
const response = yield call(fetch, 'https://raw.githubusercontent.com/ElDewrito/ElDorito/master/dist/mods/dewrito.json');
if (response.status !== 200) {
throw new Error(`unexpected HTTP error: ${response.status} ${response.statusText}`);
}
const dewritoJson = yield call(response.json.bind(response));
yield put(myRedux.actions.updateDewritoJsonFinished({
dewritoJson,
}));
} catch (err) {
yield put(myRedux.actions.updateDewritoJsonFinished({
err,
}));
}
console.debug('mastersync doDewritoJsonUpdateRequest: done');
}
function* storeMasters({
payload: {
err,
dewritoJson,
},
}) {
console.debug('mastersync storeMasters saga called');
if (err) {
yield put(ui.actions.add({
text: `Failed to fetch master server information: ${err.message}`,
type: ui.messageTypes.ERROR,
}));
return;
}
const {
masterServers,
} = dewritoJson;
yield put(myRedux.actions.setMasters({ masters: masterServers }));
console.debug('mastersync storeMasters: done');
}
export const initialState = {
state: states.IDLE,
masters: [],
caches: [],
};
myRedux = root.sub('mastersync').all({
initialState,
actions: {
updateDewritoJson: {},
updateDewritoJsonStarted: {
reducer: state => ({
...state,
state: states.LOADING,
}),
},
updateDewritoJsonFinished: {
params: {
err: v => v == null || v instanceof Error,
dewritoJson: v => v === undefined ||
check.like(v, {
masterServers: [
{
list: '',
announce: '',
stats: '',
},
],
}),
},
reducer: state => ({
...state,
state: states.IDLE,
}),
},
setMasters: {
params: {
masters: v => check.array(v),
},
reducer: (state, { masters }) => ({
...state,
masters,
}),
},
setCaches: {
params: {
masters: v => check.array(v),
},
reducer: (state, { caches }) => ({
...state,
caches,
}),
},
},
// selectors are all function(state, ...parameters)
selectors: {
hasMasters: ({ masters }) => masters.length > 0,
getMasters: ({ masters }) => ([...masters]),
hasCaches: ({ caches }) => caches.length > 0,
getCaches: ({ caches }) => ([...caches]),
},
});
export const sagas = [
takeEvery(myRedux.actionTypes.updateDewritoJson, doDewritoJsonUpdateRequest),
takeEvery(myRedux.actionTypes.updateDewritoJsonFinished, storeMasters),
];
export const {
actions,
actionTypes,
reducer,
selectors,
} = myRedux;

View File

@ -0,0 +1,3 @@
import ReduxBase from './base';
export default new ReduxBase('@eldewrito-menu');

View File

@ -0,0 +1,111 @@
import obop from 'obop';
import check from '../check';
import root from '../root';
const reduxGenerator = root.sub('serverlist');
export const initialState = {
servers: [],
};
const myRedux = reduxGenerator.all({
initialState,
actions: {
add: {
params: {
ip: check.ipv4, // server ip
ping: check.number, // server ping
name: check.string, // server name
port: check.port, // server port
hostPlayer: check.string, // hosting player
sprintEnabled: v => check.includes(['0', '1'], v),
sprintUnlimitedEnabled: v => check.includes(['0', '1'], v),
dualWielding: v => check.includes(['0', '1'], v),
assassinationEnabled: v => check.includes(['0', '1'], v),
votingEnabled: check.boolean,
teams: check.boolean,
map: check.string,
mapFile: check.string,
variant: check.maybe.string,
variantType: check.maybe.string,
teamScores: check.maybe.array.of.number,
status: check.string,
numPlayers: check.positiveInteger,
mods: check.array, // TODO - fine tune this
maxPlayers: check.positiveInteger,
passworded: check.boolean,
isDedicated: check.boolean,
gameVersion: check.string,
eldewritoVersion: check.maybe.string,
xnkid: check.maybe.string,
xnaddr: check.maybe.string,
players: check.maybe.array.of.like({
name: '',
serviceTag: '',
team: 0,
uid: '',
primaryColor: '#000000',
isAlive: true,
score: 0,
kills: 0,
assists: 0,
deaths: 0,
betrayals: 0,
timeSpentAlive: 0,
suicides: 0,
bestStreak: 0,
}),
},
reducer(current, payload) {
// do we have this server on the list already?
if (current.servers.find(server => server.ip === payload.ip)) {
// skip this server
return current;
}
// add server
return {
current,
servers: [
...current.servers,
payload,
],
};
},
},
reset: {
reducer: () => initialState,
},
},
// selectors are all function(state, ...parameters)
selectors: {
/*
Example:
filter(state, {
name: {
$in: ["substring"],
}
gameType: "slayer",
})
*/
filterServers({ servers }, filter, { order } = {}) {
let filtered = obop.where(servers, filter);
if (order) {
filtered = obop.order(filtered, order);
}
return filtered;
},
},
});
export const {
actions,
actionTypes,
reducer,
selectors,
} = myRedux;

View File

@ -0,0 +1,156 @@
import {
takeEvery,
take,
call,
select,
all,
put,
} from 'redux-saga/effects';
import fetch from 'node-fetch';
import check from '../check';
import root from '../root';
import * as serverlist from '../serverlist';
import * as mastersync from '../mastersync';
let myRedux;
export const states = {
IDLE: 0,
LOADING: 1,
};
export const initialState = {
state: states.IDLE,
};
function* inspectGameServer(addr) {
check.assert.string(addr);
const addrSplit = addr.split(':');
const ip = addrSplit[0];
check.assert(check.ipv4(ip));
const port = parseInt(addrSplit[1], 10);
check.assert(check.port(port));
try {
const response = yield call(fetch, `http://${ip}:${port}`);
if (response.status !== 200) {
throw new Error(`Unexpected HTTP error code ${response.status}: ${response.statusText}`);
}
const serverInfo = yield call(response.json.bind(response));
check.assert.string(serverInfo.name);
check.assert(check.port(serverInfo.port));
check.assert.maybe.string(serverInfo.hostPlayer);
check.assert.maybe.string(serverInfo.map);
check.assert.maybe.string(serverInfo.mapFile);
check.assert.maybe.string(serverInfo.variant);
check.assert.maybe.string(serverInfo.variantType);
check.assert.integer(serverInfo.maxPlayers);
check.assert.integer(serverInfo.numPlayers);
check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers);
check.assert.array(serverInfo.players);
yield put(serverlist.actions.add({
ip,
port,
...serverInfo,
}));
} catch (e) {
console.error(`Request to game server ${addr} failed:`, e);
}
}
function* updateGameServersFromMaster(masterListUrl) {
check.assert.string(masterListUrl);
try {
const response = yield call(fetch, masterListUrl);
if (response.status !== 200) {
throw new Error(`Server returned HTTP error code ${response.status}: ${response.statusText}`);
}
const {
listVersion,
result: {
code,
msg,
servers,
},
} = yield call(response.json.bind(response));
switch (listVersion) {
case 1:
if (code !== 0) {
throw new Error(`Server returned error code ${code}: ${msg}`);
}
yield all(servers.map(addr => call(inspectGameServer, addr)));
break;
default:
throw new Error(`Unsupported list version: ${listVersion}`);
}
} catch (e) {
console.error(`Update from ${masterListUrl} failed:`, e);
}
}
function* updateGameServers() {
yield put(mastersync.actions.updateDewritoJsonStarted());
// First let's ensure we have master servers at all
if (!(yield select(mastersync.selectors.hasMasters))) {
// Fetch master servers and hope for success
yield put(mastersync.actions.updateDewritoJson());
const { err } = yield take(mastersync.actionTypes.updateDewritoJsonFinished);
if (err) {
yield put(mastersync.actions.updateDewritoJsonFinished({ err }));
return;
}
}
// Now we have master servers, let's check all game servers!
yield put(serverlist.actions.reset());
const masterListUrls = (yield select(mastersync.selectors.getMasters))
.map(server => server.list);
yield all(masterListUrls
.map(url => call(updateGameServersFromMaster, url)));
yield put(mastersync.actions.updateDewritoJsonFinished({}));
}
myRedux = root.sub('serverlist_loader').all({
initialState,
actions: {
updateGameServers: {},
updateGameServersStarted: {
reducer: state => ({
...state,
state: states.LOADING,
}),
},
updateGameServersFinished: {
reducer: state => ({
...state,
state: states.IDLE,
}),
},
},
// selectors are all function(state, ...parameters)
selectors: {
getState: ({ state }) => state,
},
});
export const sagas = [
takeEvery(myRedux.actionTypes.updateGameServers, updateGameServers),
];
export const {
actions,
actionTypes,
reducer,
selectors,
} = myRedux;

View File

@ -0,0 +1,131 @@
import shortid from 'shortid';
import check from '../check';
import root from '../root';
export const messageTypes = {
STATUS: 0,
INFO: 1,
WARNING: 2,
ERROR: 3,
QUESTION: 4,
};
export function getTypeName(type) {
const matchingTypeItem = Object.entries(messageTypes)
.find(item => item[1] === type);
if (!matchingTypeItem) {
return undefined;
}
return matchingTypeItem[0];
}
export const initialState = {
messages: [],
};
const myRedux = root.sub('ui').all({
initialState,
actions: {
add: {
params: {
id: v => v === undefined || check.string(v),
text: v => check.string(v),
type: v => v in Object.values(messageTypes),
buttons: v => v === undefined || check.array(v),
},
reducer: (state, {
id,
buttons,
...payload
}) => {
let finalId = id;
// generate new unique id if none given
if (finalId === undefined) {
const hasMatchingId = m => m.id === finalId;
do {
finalId = shortid.generate();
} while (state.messages.find(hasMatchingId));
}
// no buttons => default to "OK" button
let finalButtons = buttons;
if (!finalButtons) {
finalButtons = [
{
text: 'OK', // TODO - localization
close: true,
},
];
}
return ({
...state,
messages: [
...state.messages,
{
...payload,
id: finalId,
buttons: finalButtons,
},
],
});
},
},
update: {
params: {
id: v => check.string(v),
text: v => v === undefined || check.string(v),
progress: v => v === undefined || check.inRange(v, 0, 1),
},
reducer: (state, { id, ...payload }) => ({
...state,
messages: state.messages.map(m =>
(m.id === id
? { ...m, ...payload }
: m),
),
}),
},
closeMessage: {
params: {
id: v => check.string(v),
},
reducer: (state, { id }) => ({
...state,
messages: state.messages.filter(m => m.id !== id),
}),
},
reset: {
reducer: () => initialState,
},
},
selectors: {
generateMessageId: ({ messages }) => {
let id;
const anyMessageWithId = m => m.id === id;
do {
id = shortid.generate();
} while (messages.find(anyMessageWithId));
return id;
},
getMessages: ({ messages }) => ([...messages]),
getMessage: ({ messages }, id) => messages.find(m => m.id === id),
},
});
export const {
actions,
actionTypes,
reducer,
selectors,
} = myRedux;

View File

@ -0,0 +1,322 @@
import {
DefinePlugin,
NamedModulesPlugin,
HashedModuleIdsPlugin,
LoaderOptionsPlugin,
HotModuleReplacementPlugin,
NoEmitOnErrorsPlugin,
optimize,
} from 'webpack';
import path from 'path';
import { isatty } from 'tty';
import chalk from 'chalk';
import _debug from 'debug';
// import slash from 'slash';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import HtmlPlugin from 'html-webpack-plugin';
import ProgressBarPlugin from 'progress-bar-webpack-plugin';
import UglifyJsPlugin from 'uglifyjs-webpack-plugin';
import Environment from '../../config/webpack/environment';
const debug = _debug('webpack:config');
debug.generated = _debug('webpack:config:generated');
debug.generated('filename:', __filename);
debug.generated('dirname:', __dirname);
const {
ModuleConcatenationPlugin,
} = optimize;
const websiteSubfolder = 'website';
export default (options) => {
const environment = new Environment({
ExtractTextPlugin, // @HACK
});
environment.input(options);
debug.generated(environment);
const {
development,
production,
server,
autoprefixerTargets,
} = environment;
const baseOutputFilename = development
? 'assets/dev/[name].dev.[ext]'
// Always use a hash (in production) to prevent files with the same name causing issues
: 'assets/prod/[chunkhash:2]/[name].[chunkhash:8].[ext]';
const webpackChunkFilename = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'js');
const webpackOutputFilename = webpackChunkFilename;
const assetOutputFilename = baseOutputFilename
.replace(/\[chunkhash(.*?)\]/g, '[hash$1]');
const cssOutputFileName = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'css')
.replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]');
// const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`;
const cssOutputRebasePath = '/';
// Default options for url-loader
const urlLoaderOptions = {
limit: 1, // Don't inline anything (but empty files) by default
name: assetOutputFilename,
publicPath: cssOutputRebasePath,
};
const config = {
name: 'frontend',
target: 'web',
devServer: {
// inline: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
// historyApiFallback: {
// index: '/',
// },
hot: true,
noInfo: true,
overlay: true,
publicPath: '',
quiet: false,
watchOptions: {
ignored: /node_modules/,
},
},
module: {
rules: [
{
test: /\.jsx?/i,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
// Look for babel configuration in project directory
babelrc: false,
// Cache transformations to the filesystem (in default temp dir)
cacheDirectory: true,
presets: [
['babel-preset-env', {
targets: {
browsers: autoprefixerTargets,
uglify: false,
},
// spec: true,
// debug: development,
modules: false, // do not transpile modules, webpack 2+ does that
}],
'babel-preset-react',
],
plugins: [
'babel-plugin-transform-react-constant-elements',
'babel-plugin-transform-class-properties',
'babel-plugin-transform-object-rest-spread',
['babel-plugin-transform-runtime', {
helpers: false,
polyfill: false,
regenerator: true,
}],
'babel-plugin-dynamic-import-webpack',
],
},
},
{
test: /\.json?/i,
loader: 'json-loader',
exclude: /node_modules/,
},
...[
/\.(gif|png|webp)$/i, // graphics
/\.svg$/i, // svg
/\.jpe?g$/i, // jpeg
/\.(mp4|ogg|webm)$/i, // video
/\.(eot|otf|ttf|woff|woff2)$/i, // fonts
/\.(wav|mp3|m4a|aac|oga)$/i, // audio
].map(test => ({
test,
loader: 'url-loader',
options: urlLoaderOptions,
})),
{
test: /\.css$/,
use: environment.styleLoaders(),
},
{
test: /\.s[ac]ss$/,
use: environment.styleLoaders(
{
loader: 'sass-loader',
},
),
},
{
test: /\.styl$/,
use: environment.styleLoaders(
{
loader: 'stylus-loader',
},
),
},
],
strictExportPresence: true,
},
output: {
filename: webpackOutputFilename,
chunkFilename: webpackChunkFilename,
path: path.join(__dirname, 'lib', websiteSubfolder),
publicPath: '',
},
plugins: [
// Show progress as a bar during build
isatty(1) && new ProgressBarPlugin({
complete: chalk.white('\u2588'),
incomplete: chalk.grey('\u2591'),
format: `:bar ${chalk.cyan.bold(':percent')} Webpack build: ${chalk.grey(':msg')}`,
}),
// Enforce case-sensitive import paths
new CaseSensitivePathsPlugin(),
// Replace specified expressions with values
new DefinePlugin({
'process.env.__DEV__': JSON.stringify(development),
'process.env.__PROD__': JSON.stringify(production),
'process.env.__SERVER__': JSON.stringify(server),
'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'),
}),
// Dev server build
...[
// Hot module reloading
new HotModuleReplacementPlugin(),
new NoEmitOnErrorsPlugin(),
// Use paths as names when serving
new NamedModulesPlugin(),
].filter(() => server),
// If we're not serving, we're creating a static build
...[// Extract imported stylesheets out into .css files
new ExtractTextPlugin({
allChunks: true,
filename: cssOutputFileName,
}),
].filter(() => !server),
// Move modules imported from node_modules/ into a vendor chunk when enabled
// FIXME - this plugin has been removed, use new option from Webpack 4
// new CommonsChunkPlugin({
// name: 'vendor',
// minChunks(module) {
// return (module.resource && module.resource.includes('node_modules'));
// },
// }),
// If we're generating an HTML file, we must be building a web app, so
// configure deterministic hashing for long-term caching.
// Generate stable module ids instead of having Webpack assign integers.
// NamedModulesPlugin allows for easier debugging and
// HashedModuleIdsPlugin does this without adding too much to bundle
// size.
development
? new NamedModulesPlugin()
: new HashedModuleIdsPlugin(),
// Production builds
...[
// JavaScript minification
new LoaderOptionsPlugin({ debug: false, minimize: true }),
// Hoisting
new ModuleConcatenationPlugin(),
].filter(() => production),
new HtmlPlugin({
template: path.join(__dirname, 'src', websiteSubfolder, 'index.html'),
filename: 'index.html',
hash: false,
inject: true,
compile: true,
favicon: false,
minify: production ? {
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
// includeAutoGeneratedTags: false,
collapseWhitespace: true,
// conservativeCollapse: true,
} : false,
cache: true,
showErrors: true,
chunks: 'all',
excludeChunks: [],
title: 'VIzon Countdown',
xhtml: false,
chunksSortMode: 'dependency',
}),
].filter(plugin => plugin !== false),
resolve: {
extensions: [
'.js', '.json', '.jsx',
],
alias: {
'moment-timezone': 'moment-timezone/builds/moment-timezone-with-data-2012-2022.js',
},
},
resolveLoader: {
modules: ['node_modules', 'nwb'],
},
optimization: {
minimize: production,
minimizer: [
new UglifyJsPlugin({
parallel: true,
uglifyOptions: {
compress: {
warnings: false,
},
output: {
comments: false,
},
},
sourceMap: true,
}),
],
},
devtool: server ? 'cheap-module-source-map' : 'source-map',
entry: {
app: [
...[
'eventsource-polyfill',
'react-hot-loader/patch',
'webpack-hot-middleware/client',
].filter(() => server).map(require.resolve),
path.join(__dirname, 'src', websiteSubfolder),
],
},
};
debug.generated(config);
return config;
};

9369
packages/frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

9167
yarn.lock Normal file

File diff suppressed because it is too large Load Diff