Merge branch 'account' into develop
This commit is contained in:
commit
ee60167d1e
14
WORKLOG.md
14
WORKLOG.md
@ -1,6 +1,15 @@
|
||||
# WORK WORK
|
||||
## NOW
|
||||
*PRODUCTION*
|
||||
* ACP
|
||||
* essential
|
||||
* error log
|
||||
* account lookup w/ pw reset
|
||||
|
||||
* nice to have
|
||||
|
||||
* bot game grind
|
||||
|
||||
* serde serialize privatise
|
||||
* stripe prod
|
||||
* mobile styles
|
||||
@ -10,6 +19,11 @@
|
||||
* graphs n shit
|
||||
* acp init
|
||||
* DO postgres
|
||||
* make our own toasts / msg pane
|
||||
* send account_instances on players update
|
||||
|
||||
* only clear effects on post_resolve
|
||||
electrify doesn't work if you ko the construct
|
||||
|
||||
## SOON
|
||||
*SERVER*
|
||||
|
||||
9
acp/.babelrc
Normal file
9
acp/.babelrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
["transform-react-jsx", { "pragma":"preact.h" }]
|
||||
]
|
||||
}
|
||||
1501
acp/.eslintrc.js
Normal file
1501
acp/.eslintrc.js
Normal file
File diff suppressed because it is too large
Load Diff
5
acp/.gitignore
vendored
Normal file
5
acp/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package-lock.json
|
||||
node_modules/
|
||||
dist/
|
||||
.cache/
|
||||
assets/molecules
|
||||
19
acp/acp.html
Normal file
19
acp/acp.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>mnml - acp</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name=apple-mobile-web-app-capable content=yes>
|
||||
<meta name=apple-mobile-web-app-status-bar-style content=black>
|
||||
<meta name="description" content="mnml pvp tbs">
|
||||
<meta name="author" content="ntr@smokestack.io">
|
||||
<link rel="stylesheet" href="./node_modules/izitoast/dist/css/iziToast.min.css"></script>
|
||||
<link href="https://fonts.googleapis.com/css?family=Jura" rel="stylesheet">
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>js is required to run mnml</noscript>
|
||||
</body>
|
||||
<script src="./acp.js"></script>
|
||||
</html>
|
||||
18
acp/acp.js
Normal file
18
acp/acp.js
Normal file
@ -0,0 +1,18 @@
|
||||
require('./../client/assets/styles/normalize.css');
|
||||
require('./../client/assets/styles/skeleton.css');
|
||||
|
||||
require('./../client/assets/styles/styles.less');
|
||||
require('./../client/assets/styles/menu.less');
|
||||
require('./../client/assets/styles/acp.less');
|
||||
require('./../client/assets/styles/nav.less');
|
||||
require('./../client/assets/styles/footer.less');
|
||||
require('./../client/assets/styles/account.less');
|
||||
require('./../client/assets/styles/controls.less');
|
||||
require('./../client/assets/styles/instance.less');
|
||||
require('./../client/assets/styles/vbox.less');
|
||||
require('./../client/assets/styles/game.less');
|
||||
require('./../client/assets/styles/styles.mobile.css');
|
||||
require('./../client/assets/styles/instance.mobile.css');
|
||||
|
||||
// kick it off
|
||||
require('./src/acp');
|
||||
49
acp/package.json
Normal file
49
acp/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "mnml-client",
|
||||
"version": "0.2.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "parcel watch acp.html --no-hmr --out-dir /var/lib/mnml/public/current",
|
||||
"build": "parcel build acp.html",
|
||||
"lint": "eslint --fix --ext .jsx src/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"anime": "^0.1.2",
|
||||
"animejs": "^3.0.1",
|
||||
"async": "^2.6.2",
|
||||
"borc": "^2.0.3",
|
||||
"docco": "^0.7.0",
|
||||
"izitoast": "^1.4.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkstate": "^1.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"node-sass": "^4.12.0",
|
||||
"parcel": "^1.12.3",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.19.0",
|
||||
"preact-context": "^1.1.3",
|
||||
"preact-redux": "^2.1.0",
|
||||
"react-stripe-elements": "^3.0.0",
|
||||
"redux": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-plugin-module-resolver": "^3.2.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"jest": "^18.0.0",
|
||||
"less": "^3.9.0"
|
||||
},
|
||||
"alias": {
|
||||
"react": "preact-compat",
|
||||
"react-dom": "preact-compat"
|
||||
}
|
||||
}
|
||||
36
acp/src/acp.game.list.jsx
Normal file
36
acp/src/acp.game.list.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
const preact = require('preact');
|
||||
const { Component } = require('preact');
|
||||
const { connect } = require('preact-redux');
|
||||
const linkState = require('linkstate').default;
|
||||
|
||||
const actions = require('./actions');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
games,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
games
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
function AcpGameList(args) {
|
||||
const {
|
||||
games,
|
||||
} = args;
|
||||
|
||||
if (!games) return false;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
{games.map((g, i) => <tr key={i}><td>{JSON.stringify(g)}</td></tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = addState(AcpGameList);
|
||||
34
acp/src/acp.jsx
Normal file
34
acp/src/acp.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
const preact = require('preact');
|
||||
// const logger = require('redux-diff-logger');
|
||||
|
||||
const { Provider, connect } = require('preact-redux');
|
||||
const { createStore, combineReducers } = require('redux');
|
||||
|
||||
const reducers = require('./reducers');
|
||||
const actions = require('./actions');
|
||||
|
||||
const Main = require('./acp.main');
|
||||
|
||||
// Redux Store
|
||||
const store = createStore(
|
||||
combineReducers(reducers),
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
|
||||
);
|
||||
|
||||
document.fonts.load('16pt "Jura"').then(() => {
|
||||
const Acp = () => (
|
||||
<Provider store={store}>
|
||||
<div id="mnml" class="acp">
|
||||
<nav>
|
||||
<h1>acp</h1>
|
||||
<hr/>
|
||||
</nav>
|
||||
<Main />
|
||||
<aside></aside>
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
preact.render(<Acp />, document.body);
|
||||
});
|
||||
173
acp/src/acp.main.jsx
Normal file
173
acp/src/acp.main.jsx
Normal file
@ -0,0 +1,173 @@
|
||||
const preact = require('preact');
|
||||
const { Component } = require('preact');
|
||||
const { connect } = require('preact-redux');
|
||||
const linkState = require('linkstate').default;
|
||||
|
||||
const actions = require('./actions');
|
||||
const { postData, errorToast } = require('./../../client/src/utils');
|
||||
|
||||
const AcpGameList = require('./acp.game.list');
|
||||
const AcpUser = require('./acp.user');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
account,
|
||||
user,
|
||||
msg,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
account,
|
||||
user,
|
||||
msg,
|
||||
};
|
||||
},
|
||||
|
||||
function receiveDispatch(dispatch) {
|
||||
function setUser(user) {
|
||||
dispatch(actions.setUser(user));
|
||||
}
|
||||
|
||||
function setGames(list) {
|
||||
dispatch(actions.setGames(list));
|
||||
}
|
||||
|
||||
function setMsg(msg) {
|
||||
dispatch(actions.setMsg(msg));
|
||||
}
|
||||
|
||||
return {
|
||||
setUser,
|
||||
setMsg,
|
||||
setGames,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
class AcpMain extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
id: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
|
||||
render(args, state) {
|
||||
const {
|
||||
setGames,
|
||||
setUser,
|
||||
setMsg,
|
||||
|
||||
msg,
|
||||
} = args;
|
||||
|
||||
const {
|
||||
name,
|
||||
id,
|
||||
} = state;
|
||||
|
||||
const reset = () => {
|
||||
setMsg(null);
|
||||
this.setState({ id: null, name: null });
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
reset();
|
||||
postData('/acp/user', { id, name })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) return setMsg(data.error);
|
||||
setUser(data);
|
||||
})
|
||||
.catch(error => errorToast(error));
|
||||
};
|
||||
|
||||
const gameList = () => {
|
||||
reset();
|
||||
postData('/acp/game/list', { number: 20 })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) return setMsg(data.error);
|
||||
console.log(data);
|
||||
setGames(data.data);
|
||||
})
|
||||
.catch(error => errorToast(error));
|
||||
};
|
||||
|
||||
const gameOpen = () => {
|
||||
reset();
|
||||
postData('/acp/game/open')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) return setMsg(data.error);
|
||||
console.log(data);
|
||||
setGames(data);
|
||||
})
|
||||
.catch(error => errorToast(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<main class='menu'>
|
||||
<div class="top">
|
||||
<div class="msg">{msg}</div>
|
||||
<AcpUser />
|
||||
<AcpGameList />
|
||||
</div>
|
||||
<div class="bottom acp list">
|
||||
<div>
|
||||
<label for="current">Username:</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="text"
|
||||
name="name"
|
||||
value={this.state.name}
|
||||
onInput={linkState(this, 'name')}
|
||||
placeholder="name"
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="text"
|
||||
name="userid"
|
||||
value={this.state.id}
|
||||
onInput={linkState(this, 'id')}
|
||||
placeholder="id"
|
||||
/>
|
||||
<button
|
||||
onClick={getUser}>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="current">Game:</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="text"
|
||||
name="userid"
|
||||
value={this.state.id}
|
||||
onInput={linkState(this, 'id')}
|
||||
placeholder="id"
|
||||
/>
|
||||
<button
|
||||
onClick={getUser}>
|
||||
Search
|
||||
</button>
|
||||
<button
|
||||
onClick={gameList}>
|
||||
Last 20
|
||||
</button>
|
||||
<button
|
||||
onClick={gameOpen}>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = addState(AcpMain);
|
||||
97
acp/src/acp.user.jsx
Normal file
97
acp/src/acp.user.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
const preact = require('preact');
|
||||
const { Component } = require('preact');
|
||||
const { connect } = require('preact-redux');
|
||||
const linkState = require('linkstate').default;
|
||||
|
||||
const { postData, errorToast } = require('./../../client/src/utils');
|
||||
const actions = require('./actions');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
user,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
user,
|
||||
};
|
||||
},
|
||||
function receiveDispatch(dispatch) {
|
||||
function setUser(user) {
|
||||
dispatch(actions.setUser(user));
|
||||
}
|
||||
|
||||
function setMsg(msg) {
|
||||
dispatch(actions.setMsg(msg));
|
||||
}
|
||||
|
||||
return {
|
||||
setUser,
|
||||
setMsg,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function AcpUser(args) {
|
||||
const {
|
||||
user,
|
||||
setUser,
|
||||
setMsg,
|
||||
} = args;
|
||||
|
||||
const {
|
||||
credits,
|
||||
} = this.state;
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
const reset = () => {
|
||||
setMsg(null);
|
||||
this.setState({ credits: null });
|
||||
};
|
||||
|
||||
const addCredits = () => {
|
||||
reset();
|
||||
postData('/acp/user/credits', { id: user.id, credits })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) return setMsg(data.error);
|
||||
return setUser(data);
|
||||
})
|
||||
.catch(error => setMsg(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="user">
|
||||
<dl>
|
||||
<h1>{user.name}</h1>
|
||||
<dt>Id</dt>
|
||||
<dd>{user.id}</dd>
|
||||
<dt>Credits</dt>
|
||||
<dd>{user.balance}</dd>
|
||||
<dt>Subscribed</dt>
|
||||
<dd>{user.subscribed.toString()}</dd>
|
||||
</dl>
|
||||
<div>
|
||||
<label for="current">Add Credits:</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="number"
|
||||
name="credits"
|
||||
value={credits}
|
||||
onInput={linkState(this, 'credits')}
|
||||
placeholder="credits"
|
||||
/>
|
||||
<button
|
||||
onClick={addCredits}>
|
||||
Add Credits
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Constructs</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = addState(AcpUser);
|
||||
4
acp/src/actions.jsx
Normal file
4
acp/src/actions.jsx
Normal file
@ -0,0 +1,4 @@
|
||||
export const setAccount = value => ({ type: 'SET_ACCOUNT', value });
|
||||
export const setUser = value => ({ type: 'SET_USER', value });
|
||||
export const setMsg = value => ({ type: 'SET_MSG', value });
|
||||
export const setGames = value => ({ type: 'SET_GAMES', value });
|
||||
18
acp/src/reducers.jsx
Normal file
18
acp/src/reducers.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
function createReducer(defaultState, actionType) {
|
||||
return function reducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case actionType:
|
||||
return action.value;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable key-spacing */
|
||||
module.exports = {
|
||||
account: createReducer(null, 'SET_ACCOUNT'),
|
||||
user: createReducer(null, 'SET_USER'),
|
||||
msg: createReducer(null, 'SET_MSG'),
|
||||
games: createReducer([], 'SET_GAMES'),
|
||||
};
|
||||
@ -12,3 +12,9 @@ cd $MNML_PATH/client
|
||||
rm -rf dist
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
echo "Building acp version $VERSION"
|
||||
cd $MNML_PATH/acp
|
||||
rm -rf dist
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
@ -6,8 +6,12 @@ MNML_PATH=$(realpath "$DIR/../")
|
||||
VERSION=$(<"$MNML_PATH/VERSION")
|
||||
|
||||
SERVER_BIN_DIR="/usr/local/mnml/bin"
|
||||
|
||||
CLIENT_DIST_DIR="/var/lib/mnml/client"
|
||||
CLIENT_PUBLIC_DIR="/var/lib/mnml/public/current"
|
||||
CLIENT_PUBLIC_DIR="/var/lib/mnml/public/client"
|
||||
|
||||
ACP_DIST_DIR="/var/lib/mnml/acp"
|
||||
ACP_PUBLIC_DIR="/var/lib/mnml/public/acp"
|
||||
|
||||
# server updates
|
||||
echo "syncing server $VERSION "
|
||||
@ -19,6 +23,11 @@ ssh -q mnml ls -lah "$SERVER_BIN_DIR"
|
||||
echo "syncing client $VERSION"
|
||||
rsync -a --delete --delete-excluded "$MNML_PATH/client/dist/" mnml:"$CLIENT_DIST_DIR/$VERSION/"
|
||||
ssh -q mnml ln -nfs "$CLIENT_DIST_DIR/$VERSION" "$CLIENT_PUBLIC_DIR"
|
||||
|
||||
# acp updates
|
||||
echo "syncing acp $VERSION"
|
||||
rsync -a --delete --delete-excluded "$MNML_PATH/acp/dist/" mnml:"$ACP_DIST_DIR/$VERSION/"
|
||||
ssh -q mnml ln -nfs "$ACP_DIST_DIR/$VERSION" "$ACP_PUBLIC_DIR"
|
||||
ssh -q mnml ls -lah "/var/lib/mnml/public"
|
||||
|
||||
echo "restarting mnml service"
|
||||
|
||||
55
client/assets/styles/account.less
Normal file
55
client/assets/styles/account.less
Normal file
@ -0,0 +1,55 @@
|
||||
@import 'colours.less';
|
||||
|
||||
.account {
|
||||
margin-top: 2em;
|
||||
grid-area: bottom;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-gap: 0 1em;
|
||||
|
||||
button {
|
||||
display: block;
|
||||
// height: 3em;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 75%;
|
||||
height: 3em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.list {
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
|
||||
figure {
|
||||
font-size: 125%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stripe-btn {
|
||||
background: @yellow;
|
||||
color: black;
|
||||
border-radius: 2px;
|
||||
border-width: 0;
|
||||
|
||||
&:active, &:focus, &:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
border: 1px solid @yellow;
|
||||
color: @yellow;
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
24
client/assets/styles/acp.less
Normal file
24
client/assets/styles/acp.less
Normal file
@ -0,0 +1,24 @@
|
||||
#mnml.acp {
|
||||
user-select: text;
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
|
||||
.bottom {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
@blue: #3050f8;
|
||||
@white: #f5f5f5; // whitesmoke
|
||||
@purple: #9355b5; // 6lack - that far cover
|
||||
@yellow: #ffa100;
|
||||
|
||||
@black: black;
|
||||
@gray: #222;
|
||||
@ -30,3 +31,33 @@ svg {
|
||||
stroke: @blue;
|
||||
}
|
||||
}
|
||||
|
||||
.green {
|
||||
color: @green;
|
||||
stroke: @green;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: @red;
|
||||
stroke: @red;
|
||||
}
|
||||
|
||||
.red-fill {
|
||||
fill: @red;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: @blue;
|
||||
stroke: @blue;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: #333;
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: @white;
|
||||
stroke: @white;
|
||||
}
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
aside {
|
||||
grid-area: ctrl;
|
||||
display: grid;
|
||||
|
||||
grid-template-areas:
|
||||
"timer controls"
|
||||
"timer controls"
|
||||
"timer controls";
|
||||
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-gap: 0.5em 0;
|
||||
|
||||
padding: 1em;
|
||||
padding: 1em 1em 1em 0;
|
||||
|
||||
// fix chrome being inconsistent
|
||||
table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
text-align: right;
|
||||
@ -28,6 +40,30 @@ aside {
|
||||
border-color: forestgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.timer-container {
|
||||
grid-area: timer;
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
width: 0.25em;
|
||||
max-width: 0.25em;
|
||||
|
||||
margin: 0 1em 0 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.timer {
|
||||
background: whitesmoke;
|
||||
transition-property: all;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
}
|
||||
|
||||
.ready-btn:hover, .ready-btn:focus, .ready-btn:active {
|
||||
|
||||
51
client/assets/styles/footer.less
Normal file
51
client/assets/styles/footer.less
Normal file
@ -0,0 +1,51 @@
|
||||
footer {
|
||||
display: none;
|
||||
flex-flow: row wrap;
|
||||
grid-area: footer;
|
||||
margin: 0;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: #222;
|
||||
font-size: 1.5em;
|
||||
padding: 0.25em;
|
||||
|
||||
&[disabled] {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
background: #222;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.ready {
|
||||
background: forestgreen;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.timer-container {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
height: 0.25em;
|
||||
max-height: 0.25em;
|
||||
|
||||
border: none;
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
background: whitesmoke;
|
||||
transition-property: all;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-btn, #instance-nav {
|
||||
display: none;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
@import 'colours.less';
|
||||
/* GAME */
|
||||
|
||||
.game {
|
||||
@ -322,15 +323,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*@keyframes rotate {
|
||||
0% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateY(50deg);
|
||||
}
|
||||
}
|
||||
*/
|
||||
.resolving .skills button {
|
||||
opacity: 0;
|
||||
}
|
||||
@ -338,80 +330,9 @@
|
||||
.skill-animation {
|
||||
opacity: 0;
|
||||
stroke-width: 5px;
|
||||
height: 5em;
|
||||
}
|
||||
|
||||
/*
|
||||
COMBAT ANIMATIONS
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------
|
||||
* Generated by Animista on 2019-5-21 11:38:30
|
||||
* w: http://animista.net, t: @cssanimista
|
||||
* ---------------------------------------------- */
|
||||
|
||||
|
||||
/*.attack-cast {
|
||||
-webkit-animation: attack-cast 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
animation: attack-cast 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes attack-cast {
|
||||
0% {
|
||||
-webkit-transform: translateY(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateY(-5em);
|
||||
transform: translateY(-5em);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes attack-cast {
|
||||
0% {
|
||||
-webkit-transform: translateY(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateY(-5em);
|
||||
transform: translateY(-5em);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attack-hit {
|
||||
-webkit-animation: attack-hit 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: attack-hit 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes attack-hit {
|
||||
0% {
|
||||
-webkit-transform: translateY(-5em) translateX(5em);
|
||||
transform: translateY(-5em) translateX(5em);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateY(0) translateX(0);
|
||||
transform: translateY(0) translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes attack-hit {
|
||||
0% {
|
||||
-webkit-transform: translateY(-5em) translateX(5em);
|
||||
transform: translateY(-5em) translateX(5em);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateY(0) translateX(0);
|
||||
transform: translateY(0) translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.game {
|
||||
grid-template-areas:
|
||||
|
||||
@ -426,6 +426,12 @@
|
||||
margin-top: 1em;
|
||||
grid-area: playername;
|
||||
}
|
||||
|
||||
.team {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Nav*/
|
||||
|
||||
90
client/assets/styles/menu.less
Normal file
90
client/assets/styles/menu.less
Normal file
@ -0,0 +1,90 @@
|
||||
@import 'colours.less';
|
||||
|
||||
.menu {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: minmax(min-content, 2fr) 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"bottom";
|
||||
|
||||
.top {
|
||||
grid-area: top;
|
||||
padding: 0 0 0.5em 2em;
|
||||
border-bottom: 0.1em solid #222;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
.team {
|
||||
display: grid;
|
||||
grid-area: top;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
|
||||
max-height: 100%;
|
||||
|
||||
.team-select:not(:nth-child(3n)) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.construct {
|
||||
flex: 1 1 33%;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid black;
|
||||
transition-property: border;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inventory {
|
||||
margin-top: 2em;
|
||||
grid-area: bottom;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.list {
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-gap: 1em;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
figure {
|
||||
font-size: 125%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
client/assets/styles/nav.less
Normal file
99
client/assets/styles/nav.less
Normal file
@ -0,0 +1,99 @@
|
||||
nav {
|
||||
grid-area: nav;
|
||||
padding-left: 2em;
|
||||
margin-right: 2em;
|
||||
max-height: 100%;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
h2:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1em 0;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
button {
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
color: #888;
|
||||
flex: 1 1 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
button.active {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
button[disabled], button[disabled]:hover {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: whitesmoke;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button:focus, button:active {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
letter-spacing: 0.05em;
|
||||
flex: 1;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.account-info svg {
|
||||
margin: 0.5em 0 0 1em;
|
||||
height: 1em;
|
||||
background-color: black;
|
||||
stroke: whitesmoke;
|
||||
}
|
||||
|
||||
.ping-path {
|
||||
fill: none;
|
||||
stroke-width: 4px;
|
||||
stroke-dasharray: 121, 242;
|
||||
animation: saw 2s infinite linear;
|
||||
|
||||
transition-property: stroke-color;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.ping-text {
|
||||
margin-left: 1em;
|
||||
min-width: 3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes saw {
|
||||
0% {
|
||||
stroke-dashoffset: 363;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,6 @@
|
||||
@import 'colours.less';
|
||||
|
||||
/*
|
||||
GLOBAL
|
||||
*/
|
||||
|
||||
html, body, #mnml {
|
||||
/*width: 100%;*/
|
||||
margin: 0;
|
||||
|
||||
background-color: black;
|
||||
@ -48,6 +43,9 @@ html {
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0;
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
}
|
||||
|
||||
h2 {
|
||||
@ -66,6 +64,7 @@ h4 {
|
||||
}
|
||||
|
||||
hr {
|
||||
color: #222;
|
||||
margin: 1.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
@ -85,70 +84,11 @@ figure {
|
||||
"nav main ctrl";
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-area: nav;
|
||||
padding-left: 2em;
|
||||
margin-right: 2em;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
nav h2:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
nav hr {
|
||||
margin: 1em 0;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
nav button {
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
color: #888;
|
||||
flex: 1 1 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
nav button.active {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
nav button[disabled], nav button[disabled]:hover {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav button:hover {
|
||||
color: whitesmoke;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
nav button:focus, nav button:active {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em;
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
tr.right:focus, tr.right:hover {
|
||||
box-shadow: inset -0.5em 0 0 0 whitesmoke;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition-property: color, background;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
button, input {
|
||||
font-family: 'Jura';
|
||||
color: whitesmoke;
|
||||
@ -166,22 +106,34 @@ button, input {
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
&:hover {
|
||||
color: whitesmoke;
|
||||
border-color: @gray-hover;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
&:focus {
|
||||
/*colour necesary to bash skellington*/
|
||||
color: @gray-focus;
|
||||
border-color: @gray-focus;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: whitesmoke;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: whitesmoke;
|
||||
border-color: @gray-hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
/*colour necesary to bash skellington*/
|
||||
color: @gray-focus;
|
||||
border-color: @gray-focus;
|
||||
}
|
||||
|
||||
a {
|
||||
color: whitesmoke;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
svg {
|
||||
@ -191,19 +143,6 @@ svg {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.skill-animation {
|
||||
height: 5em;
|
||||
}
|
||||
|
||||
.team .avatar {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
@ -246,64 +185,19 @@ button[disabled] {
|
||||
border-color: #222;
|
||||
}
|
||||
|
||||
/*
|
||||
COLOURS
|
||||
*/
|
||||
|
||||
.green {
|
||||
color: #1FF01F;
|
||||
stroke: #1FF01F;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #a52a2a;
|
||||
stroke: #a52a2a;
|
||||
}
|
||||
|
||||
.red-fill {
|
||||
fill: #a52a2a;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #3050f8;
|
||||
stroke: #3050f8;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.yellow {
|
||||
color: #FFD123;
|
||||
stroke: #FFD123;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: #A25AC1;
|
||||
stroke: #A25AC1;
|
||||
}
|
||||
|
||||
.cyan {
|
||||
color: #6AD1BF;
|
||||
stroke: #6AD1BF;
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: #333;
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: whitesmoke;
|
||||
stroke: whitesmoke;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
LOGIN
|
||||
*/
|
||||
|
||||
.login {
|
||||
width: 25%;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin-bottom: 2em;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
#mnml input, #mnml select {
|
||||
@ -316,242 +210,6 @@ button[disabled] {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
/*
|
||||
account
|
||||
*/
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
grid-area: hd;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.account {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.account-status {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
letter-spacing: 0.05em;
|
||||
flex: 1;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.account-status svg {
|
||||
margin: 0.5em 0 0 1em;
|
||||
height: 1em;
|
||||
background-color: black;
|
||||
stroke: whitesmoke;
|
||||
}
|
||||
|
||||
.ping-path {
|
||||
fill: none;
|
||||
stroke-width: 4px;
|
||||
stroke-dasharray: 121, 242;
|
||||
animation: saw 2s infinite linear;
|
||||
|
||||
transition-property: stroke-color;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.ping-text {
|
||||
margin-left: 1em;
|
||||
min-width: 3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes saw {
|
||||
0% {
|
||||
stroke-dashoffset: 363;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TEAM
|
||||
*/
|
||||
|
||||
.team {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.menu-construct {
|
||||
margin: 0.5em;
|
||||
flex: 1 1 auto;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid black;
|
||||
transition-property: border;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.spawn-btn.menu-construct {
|
||||
border: 1px solid #333;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spawn-btn.menu-construct.selected {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
.spawn-btn.menu-construct:hover {
|
||||
border: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.spawn-btn input {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.spawn-btn button {
|
||||
flex: 1 1 100%;
|
||||
margin: 0.5em 1em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.spawn-btn input[disabled], .spawn-btn button[disabled] {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.play {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: minmax(min-content, 2fr) 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
grid-template-areas:
|
||||
"team"
|
||||
"inventory";
|
||||
|
||||
.team {
|
||||
grid-area: team;
|
||||
|
||||
/* poor man's <hr>*/
|
||||
padding: 0.5em 2em 0 0;
|
||||
border-bottom: 0.1em solid #444;
|
||||
|
||||
}
|
||||
|
||||
.menu-construct {
|
||||
flex: 1 0 33%;
|
||||
}
|
||||
|
||||
.inventory {
|
||||
margin: 2em 2em 0 0;
|
||||
grid-area: inventory;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.list {
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-gap: 1em;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
figure {
|
||||
font-size: 125%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-instance-btn {
|
||||
flex: 1 1 100%;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.stripe-btn {
|
||||
width: 100%;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.25em 0;
|
||||
background: whitesmoke;
|
||||
color: black;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.stripe-btn:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
border: 1px solid #222;
|
||||
float: right;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.create-form {
|
||||
grid-area: create;
|
||||
flex: 1;
|
||||
|
||||
justify-self: flex-end;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.create-form button {
|
||||
flex: 1;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.create-form button:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.create-form button:hover, .create-form button:focus {
|
||||
border-color: whitesmoke;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
figure.gray {
|
||||
color: #333;
|
||||
stroke-color: #333;
|
||||
@ -562,56 +220,9 @@ figure.gray {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
main .top button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timer-container {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
height: 0.25em;
|
||||
max-height: 0.25em;
|
||||
|
||||
border: none;
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
background: whitesmoke;
|
||||
transition-property: all;
|
||||
transition-duration: 0.25s;
|
||||
transition-delay: 0;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: none;
|
||||
flex-flow: row wrap;
|
||||
grid-area: footer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer button {
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: #222;
|
||||
font-size: 1.5em;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
footer button:disabled {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
footer button:not(:last-child) {
|
||||
background: #222;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
footer button .ready {
|
||||
background: forestgreen;
|
||||
color: black;
|
||||
.credits {
|
||||
color: @yellow;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
@ -624,10 +235,6 @@ footer button .ready {
|
||||
}
|
||||
}
|
||||
|
||||
#nav-btn, #instance-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
display: none;
|
||||
}
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
main {
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.login {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>mnml.gg</title>
|
||||
<title>mnml - abstract strategy</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name=apple-mobile-web-app-capable content=yes>
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
require('./assets/styles/styles.less');
|
||||
require('./assets/styles/menu.less');
|
||||
require('./assets/styles/nav.less');
|
||||
require('./assets/styles/footer.less');
|
||||
require('./assets/styles/account.less');
|
||||
require('./assets/styles/controls.less');
|
||||
require('./assets/styles/instance.less');
|
||||
require('./assets/styles/vbox.less');
|
||||
require('./assets/styles/game.less');
|
||||
require('./assets/styles/controls.less');
|
||||
require('./assets/styles/styles.mobile.css');
|
||||
require('./assets/styles/instance.mobile.css');
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"docco": "^0.7.0",
|
||||
"izitoast": "^1.4.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkstate": "^1.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"node-sass": "^4.12.0",
|
||||
"parcel": "^1.12.3",
|
||||
|
||||
151
client/src/components/account.management.jsx
Normal file
151
client/src/components/account.management.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
const preact = require('preact');
|
||||
const { Component } = require('preact')
|
||||
const { connect } = require('preact-redux');
|
||||
const linkState = require('linkstate').default;
|
||||
|
||||
const SpawnButton = require('./spawn.button');
|
||||
|
||||
const { postData, errorToast, infoToast } = require('../utils');
|
||||
const actions = require('../actions');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
ws,
|
||||
} = state;
|
||||
|
||||
|
||||
function setPassword(current, password) {
|
||||
postData('/account/password', { current, password })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.success) return errorToast(data.error_message);
|
||||
infoToast('Password changed. Reloading...')
|
||||
setTimeout(() => window.location.reload(), 5000);
|
||||
})
|
||||
.catch(error => errorToast(error));
|
||||
}
|
||||
|
||||
function logout() {
|
||||
postData('/account/logout').then(() => window.location.reload(true));
|
||||
}
|
||||
|
||||
|
||||
function sendConstructSpawn(name) {
|
||||
return ws.sendMtxConstructSpawn(name);
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
setPassword,
|
||||
sendConstructSpawn,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
class AccountStatus extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
setPassword: { current: '', password: '', confirm: ''},
|
||||
};
|
||||
}
|
||||
|
||||
render(args) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
setPassword,
|
||||
sendConstructSpawn,
|
||||
} = args;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const passwordsEqual = () =>
|
||||
this.state.setPassword.password === this.state.setPassword.confirm;
|
||||
|
||||
const setPasswordDisabled = () => {
|
||||
const { current, password, confirm } = this.state.setPassword;
|
||||
return !(passwordsEqual() && password && current && confirm);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class='account'>
|
||||
<div>
|
||||
<h1>{account.name}</h1>
|
||||
<dl>
|
||||
<dt>Subscription</dt>
|
||||
<dd>{account.subscribed ? 'some date' : 'unsubscribed'}</dd>
|
||||
</dl>
|
||||
<button><a href={`mailto:support@mnml.gg?subject=Account%20Support:%20${account.name}`}>✉ support</a></button>
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email:</label>
|
||||
<dl>
|
||||
<dt>Current Email</dt>
|
||||
<dd>{account.email ? account.email : 'No email set'}</dd>
|
||||
<dt>Status</dt>
|
||||
<dd>{account.email_confirmed ? 'Confirmed' : 'Unconfirmed'}</dd>
|
||||
</dl>
|
||||
<input
|
||||
class="login-input"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="new email"
|
||||
/>
|
||||
<button>Update</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="current">Password:</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
name="current"
|
||||
value={this.state.setPassword.current}
|
||||
onInput={linkState(this, 'setPassword.current')}
|
||||
placeholder="current"
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
name="new"
|
||||
value={this.state.setPassword.password}
|
||||
onInput={linkState(this, 'setPassword.password')}
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
name="confirm"
|
||||
value={this.state.setPassword.confirm}
|
||||
onInput={linkState(this, 'setPassword.confirm')}
|
||||
placeholder="confirm"
|
||||
/>
|
||||
<button
|
||||
disabled={setPasswordDisabled()}
|
||||
onClick={() => setPassword(this.state.setPassword.current, this.state.setPassword.password)}>
|
||||
Set Password
|
||||
</button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<figure>
|
||||
<figcaption>spawn new construct</figcaption>
|
||||
<button onClick={() => sendConstructSpawn()} type="submit">
|
||||
¤50
|
||||
</button>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = addState(AccountStatus);
|
||||
33
client/src/components/account.page.jsx
Normal file
33
client/src/components/account.page.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
const { connect } = require('preact-redux');
|
||||
const preact = require('preact');
|
||||
|
||||
const Team = require('./team');
|
||||
const AccountManagement = require('./account.management');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
ws,
|
||||
account,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
account,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
function Account(args) {
|
||||
const {
|
||||
account,
|
||||
} = args;
|
||||
|
||||
return (
|
||||
<main class="menu">
|
||||
<Team />
|
||||
<AccountManagement />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = addState(Account);
|
||||
@ -1,9 +1,7 @@
|
||||
const { connect } = require('preact-redux');
|
||||
const preact = require('preact');
|
||||
const { Elements, injectStripe } = require('react-stripe-elements');
|
||||
|
||||
const { saw } = require('./shapes');
|
||||
const { postData } = require('./../utils');
|
||||
const actions = require('../actions');
|
||||
|
||||
function pingColour(ping) {
|
||||
@ -12,55 +10,6 @@ function pingColour(ping) {
|
||||
return 'red';
|
||||
}
|
||||
|
||||
function BitsBtn(args) {
|
||||
const {
|
||||
stripe,
|
||||
account,
|
||||
} = args;
|
||||
function subscribeClick(e) {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}],
|
||||
successUrl: 'http://localhost/api/payments/success',
|
||||
cancelUrl: 'http://localhost/api/payments/cancel',
|
||||
clientReferenceId: account.id
|
||||
});
|
||||
}
|
||||
|
||||
function bitsClick(e) {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{sku: 'sku_FHUfNEhWQaVDaT', quantity: 1}],
|
||||
successUrl: 'http://localhost/payments/success',
|
||||
cancelUrl: 'http://localhost/payments/cancel',
|
||||
clientReferenceId: account.id
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = account.subscribed
|
||||
? <h3 class="account-header">Subscribed</h3>
|
||||
: <button
|
||||
onClick={subscribeClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Subscribe
|
||||
</button>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="error-message"></div>
|
||||
{subscription}
|
||||
<button
|
||||
onClick={bitsClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Get Credits
|
||||
</button>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StripeBitsBtn = injectStripe(BitsBtn);
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
@ -68,23 +17,28 @@ const addState = connect(
|
||||
ping,
|
||||
} = state;
|
||||
|
||||
|
||||
function logout() {
|
||||
postData('/logout').then(() => window.location.reload(true));
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
};
|
||||
},
|
||||
function receiveDispatch(dispatch) {
|
||||
function selectConstructs() {
|
||||
return dispatch(actions.setNav('team'));
|
||||
function accountPage() {
|
||||
dispatch(actions.setGame(null));
|
||||
dispatch(actions.setInstance(null));
|
||||
dispatch(actions.setCombiner([]));
|
||||
dispatch(actions.setReclaiming(false));
|
||||
dispatch(actions.setActiveSkill(null));
|
||||
dispatch(actions.setActiveConstruct(null));
|
||||
dispatch(actions.setInfo(null));
|
||||
dispatch(actions.setItemEquip(null));
|
||||
dispatch(actions.setItemUnequip([]));
|
||||
dispatch(actions.setVboxHighlight([]));
|
||||
return dispatch(actions.setNav('account'));
|
||||
}
|
||||
|
||||
return {
|
||||
selectConstructs,
|
||||
accountPage,
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -94,25 +48,20 @@ function AccountStatus(args) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
selectConstructs,
|
||||
accountPage,
|
||||
} = args;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div class="account">
|
||||
<div class="account-status">
|
||||
<div class="account-info">
|
||||
<h2 class="account-header">{account.name}</h2>
|
||||
{saw(pingColour(ping))}
|
||||
<div class="ping-text">{ping}ms</div>
|
||||
</div>
|
||||
<h3 class="account-header">{`¤${account.balance}`}</h3>
|
||||
<Elements>
|
||||
<StripeBitsBtn account={account} />
|
||||
</Elements>
|
||||
<button onClick={() => selectConstructs()}>Constructs </button>
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
<h3 class="account-header credits">{`¤${account.balance}`}</h3>
|
||||
<button onClick={() => accountPage()}>⚙ account</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,12 +10,14 @@ const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
ws,
|
||||
account,
|
||||
game,
|
||||
instance,
|
||||
nav,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
account,
|
||||
game,
|
||||
instance,
|
||||
nav,
|
||||
@ -26,15 +28,18 @@ const addState = connect(
|
||||
function Controls(args) {
|
||||
const {
|
||||
game,
|
||||
account,
|
||||
instance,
|
||||
nav,
|
||||
sendGameReady,
|
||||
} = args;
|
||||
|
||||
if (!account) return false;
|
||||
|
||||
if (game) return <GameCtrl />;
|
||||
if (instance) return <InstanceCtrl />;
|
||||
if (nav === 'play' || !nav) return <PlayCtrl />
|
||||
if (nav === 'team' || !nav) return <TeamCtrl />
|
||||
if (nav === 'team' || nav === 'account') return <TeamCtrl />
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -20,16 +20,22 @@ const addState = connect(
|
||||
return ws.sendGameReady(game.id);
|
||||
}
|
||||
|
||||
function getInstanceState() {
|
||||
return ws.sendInstanceState(game.instance);
|
||||
}
|
||||
|
||||
return {
|
||||
game,
|
||||
sendReady,
|
||||
account,
|
||||
getInstanceState,
|
||||
animating,
|
||||
};
|
||||
},
|
||||
|
||||
function receiveDispatch(dispatch) {
|
||||
function quit() {
|
||||
dispatch(actions.setNav('transition'));
|
||||
dispatch(actions.setGame(null));
|
||||
dispatch(actions.setInstance(null));
|
||||
}
|
||||
@ -74,6 +80,7 @@ function Controls(args) {
|
||||
animating,
|
||||
sendReady,
|
||||
quit,
|
||||
getInstanceState,
|
||||
} = args;
|
||||
|
||||
if (!game) return false;
|
||||
@ -81,11 +88,43 @@ function Controls(args) {
|
||||
const opponent = game.players.find(t => t.id !== account.id);
|
||||
const player = game.players.find(t => t.id === account.id);
|
||||
|
||||
function quitClick() {
|
||||
getInstanceState();
|
||||
quit();
|
||||
}
|
||||
|
||||
const zero = Date.parse(game.phase_start);
|
||||
const now = Date.now();
|
||||
const end = Date.parse(game.phase_end);
|
||||
const timerPct = game.phase_end
|
||||
? ((now - zero) / (end - zero) * 100)
|
||||
: 100;
|
||||
|
||||
const displayColour = !game.phase_end
|
||||
? '#222'
|
||||
: player.ready
|
||||
? 'forestgreen'
|
||||
: timerPct > 80
|
||||
? 'red'
|
||||
: 'whitesmoke';
|
||||
|
||||
const timerStyles = {
|
||||
height: `${timerPct > 100 ? 100 : timerPct}%`,
|
||||
background: displayColour,
|
||||
};
|
||||
|
||||
const timer = (
|
||||
<div class="timer-container">
|
||||
<div class="timer" style={timerStyles} > </div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const readyBtn = <button disabled={animating} class="ready" onClick={() => sendReady()}>Ready</button>;
|
||||
const quitBtn = <button disabled={animating} onClick={() => quit()}>Back</button>;
|
||||
const quitBtn = <button disabled={animating} onClick={quitClick}>Back</button>;
|
||||
|
||||
return (
|
||||
<aside class="controls">
|
||||
{timer}
|
||||
{scoreboard(game, opponent, true)}
|
||||
{game.phase === 'Finish' ? quitBtn : readyBtn}
|
||||
{scoreboard(game, player)}
|
||||
|
||||
@ -66,8 +66,35 @@ function Controls(args) {
|
||||
const opponent = instance.players.find(t => t.id !== account.id);
|
||||
const player = instance.players.find(t => t.id === account.id);
|
||||
|
||||
const zero = Date.parse(instance.phase_start);
|
||||
const now = Date.now();
|
||||
const end = Date.parse(instance.phase_end);
|
||||
const timerPct = instance.phase_end
|
||||
? ((now - zero) / (end - zero) * 100)
|
||||
: 100;
|
||||
|
||||
const displayColour = !instance.phase_end
|
||||
? '#222'
|
||||
: player.ready
|
||||
? 'forestgreen'
|
||||
: timerPct > 80
|
||||
? 'red'
|
||||
: 'whitesmoke';
|
||||
|
||||
const timerStyles = {
|
||||
height: `${timerPct > 100 ? 100 : timerPct}%`,
|
||||
background: displayColour,
|
||||
};
|
||||
|
||||
const timer = (
|
||||
<div class="timer-container">
|
||||
<div class="timer" style={timerStyles} > </div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside class="controls">
|
||||
{timer}
|
||||
{scoreboard(instance, opponent, true)}
|
||||
<button class="ready" onClick={() => sendReady()}>Ready</button>
|
||||
{scoreboard(instance, player)}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
const { connect } = require('preact-redux');
|
||||
const { Elements } = require('react-stripe-elements');
|
||||
|
||||
const preact = require('preact');
|
||||
const toast = require('izitoast');
|
||||
|
||||
const actions = require('./../actions');
|
||||
const StripeBtns = require('./stripe.buttons');
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
@ -63,15 +66,18 @@ function Inventory(args) {
|
||||
return (
|
||||
<div class="inventory">
|
||||
<div>
|
||||
<h1>¤ {account.balance}</h1>
|
||||
<h1>Shop</h1>
|
||||
<Elements>
|
||||
<StripeBtns account={account} />
|
||||
</Elements>
|
||||
<div class='list'>
|
||||
{shop.owned.map(useMtx)}
|
||||
{shop.available.map(availableMtx)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Shop</h1>
|
||||
<h1 class="credits">¤ {account.balance}</h1>
|
||||
<div class='list'>
|
||||
{shop.available.map(availableMtx)}
|
||||
{shop.owned.map(useMtx)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
const preact = require('preact');
|
||||
const { Component } = require('preact')
|
||||
const { connect } = require('preact-redux');
|
||||
const linkState = require('linkstate').default;
|
||||
|
||||
const { postData, errorToast } = require('../utils');
|
||||
|
||||
@ -11,10 +12,10 @@ const addState = connect(
|
||||
ws
|
||||
} = state;
|
||||
function submitLogin(name, password) {
|
||||
postData('/login', { name, password })
|
||||
postData('/account/login', { name, password })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.success) return errorToast(data.error_message);
|
||||
if (data.error) return errorToast(data.error);
|
||||
console.log(data.response);
|
||||
ws.connect();
|
||||
})
|
||||
@ -22,10 +23,10 @@ const addState = connect(
|
||||
}
|
||||
|
||||
function submitRegister(name, password, code) {
|
||||
postData('/register', { name, password, code })
|
||||
postData('/account/register', { name, password, code })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.success) return errorToast(data.error_message);
|
||||
if (data.error) return errorToast(data.error);
|
||||
console.log(data.response);
|
||||
ws.connect();
|
||||
})
|
||||
@ -43,87 +44,122 @@ class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { name: '', password: '', code: ''};
|
||||
this.state = {
|
||||
login: { name: '', password: '', code: ''},
|
||||
register: { name: '', password: '', confirm: '', code: ''},
|
||||
};
|
||||
|
||||
this.nameInput = this.nameInput.bind(this);
|
||||
this.passwordInput = this.passwordInput.bind(this);
|
||||
this.codeInput = this.codeInput.bind(this);
|
||||
this.loginSubmit = this.loginSubmit.bind(this);
|
||||
this.registerSubmit = this.registerSubmit.bind(this);
|
||||
}
|
||||
|
||||
nameInput(event) {
|
||||
this.setState({ name: event.target.value });
|
||||
}
|
||||
|
||||
passwordInput(event) {
|
||||
this.setState({ password: event.target.value });
|
||||
}
|
||||
|
||||
codeInput(event) {
|
||||
this.setState({ code: event.target.value });
|
||||
}
|
||||
|
||||
loginSubmit(event) {
|
||||
event.preventDefault();
|
||||
console.log(this.state);
|
||||
this.props.submitLogin(this.state.name, this.state.password);
|
||||
this.setState({ name: '', password: '' });
|
||||
this.props.submitLogin(this.state.login.name, this.state.login.password);
|
||||
this.setState({ login: { name: '', password: '' }});
|
||||
}
|
||||
|
||||
registerSubmit(event) {
|
||||
event.preventDefault();
|
||||
this.props.submitRegister(this.state.name, this.state.password, this.state.code);
|
||||
console.log(this.state);
|
||||
this.setState({ name: '', password: '', code: ''});
|
||||
this.props.submitRegister(this.state.register.name, this.state.register.password, this.state.register.code);
|
||||
this.setState({ register: { name: '', password: '', confirm: '', code: ''}});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const registerConfirm = () =>
|
||||
this.state.register.password === this.state.register.confirm;
|
||||
|
||||
const loginDisabled = () => {
|
||||
const { password, name } = this.state.login;
|
||||
return !(password && name);
|
||||
}
|
||||
|
||||
const registerDisabled = () => {
|
||||
const { password, name, code } = this.state.register;
|
||||
return !(registerConfirm() && password && name && code);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1 class="mobile-title" >mnml.gg</h1>
|
||||
<h1>mnml.gg</h1>
|
||||
<div class="login">
|
||||
<div>mnml is an abstract turn based strategy game</div>
|
||||
<div>free to play</div>
|
||||
<div>no email required</div>
|
||||
</div>
|
||||
<div class="login">
|
||||
<h2>Login</h2>
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="email"
|
||||
placeholder="username"
|
||||
tabIndex={1}
|
||||
value={this.state.name}
|
||||
onInput={this.nameInput}
|
||||
value={this.state.login.name}
|
||||
onInput={linkState(this, 'login.name')}
|
||||
/>
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
tabIndex={2}
|
||||
value={this.state.password}
|
||||
onInput={this.passwordInput}
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="text"
|
||||
placeholder="code"
|
||||
tabIndex={3}
|
||||
value={this.state.code}
|
||||
onInput={this.codeInput}
|
||||
value={this.state.login.password}
|
||||
onInput={linkState(this, 'login.password')}
|
||||
/>
|
||||
<button
|
||||
class="login-btn"
|
||||
tabIndex={4}
|
||||
disabled={loginDisabled()}
|
||||
onClick={this.loginSubmit}>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
<div class="login">
|
||||
<h2>Register</h2>
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="email"
|
||||
placeholder="username"
|
||||
value={this.state.register.name}
|
||||
onInput={linkState(this, 'register.name')}
|
||||
/>
|
||||
<label for="password">Password - min 12 chars</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={this.state.register.password}
|
||||
onInput={linkState(this, 'register.password')}
|
||||
/>
|
||||
<label for="confirm">Confirm Password</label>
|
||||
<input
|
||||
class={`${registerConfirm() ? '' : 'red'} login-input`}
|
||||
type="password"
|
||||
placeholder="confirm"
|
||||
value={this.state.register.confirm}
|
||||
onInput={linkState(this, 'register.confirm')}
|
||||
/>
|
||||
<label for="code">Access Code</label>
|
||||
<input
|
||||
class="login-input"
|
||||
type="text"
|
||||
placeholder="code"
|
||||
value={this.state.register.code}
|
||||
onInput={linkState(this, 'register.code')}
|
||||
/>
|
||||
<button
|
||||
class="login-btn"
|
||||
tabIndex={5}
|
||||
disabled={registerDisabled()}
|
||||
onClick={this.registerSubmit}>
|
||||
Register
|
||||
</button>
|
||||
<button
|
||||
class="login-btn"
|
||||
onClick={() => document.location.assign('https://discord.gg/YJJgurM')}>
|
||||
Discord + Invites
|
||||
Discord + Codes
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -7,6 +7,7 @@ const Game = require('./game');
|
||||
const Instance = require('./instance.component');
|
||||
const Team = require('./team');
|
||||
const Play = require('./play');
|
||||
const Account = require('./account.page');
|
||||
|
||||
const addState = connect(
|
||||
state => {
|
||||
@ -38,6 +39,7 @@ function Main(props) {
|
||||
if (nav === 'transition') return false;
|
||||
if (nav === 'play') return <Play />;
|
||||
if (nav === 'team') return <Team />;
|
||||
if (nav === 'account') return <Account />;
|
||||
|
||||
return (
|
||||
<Play />
|
||||
|
||||
@ -69,6 +69,8 @@ function Nav(args) {
|
||||
hideNav,
|
||||
} = args;
|
||||
|
||||
if (!account) return false;
|
||||
|
||||
function navTo(p) {
|
||||
return setNav(p);
|
||||
}
|
||||
@ -90,6 +92,7 @@ function Nav(args) {
|
||||
|
||||
const canJoin = team.some(c => !c);
|
||||
|
||||
|
||||
return (
|
||||
<nav onClick={hideNav} >
|
||||
<h1 class="header-title">mnml.gg</h1>
|
||||
|
||||
@ -27,7 +27,8 @@ function JoinButtons(args) {
|
||||
} = args;
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<aside class='play-ctrl'>
|
||||
<div class="timer-container"></div>
|
||||
<button
|
||||
class='pvp ready'
|
||||
onClick={() => sendInstanceQueue()}
|
||||
|
||||
@ -97,7 +97,7 @@ function Play(args) {
|
||||
if (mtxActive === 'Rename') return setConstructRename(construct.id);
|
||||
return sendConstructAvatarReroll(construct.id);
|
||||
}}
|
||||
class="menu-construct" >
|
||||
class="construct">
|
||||
<ConstructAvatar construct={construct} />
|
||||
{constructName}
|
||||
{confirm}
|
||||
@ -107,8 +107,8 @@ function Play(args) {
|
||||
});
|
||||
|
||||
return (
|
||||
<main class="play">
|
||||
<div class="team">
|
||||
<main class="menu">
|
||||
<div class="team top">
|
||||
{constructPanels}
|
||||
</div>
|
||||
<Inventory />
|
||||
|
||||
54
client/src/components/stripe.buttons.jsx
Normal file
54
client/src/components/stripe.buttons.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
const preact = require('preact');
|
||||
const { injectStripe } = require('react-stripe-elements');
|
||||
|
||||
function BitsBtn(args) {
|
||||
const {
|
||||
stripe,
|
||||
account,
|
||||
} = args;
|
||||
function subscribeClick() {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{ plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1 }],
|
||||
successUrl: 'http://localhost',
|
||||
cancelUrl: 'http://localhost',
|
||||
clientReferenceId: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
function bitsClick() {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{ sku: 'sku_FHUfNEhWQaVDaT', quantity: 1 }],
|
||||
successUrl: 'http://localhost',
|
||||
cancelUrl: 'http://localhost',
|
||||
clientReferenceId: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = account.subscribed
|
||||
? <button
|
||||
class="stripe-btn"
|
||||
disabled>
|
||||
Subscribed
|
||||
</button>
|
||||
: <button
|
||||
onClick={subscribeClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Subscribe
|
||||
</button>;
|
||||
|
||||
return (
|
||||
<div class='list'>
|
||||
{subscription}
|
||||
<button
|
||||
onClick={bitsClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Get Credits
|
||||
</button>
|
||||
<div id="error-message"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = injectStripe(BitsBtn);
|
||||
@ -29,6 +29,7 @@ function TeamCtrl(args) {
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<div class="timer-container"></div>
|
||||
<button
|
||||
class='ready'
|
||||
disabled={teamSelect.some(c => !c)}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
const preact = require('preact');
|
||||
const { connect } = require('preact-redux');
|
||||
const range = require('lodash/range');
|
||||
|
||||
const actions = require('./../actions');
|
||||
const { COLOURS } = require('./../utils');
|
||||
const { stringSort } = require('./../utils');
|
||||
const SpawnButton = require('./spawn.button');
|
||||
const { ConstructAvatar } = require('./construct');
|
||||
|
||||
const addState = connect(
|
||||
@ -19,7 +16,6 @@ const addState = connect(
|
||||
return {
|
||||
constructs,
|
||||
teamSelect,
|
||||
sendConstructSpawn,
|
||||
};
|
||||
},
|
||||
function receiveDispatch(dispatch) {
|
||||
@ -31,7 +27,6 @@ const addState = connect(
|
||||
setTeam,
|
||||
};
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
function Team(args) {
|
||||
@ -39,7 +34,6 @@ function Team(args) {
|
||||
constructs,
|
||||
teamSelect,
|
||||
setTeam,
|
||||
sendConstructSpawn,
|
||||
} = args;
|
||||
|
||||
if (!constructs) return <div></div>;
|
||||
@ -70,29 +64,19 @@ function Team(args) {
|
||||
return (
|
||||
<div
|
||||
key={construct.id}
|
||||
class="menu-construct"
|
||||
class="construct team-select"
|
||||
style={ { 'border-color': borderColour || 'whitesmoke' } }
|
||||
onClick={() => selectConstruct(construct.id)} >
|
||||
<div class="controls">
|
||||
<h2>
|
||||
{construct.name}
|
||||
</h2>
|
||||
</div>
|
||||
<ConstructAvatar construct={construct} />
|
||||
<h2>{construct.name}</h2>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const spawnButtonsNum = (3 - constructs.length % 3);
|
||||
|
||||
const spawnButtons = range(spawnButtonsNum)
|
||||
.map(i => <SpawnButton key={constructs.length + i} spawn={() => sendConstructSpawn()} />);
|
||||
|
||||
return (
|
||||
<main class="team">
|
||||
<section class="team top">
|
||||
{constructPanels}
|
||||
{spawnButtons}
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -213,6 +213,15 @@ function errorToast(message) {
|
||||
});
|
||||
}
|
||||
|
||||
function infoToast(message) {
|
||||
toast.info({
|
||||
position: 'topRight',
|
||||
drag: false,
|
||||
close: false,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
function convertItem(v) {
|
||||
if (['Red', 'Green', 'Blue'].includes(v)) {
|
||||
return (
|
||||
@ -233,6 +242,7 @@ module.exports = {
|
||||
postData,
|
||||
convertItem,
|
||||
errorToast,
|
||||
infoToast,
|
||||
NULL_UUID,
|
||||
STATS,
|
||||
COLOURS,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
error_log /var/log/mnml/nginx.log;
|
||||
|
||||
upstream mnml_http {
|
||||
server 127.0.0.1:40000;
|
||||
}
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
upstream mnml_dev {
|
||||
server 0.0.0.0:41337;
|
||||
upstream mnml_http {
|
||||
server 127.0.0.1:40000;
|
||||
}
|
||||
|
||||
upstream mnml_ws {
|
||||
server 127.0.0.1:40055;
|
||||
}
|
||||
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
@ -9,25 +14,19 @@ map $http_upgrade $connection_upgrade {
|
||||
|
||||
# DEV
|
||||
server {
|
||||
root /var/lib/mnml/public/;
|
||||
index index.html;
|
||||
|
||||
server_name dev.mnml.gg; # managed by Certbot
|
||||
|
||||
location / {
|
||||
root /var/lib/mnml/public/current;
|
||||
index index.html;
|
||||
try_files $uri $uri/ index.html;
|
||||
}
|
||||
|
||||
location /imgs/ {
|
||||
root /var/lib/mnml/public/;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
listen [::]:443;
|
||||
ssl on;
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/dev.mnml.gg/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/dev.mnml.gg/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
location /api/ws {
|
||||
proxy_pass http://mnml_dev;
|
||||
proxy_pass http://mnml_ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
@ -35,15 +34,35 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://mnml_dev;
|
||||
proxy_pass http://mnml_http;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/mnml.gg/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/mnml.gg/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
server_name grep.live;
|
||||
|
||||
location / {
|
||||
root /var/lib/mnml/public/current/;
|
||||
index acp.html;
|
||||
try_files $uri $uri/ acp.html;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/grep.live/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/grep.live/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
# http -> https
|
||||
server {
|
||||
server_name dev.mnml.gg;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ iron = "0.6"
|
||||
bodyparser = "0.8"
|
||||
persistent = "0.4"
|
||||
router = "0.6"
|
||||
mount = "0.4"
|
||||
cookie = "0.12"
|
||||
crossbeam-channel = "0.3"
|
||||
ws = "0.8"
|
||||
|
||||
@ -8,7 +8,7 @@ use serde_cbor::{from_slice};
|
||||
|
||||
use postgres::transaction::Transaction;
|
||||
|
||||
use net::MnmlHttpError;
|
||||
use http::MnmlHttpError;
|
||||
use names::{name as generate_name};
|
||||
use construct::{Construct, construct_recover, construct_spawn};
|
||||
use instance::{Instance, instance_delete};
|
||||
@ -29,6 +29,23 @@ pub struct Account {
|
||||
pub subscribed: bool,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<postgres::rows::Row<'a>> for Account {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(row: postgres::rows::Row) -> Result<Self, Error> {
|
||||
let id: Uuid = row.get("id");
|
||||
|
||||
let db_balance: i64 = row.get("balance");
|
||||
let balance = u32::try_from(db_balance)
|
||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
|
||||
|
||||
let subscribed: bool = row.get("subscribed");
|
||||
let name: String = row.get("name");
|
||||
|
||||
Ok(Account { id, name, balance, subscribed })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(db: &Db, id: Uuid) -> Result<Account, Error> {
|
||||
let query = "
|
||||
SELECT id, name, balance, subscribed
|
||||
@ -42,13 +59,23 @@ pub fn select(db: &Db, id: Uuid) -> Result<Account, Error> {
|
||||
let row = result.iter().next()
|
||||
.ok_or(format_err!("account not found {:?}", id))?;
|
||||
|
||||
let db_balance: i64 = row.get(2);
|
||||
let balance = u32::try_from(db_balance)
|
||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
|
||||
Account::try_from(row)
|
||||
}
|
||||
|
||||
let subscribed: bool = row.get(3);
|
||||
pub fn select_name(db: &Db, name: &String) -> Result<Account, Error> {
|
||||
let query = "
|
||||
SELECT id, name, balance, subscribed
|
||||
FROM accounts
|
||||
WHERE name = $1;
|
||||
";
|
||||
|
||||
Ok(Account { id, name: row.get(1), balance, subscribed })
|
||||
let result = db
|
||||
.query(query, &[&name])?;
|
||||
|
||||
let row = result.iter().next()
|
||||
.ok_or(format_err!("account not found name={:?}", name))?;
|
||||
|
||||
Account::try_from(row)
|
||||
}
|
||||
|
||||
pub fn from_token(db: &Db, token: String) -> Result<Account, Error> {
|
||||
@ -65,15 +92,7 @@ pub fn from_token(db: &Db, token: String) -> Result<Account, Error> {
|
||||
let row = result.iter().next()
|
||||
.ok_or(err_msg("invalid token"))?;
|
||||
|
||||
let id: Uuid = row.get(0);
|
||||
let name: String = row.get(1);
|
||||
let subscribed: bool = row.get(2);
|
||||
let db_balance: i64 = row.get(3);
|
||||
|
||||
let balance = u32::try_from(db_balance)
|
||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
|
||||
|
||||
Ok(Account { id, name, balance, subscribed })
|
||||
Account::try_from(row)
|
||||
}
|
||||
|
||||
pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<Account, MnmlHttpError> {
|
||||
@ -101,23 +120,16 @@ pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<A
|
||||
},
|
||||
};
|
||||
|
||||
let id: Uuid = row.get(0);
|
||||
let hash: String = row.get(1);
|
||||
let name: String = row.get(2);
|
||||
let db_balance: i64 = row.get(3);
|
||||
let subscribed: bool = row.get(4);
|
||||
|
||||
if !verify(password, &hash)? {
|
||||
return Err(MnmlHttpError::PasswordNotMatch);
|
||||
}
|
||||
|
||||
let balance = u32::try_from(db_balance)
|
||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
|
||||
|
||||
Ok(Account { id, name, balance, subscribed })
|
||||
Account::try_from(row)
|
||||
.or(Err(MnmlHttpError::ServerError))
|
||||
}
|
||||
|
||||
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
|
||||
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, MnmlHttpError> {
|
||||
let mut rng = thread_rng();
|
||||
let token: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
@ -136,11 +148,73 @@ pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
|
||||
.query(query, &[&token, &id])?;
|
||||
|
||||
result.iter().next()
|
||||
.ok_or(format_err!("account not updated {:?}", id))?;
|
||||
.ok_or(MnmlHttpError::Unauthorized)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result<String, MnmlHttpError> {
|
||||
if password.len() < PASSWORD_MIN_LEN {
|
||||
return Err(MnmlHttpError::PasswordUnacceptable);
|
||||
}
|
||||
|
||||
let query = "
|
||||
SELECT id, password
|
||||
FROM accounts
|
||||
WHERE id = $1
|
||||
";
|
||||
|
||||
let result = tx
|
||||
.query(query, &[&id])?;
|
||||
|
||||
let row = match result.iter().next() {
|
||||
Some(row) => row,
|
||||
None => {
|
||||
let mut rng = thread_rng();
|
||||
let garbage: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.take(64)
|
||||
.collect();
|
||||
|
||||
// verify garbage to prevent timing attacks
|
||||
verify(garbage.clone(), &garbage).ok();
|
||||
return Err(MnmlHttpError::AccountNotFound);
|
||||
},
|
||||
};
|
||||
|
||||
let id: Uuid = row.get(0);
|
||||
let db_pw: String = row.get(1);
|
||||
|
||||
if !verify(current, &db_pw)? {
|
||||
return Err(MnmlHttpError::PasswordNotMatch);
|
||||
}
|
||||
|
||||
let rounds = 8;
|
||||
let password = hash(&password, rounds)?;
|
||||
|
||||
let query = "
|
||||
UPDATE accounts
|
||||
SET password = $1, updated_at = now()
|
||||
WHERE id = $2
|
||||
RETURNING id, name;
|
||||
";
|
||||
|
||||
let result = tx
|
||||
.query(query, &[&password, &id])?;
|
||||
|
||||
let row = match result.iter().next() {
|
||||
Some(row) => row,
|
||||
None => return Err(MnmlHttpError::DbError),
|
||||
};
|
||||
|
||||
let name: String = row.get(1);
|
||||
|
||||
info!("password updated name={:?} id={:?}", name, id);
|
||||
|
||||
new_token(tx, id)
|
||||
}
|
||||
|
||||
|
||||
pub fn credit(tx: &mut Transaction, id: Uuid, credits: i64) -> Result<String, Error> {
|
||||
let query = "
|
||||
UPDATE accounts
|
||||
|
||||
125
server/src/acp.rs
Normal file
125
server/src/acp.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use iron::prelude::*;
|
||||
use iron::status;
|
||||
|
||||
use iron::{BeforeMiddleware};
|
||||
use persistent::Read;
|
||||
use router::Router;
|
||||
|
||||
use serde::{Deserialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use account;
|
||||
use game;
|
||||
|
||||
use http::{State, MnmlHttpError, json_object};
|
||||
|
||||
struct AcpMiddleware;
|
||||
impl BeforeMiddleware for AcpMiddleware {
|
||||
fn before(&self, req: &mut Request) -> IronResult<()> {
|
||||
match req.extensions.get::<account::Account>() {
|
||||
Some(a) => {
|
||||
if ["ntr", "mashy"].contains(&a.name.to_ascii_lowercase().as_ref()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(MnmlHttpError::Unauthorized.into());
|
||||
},
|
||||
None => Err(MnmlHttpError::Unauthorized.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Deserialize)]
|
||||
struct GetUser {
|
||||
name: Option<String>,
|
||||
id: Option<Uuid>,
|
||||
}
|
||||
|
||||
fn acp_user(req: &mut Request) -> IronResult<Response> {
|
||||
let state = req.get::<Read<State>>().unwrap();
|
||||
let params = match req.get::<bodyparser::Struct<GetUser>>() {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return Err(MnmlHttpError::BadRequest.into()),
|
||||
};
|
||||
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
let user = match params.id {
|
||||
Some(id) => account::select(&db, id)
|
||||
.or(Err(MnmlHttpError::NotFound))?,
|
||||
|
||||
None => match params.name {
|
||||
Some(n) => account::select_name(&db, &n)
|
||||
.or(Err(MnmlHttpError::NotFound))?,
|
||||
None => return Err(MnmlHttpError::BadRequest.into()),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(json_object(status::Ok, serde_json::to_string(&user).unwrap()))
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Deserialize)]
|
||||
struct GetGame {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
fn acp_game(req: &mut Request) -> IronResult<Response> {
|
||||
let state = req.get::<Read<State>>().unwrap();
|
||||
let params = match req.get::<bodyparser::Struct<GetGame>>() {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return Err(MnmlHttpError::BadRequest.into()),
|
||||
};
|
||||
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
let game = game::select(&db, params.id)
|
||||
.or(Err(MnmlHttpError::NotFound))?;
|
||||
|
||||
Ok(json_object(status::Ok, serde_json::to_string(&game).unwrap()))
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Deserialize)]
|
||||
struct GameList {
|
||||
number: u32,
|
||||
}
|
||||
|
||||
fn game_list(req: &mut Request) -> IronResult<Response> {
|
||||
let state = req.get::<Read<State>>().unwrap();
|
||||
let params = match req.get::<bodyparser::Struct<GameList>>() {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return Err(MnmlHttpError::BadRequest.into()),
|
||||
};
|
||||
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
let list = game::list(&db, params.number)
|
||||
.or(Err(MnmlHttpError::ServerError))?;
|
||||
|
||||
Ok(json_object(status::Ok, serde_json::to_string(&list).unwrap()))
|
||||
}
|
||||
|
||||
fn game_open(req: &mut Request) -> IronResult<Response> {
|
||||
let state = req.get::<Read<State>>().unwrap();
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
let list = game::games_need_upkeep(&mut tx)
|
||||
.or(Err(MnmlHttpError::ServerError))?;
|
||||
|
||||
tx.commit()
|
||||
.or(Err(MnmlHttpError::ServerError))?;
|
||||
|
||||
Ok(json_object(status::Ok, serde_json::to_string(&list).unwrap()))
|
||||
}
|
||||
|
||||
pub fn acp_mount() -> Chain {
|
||||
let mut router = Router::new();
|
||||
router.post("user", acp_user, "acp_user");
|
||||
router.post("game", acp_game, "acp_game");
|
||||
router.post("game/list", game_list, "acp_game_list");
|
||||
router.post("game/open", game_open, "acp_game_open");
|
||||
|
||||
let mut chain = Chain::new(router);
|
||||
chain.link_before(AcpMiddleware);
|
||||
chain
|
||||
}
|
||||
@ -12,6 +12,7 @@ use failure::Error;
|
||||
use failure::err_msg;
|
||||
|
||||
use account::Account;
|
||||
use pg::Db;
|
||||
|
||||
use construct::{Construct};
|
||||
use skill::{Skill, Cast, Resolution, Event, resolution_steps};
|
||||
@ -655,6 +656,55 @@ pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {
|
||||
return Ok(game);
|
||||
}
|
||||
|
||||
pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> {
|
||||
let query = "
|
||||
SELECT *
|
||||
FROM games
|
||||
WHERE id = $1;
|
||||
";
|
||||
|
||||
let result = db
|
||||
.query(query, &[&id])?;
|
||||
|
||||
let returned = match result.iter().next() {
|
||||
Some(row) => row,
|
||||
None => return Err(err_msg("game not found")),
|
||||
};
|
||||
|
||||
// tells from_slice to cast into a construct
|
||||
let game_bytes: Vec<u8> = returned.get("data");
|
||||
let game = from_slice::<Game>(&game_bytes)?;
|
||||
|
||||
return Ok(game);
|
||||
}
|
||||
|
||||
pub fn list(db: &Db, number: u32) -> Result<Vec<Game>, Error> {
|
||||
let query = "
|
||||
SELECT data
|
||||
FROM games
|
||||
ORDER BY created_at
|
||||
LIMIT $1;
|
||||
";
|
||||
|
||||
let result = db
|
||||
.query(query, &[&number])?;
|
||||
|
||||
let mut list = vec![];
|
||||
|
||||
for row in result.into_iter() {
|
||||
let bytes: Vec<u8> = row.get(0);
|
||||
|
||||
match from_slice::<Game>(&bytes) {
|
||||
Ok(i) => list.push(i),
|
||||
Err(e) => {
|
||||
warn!("{:?}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(list);
|
||||
}
|
||||
|
||||
pub fn games_need_upkeep(tx: &mut Transaction) -> Result<Vec<Game>, Error> {
|
||||
let query = "
|
||||
SELECT data, id
|
||||
|
||||
@ -9,15 +9,17 @@ use iron::mime::Mime;
|
||||
use iron::{typemap, BeforeMiddleware,AfterMiddleware};
|
||||
use persistent::Read;
|
||||
use router::Router;
|
||||
use mount::{Mount};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use acp;
|
||||
use account;
|
||||
use pg::PgPool;
|
||||
use payments::{stripe};
|
||||
|
||||
pub const TOKEN_HEADER: &str = "x-auth-token";
|
||||
pub const AUTH_CLEAR: &str =
|
||||
"x-auth-token=; HttpOnly; SameSite=Strict; Max-Age=-1;";
|
||||
"x-auth-token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=-1;";
|
||||
|
||||
#[derive(Clone, Copy, Fail, Debug, Serialize, Deserialize)]
|
||||
pub enum MnmlHttpError {
|
||||
@ -30,6 +32,8 @@ pub enum MnmlHttpError {
|
||||
Unauthorized,
|
||||
#[fail(display="bad request")]
|
||||
BadRequest,
|
||||
#[fail(display="not found")]
|
||||
NotFound,
|
||||
#[fail(display="account name taken or invalid")]
|
||||
AccountNameNotProvided,
|
||||
#[fail(display="account name not provided")]
|
||||
@ -58,42 +62,40 @@ impl From<postgres::Error> for MnmlHttpError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<r2d2::Error> for MnmlHttpError {
|
||||
fn from(_err: r2d2::Error) -> Self {
|
||||
MnmlHttpError::DbError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<failure::Error> for MnmlHttpError {
|
||||
fn from(_err: failure::Error) -> Self {
|
||||
fn from(err: failure::Error) -> Self {
|
||||
warn!("{:?}", err);
|
||||
MnmlHttpError::ServerError
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct JsonResponse {
|
||||
response: Option<String>,
|
||||
success: bool,
|
||||
error_message: Option<String>
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub enum Json {
|
||||
Error(String),
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl JsonResponse {
|
||||
fn success(response: String) -> Self {
|
||||
JsonResponse { response: Some(response), success: true, error_message: None }
|
||||
}
|
||||
|
||||
fn error(msg: String) -> Self {
|
||||
JsonResponse { response: None, success: false, error_message: Some(msg) }
|
||||
}
|
||||
}
|
||||
|
||||
fn iron_response (status: status::Status, message: String) -> Response {
|
||||
pub fn json_response(status: status::Status, response: Json) -> Response {
|
||||
let content_type = "application/json".parse::<Mime>().unwrap();
|
||||
let msg = match status {
|
||||
status::Ok => JsonResponse::success(message),
|
||||
_ => JsonResponse::error(message)
|
||||
};
|
||||
let msg_out = serde_json::to_string(&msg).unwrap();
|
||||
return Response::with((content_type, status, msg_out));
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
return Response::with((content_type, status, json));
|
||||
}
|
||||
|
||||
pub fn json_object(status: status::Status, object: String) -> Response {
|
||||
let content_type = "application/json".parse::<Mime>().unwrap();
|
||||
return Response::with((content_type, status, object));
|
||||
}
|
||||
|
||||
impl From<MnmlHttpError> for IronError {
|
||||
fn from(m_err: MnmlHttpError) -> Self {
|
||||
let (err, res) = match m_err {
|
||||
let (err, status) = match m_err {
|
||||
MnmlHttpError::ServerError |
|
||||
MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError),
|
||||
|
||||
@ -107,8 +109,12 @@ impl From<MnmlHttpError> for IronError {
|
||||
MnmlHttpError::InvalidCode |
|
||||
MnmlHttpError::TokenDoesNotMatch |
|
||||
MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized),
|
||||
|
||||
MnmlHttpError::NotFound => (m_err.compat(), status::NotFound),
|
||||
};
|
||||
IronError { error: Box::new(err), response: iron_response(res, m_err.to_string()) }
|
||||
|
||||
let response = json_response(status, Json::Error(m_err.to_string()));
|
||||
IronError { error: Box::new(err), response }
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,10 +169,15 @@ fn token_res(token: String) -> Response {
|
||||
let v = Cookie::build(TOKEN_HEADER, token)
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Strict)
|
||||
.path("/")
|
||||
.max_age(Duration::weeks(1)) // 1 week aligns with db set
|
||||
.finish();
|
||||
|
||||
let mut res = iron_response(status::Ok, "token_res".to_string());
|
||||
let mut res = json_response(
|
||||
status::Ok,
|
||||
Json::Message("authenticated".to_string())
|
||||
);
|
||||
|
||||
res.headers.set(SetCookie(vec![v.to_string()]));
|
||||
|
||||
return res;
|
||||
@ -220,7 +231,7 @@ fn login(req: &mut Request) -> IronResult<Response> {
|
||||
|
||||
match account::login(&mut tx, ¶ms.name, ¶ms.password) {
|
||||
Ok(a) => {
|
||||
let token = account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::ServerError))?;
|
||||
let token = account::new_token(&mut tx, a.id)?;
|
||||
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
||||
Ok(token_res(token))
|
||||
},
|
||||
@ -238,11 +249,11 @@ fn logout(req: &mut Request) -> IronResult<Response> {
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::Unauthorized))?;
|
||||
account::new_token(&mut tx, a.id)?;
|
||||
|
||||
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
||||
|
||||
let mut res = iron_response(status::Ok, "logout".to_string());
|
||||
let mut res = json_response(status::Ok, Json::Message("logged out".to_string()));
|
||||
res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()]));
|
||||
Ok(res)
|
||||
|
||||
@ -251,6 +262,33 @@ fn logout(req: &mut Request) -> IronResult<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,Deserialize)]
|
||||
struct SetPassword {
|
||||
current: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
fn set_password(req: &mut Request) -> IronResult<Response> {
|
||||
let state = req.get::<Read<State>>().unwrap();
|
||||
let params = match req.get::<bodyparser::Struct<SetPassword>>() {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return Err(IronError::from(MnmlHttpError::BadRequest)),
|
||||
};
|
||||
|
||||
match req.extensions.get::<account::Account>() {
|
||||
Some(a) => {
|
||||
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
||||
|
||||
let token = account::set_password(&mut tx, a.id, ¶ms.current, ¶ms.password)?;
|
||||
|
||||
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
||||
|
||||
Ok(token_res(token))
|
||||
},
|
||||
None => Err(IronError::from(MnmlHttpError::Unauthorized)),
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10;
|
||||
|
||||
@ -261,18 +299,33 @@ pub struct State {
|
||||
|
||||
impl Key for State { type Value = State; }
|
||||
|
||||
pub fn start(pool: PgPool) {
|
||||
fn account_mount() -> Router {
|
||||
let mut router = Router::new();
|
||||
|
||||
// auth
|
||||
router.post("/api/login", login, "login");
|
||||
router.post("/api/logout", logout, "logout");
|
||||
router.post("/api/register", register, "register");
|
||||
router.post("login", login, "login");
|
||||
router.post("logout", logout, "logout");
|
||||
router.post("register", register, "register");
|
||||
router.post("password", set_password, "set_password");
|
||||
router.post("email", logout, "email");
|
||||
|
||||
// payments
|
||||
router.post("/api/payments/stripe", stripe, "stripe");
|
||||
router
|
||||
}
|
||||
|
||||
let mut chain = Chain::new(router);
|
||||
fn payment_mount() -> Router {
|
||||
let mut router = Router::new();
|
||||
router.post("stripe", stripe, "stripe");
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
pub fn start(pool: PgPool) {
|
||||
let mut mounts = Mount::new();
|
||||
|
||||
mounts.mount("/api/account/", account_mount());
|
||||
mounts.mount("/api/payments/", payment_mount());
|
||||
mounts.mount("/api/acp/", acp::acp_mount());
|
||||
|
||||
let mut chain = Chain::new(mounts);
|
||||
chain.link(Read::<State>::both(State { pool }));
|
||||
chain.link_before(Read::<bodyparser::MaxBodyLength>::one(MAX_BODY_LENGTH));
|
||||
chain.link_before(AuthMiddleware);
|
||||
@ -396,9 +396,11 @@ impl Instance {
|
||||
|
||||
// if you don't win, you lose
|
||||
// ties can happen if both players forfeit
|
||||
// in this case we just finish the game and
|
||||
// dock them 10k mmr
|
||||
let winner_id = match game.winner() {
|
||||
Some(w) => w.id,
|
||||
None => Uuid::nil(),
|
||||
None => return Ok(self.finish()),
|
||||
};
|
||||
|
||||
for player in game.players.iter() {
|
||||
|
||||
@ -23,12 +23,14 @@ extern crate iron;
|
||||
extern crate bodyparser;
|
||||
extern crate persistent;
|
||||
extern crate router;
|
||||
extern crate mount;
|
||||
extern crate cookie;
|
||||
|
||||
extern crate ws;
|
||||
extern crate crossbeam_channel;
|
||||
|
||||
mod account;
|
||||
mod acp;
|
||||
mod construct;
|
||||
mod effect;
|
||||
mod game;
|
||||
@ -38,7 +40,7 @@ mod img;
|
||||
mod mob;
|
||||
mod mtx;
|
||||
mod names;
|
||||
mod net;
|
||||
mod http;
|
||||
mod payments;
|
||||
mod pg;
|
||||
mod player;
|
||||
@ -98,7 +100,7 @@ fn main() {
|
||||
|
||||
let pg_pool = pool.clone();
|
||||
|
||||
spawn(move || net::start(http_pool));
|
||||
spawn(move || http::start(http_pool));
|
||||
spawn(move || warden.listen());
|
||||
spawn(move || warden::upkeep_tick(warden_tick_tx));
|
||||
spawn(move || pg::listen(pg_pool, pg_events_tx));
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use rand::prelude::*;
|
||||
use rand::{thread_rng};
|
||||
|
||||
const FIRSTS: [&'static str; 47] = [
|
||||
const FIRSTS: [&'static str; 50] = [
|
||||
"artificial",
|
||||
"ambient",
|
||||
"borean",
|
||||
@ -20,10 +20,13 @@ const FIRSTS: [&'static str; 47] = [
|
||||
"fierce",
|
||||
"fossilised",
|
||||
"frozen",
|
||||
"gravitational",
|
||||
"jovian",
|
||||
"inverted",
|
||||
"leafy",
|
||||
"lurking",
|
||||
"limitless",
|
||||
"magnetic",
|
||||
"metallic",
|
||||
"mossy",
|
||||
"mighty",
|
||||
@ -37,6 +40,7 @@ const FIRSTS: [&'static str; 47] = [
|
||||
"oxygenated",
|
||||
"oscillating",
|
||||
"ossified",
|
||||
"orbiting",
|
||||
"piscine",
|
||||
"purified",
|
||||
"recalcitrant",
|
||||
@ -46,18 +50,18 @@ const FIRSTS: [&'static str; 47] = [
|
||||
"supercooled",
|
||||
"subsonic",
|
||||
"synthetic",
|
||||
"sweet",
|
||||
"terrestrial",
|
||||
"weary",
|
||||
];
|
||||
|
||||
const LASTS: [&'static str; 52] = [
|
||||
const LASTS: [&'static str; 55] = [
|
||||
"artifact",
|
||||
"assembly",
|
||||
"carbon",
|
||||
"console",
|
||||
"construct",
|
||||
"craft",
|
||||
"core",
|
||||
"design",
|
||||
"drone",
|
||||
"distortion",
|
||||
@ -77,6 +81,8 @@ const LASTS: [&'static str; 52] = [
|
||||
"lifeform",
|
||||
"landmass",
|
||||
"lens",
|
||||
"mantle",
|
||||
"magnetism",
|
||||
"mechanism",
|
||||
"mountain",
|
||||
"nectar",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::io::Read;
|
||||
|
||||
use net::State;
|
||||
use http::State;
|
||||
use iron::prelude::*;
|
||||
use iron::response::HttpResponse;
|
||||
use iron::status;
|
||||
@ -14,7 +14,7 @@ use failure::err_msg;
|
||||
|
||||
use stripe::{Event, EventObject, CheckoutSession, SubscriptionStatus};
|
||||
|
||||
use net::{MnmlHttpError};
|
||||
use http::{MnmlHttpError};
|
||||
use pg::{PgPool};
|
||||
use account;
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ use pg::{Db};
|
||||
use pg::{PgPool};
|
||||
use skill::{Skill, dev_resolve, Resolutions};
|
||||
use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip};
|
||||
use net::{AUTH_CLEAR, TOKEN_HEADER};
|
||||
use http::{AUTH_CLEAR, TOKEN_HEADER};
|
||||
|
||||
#[derive(Debug,Clone,Serialize,Deserialize)]
|
||||
pub enum RpcMessage {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user