Massive refactor complete

This commit is contained in:
2019-05-31 13:44:01 +10:00
parent b97d8fc184
commit 4a2bfb3db9
47 changed files with 1223 additions and 991 deletions
+1
View File
@@ -18,6 +18,7 @@ let excluded = [ //messages that should not be logged
'idle',
'Combat log sent',
'News sent',
'Can\'t train while attacking',
'Can\'t untrain while attacking'
+34
View File
@@ -0,0 +1,34 @@
14 Hours Of Refactoring And Debugging
---
_31 May 2019_
What started as a relatively simple task - raising state out of the profile page and into the redux store - turned into a massive overhaul of the entire project. Almost every file has been poured over with a fine toothed come to ensure that it works just right, and that the coding standards are up to scratch with the rest of the project.
I started working at midnight, which is typical for me lately but I'm still working on it now at 2pm. This was... quite an experience. Also, the mere fact that I'm still excited about this project after this ordeal shows that there is definitely something worth working on here.
I began working on equipment before the refactoring, and the equipment was passed over during it, simply because I haven't finished it yet. However, now that the profile is stored in redux, I should be able to access information such as the scientist count much more easily. So, look forward to the equipment, coming... tomorrow probably. I'm about to get some sleep.
```js
{
"Weapons": {
"Stick": { "cost": 50, "combatBoost": 0.02, "scientistsRequired": 1 },
"Dagger": { "cost": 75, "combatBoost": 0.03, "scientistsRequired": 2 },
"Sword": { "cost": 100, "combatBoost": 0.04, "scientistsRequired": 3 },
"Longsword": { "cost": 150, "combatBoost": 0.05, "scientistsRequired": 4 },
"Frying Pan": { "cost": 200, "combatBoost": 0.06, "scientistsRequired": 5 }
},
"Armour": {
"Leather": { "cost": 75, "combatBoost": 0.02, "scientistsRequired": 2 },
"Gambeson": { "cost": 100, "combatBoost": 0.03, "scientistsRequired": 3 },
"Chainmail": { "cost": 150, "combatBoost": 0.04, "scientistsRequired": 4 },
"Platemail": { "cost": 200, "combatBoost": 0.05, "scientistsRequired": 5 }
}
}
```
In the mean time, here's the placeholder data for the equipment! I probably shouldn't be showing this, but meh. It's stored as a JSON file, rather than in the database, to make it faster to edit. Player inventories will be stored in the database, however.
Oh, also, who would like a public-facing API? I'm not sure what for though, but still...
P.S. The wait time between recruitments has been lowered to <span style="color:red">20 hours</span>.
+7
View File
@@ -267,3 +267,10 @@ footer {
.minWidth { /* hacky */
min-width: 80px;
}
pre {
color: pink;
background-color: #222222;
margin-top: 1em;
margin-bottom: 1em;
}
+70 -51
View File
@@ -4,7 +4,7 @@ require('dotenv').config();
//libraries
let bcrypt = require('bcrypt');
let formidable = require('formidable');
let sendmail = require('sendmail')();
let sendmail = require('sendmail')({silent: true});
//utilities
let { log, validateEmail } = require('../common/utilities.js');
@@ -20,7 +20,7 @@ const signupRequest = (connection) => (req, res) => {
//prevent too many clicks
if (isThrottled(fields.email)) {
res.status(400).write(log('signup throttled', fields.email));
res.status(400).write(log('Signup throttled', fields.email));
res.end();
return;
}
@@ -65,11 +65,15 @@ const signupRequest = (connection) => (req, res) => {
connection.query(query, [fields.email, fields.username, salt, hash, rand], (err) => {
if (err) throw err;
//TODO: make the verification email prettier
//build the verification email
let addr = `http://${process.env.WEB_ADDRESS}/verifyrequest?email=${fields.email}&verify=${rand}`;
let msg = 'Hello! Please visit the following address to verify your account: ';
let msgHtml = `<html><body><p>${msg}<a href='${addr}'>${addr}</a></p></body></html>`;
//BUGFIX: is gmail being cruel?
let sentinel = false;
//send the verification email
sendmail({
from: `signup@${process.env.WEB_ADDRESS}`,
@@ -78,32 +82,39 @@ const signupRequest = (connection) => (req, res) => {
text: msg + addr,
html: msgHtml
}, (err, reply) => {
//final check
if (err) {
res.status(400).write(log('Something went wrong (did you use a valid email?)', err));
res.end();
return;
}
if (err) { //final check
let msg = log('Something went wrong (did you use a valid email?)', err);
res.status(200).write(log('Verification email sent!', fields.email));
if (!sentinel) {
res.status(400).write(msg);
res.end();
}
} else {
let msg = log('Verification email sent!', fields.email);
if (!sentinel) {
res.status(200).json({ msg: msg });
res.end();
}
}
sentinel = true;
});
});
});
});
});
});
}
};
const verifyRequest = (connection) => (req, res) => {
//get the saved data
let query = 'SELECT email, username, salt, hash, verify FROM signups WHERE email = ?;';
let query = 'SELECT * FROM signups WHERE email = ?;';
connection.query(query, [req.query.email], (err, results) => {
if (err) throw err;
//correct number of results
if (results.length != 1) {
if (results.length !== 1) {
console.log(req.query.email);
res.status(400).write(log('That account does not exist or this link has already been used.', req.query.email, req.query.verify));
res.end();
return;
@@ -126,12 +137,13 @@ const verifyRequest = (connection) => (req, res) => {
connection.query(query, [results[0].email], (err) => {
if (err) throw err;
//TODO: prettier verification page
res.status(200).write(log('Verification succeeded!', req.query.email));
res.end();
});
});
});
}
};
const loginRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -155,7 +167,7 @@ const loginRequest = (connection) => (req, res) => {
//found this email?
if (results.length === 0) {
res.status(400).write(log('Incorrect email or password', fields.email, 'Did not find this email'));
res.status(400).write(log('Incorrect email or password', fields.email, 'Did not find this email')); //NOTE: deliberately obscure incorrect email or password
res.end();
return;
}
@@ -183,25 +195,25 @@ const loginRequest = (connection) => (req, res) => {
id: results[0].id,
email: fields.email,
username: results[0].username,
token: rand
token: rand,
msg: log('Logged in', fields.email, rand)
});
res.end();
log('Logged in', fields.email, rand);
});
});
});
});
}
};
const logoutRequest = (connection) => (req, res) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId IN (SELECT accounts.id FROM accounts WHERE email = ?) AND token = ?;';
connection.query(query, [req.body.email, req.body.token], (err) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId = ? AND token = ?;'; //NOTE: The user now loses this access token
connection.query(query, [req.body.id, req.body.token], (err) => {
if (err) throw err;
log('Logged out', req.body.email, req.body.token);
log('Logged out', req.body.id, req.body.token);
});
res.end();
}
res.end(); //NOTE: don't send a response
};
const passwordChangeRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -212,23 +224,19 @@ const passwordChangeRequest = (connection) => (req, res) => {
if (err) throw err;
//validate password, retype
if (!validateEmail(fields.email) || fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid password change data', fields.email));
if (fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid password change data', fields.id));
res.end();
return;
}
//validate token
query = 'SELECT sessions.token FROM sessions WHERE sessions.accountId IN (SELECT id FROM accounts WHERE email = ?);';
connection.query(query, [fields.email], (err, results) => {
query = 'SELECT COUNT(*) AS total FROM sessions WHERE sessions.accountId = ? AND sessions.token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
if (err) throw err;
let found = false;
results.map((result) => { if (result.token == fields.token) found = true; });
if (!found) {
res.status(400).write(log('Invalid password change authentication', fields.email, fields.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid password change credentials', fields.id, fields.token));
res.end();
return;
}
@@ -239,26 +247,26 @@ const passwordChangeRequest = (connection) => (req, res) => {
bcrypt.hash(fields.password, salt, (err, hash) => {
if (err) throw err;
let query = 'UPDATE accounts SET salt = ?, hash = ? WHERE email = ?;';
connection.query(query, [salt, hash, fields.email], (err) => {
let query = 'UPDATE accounts SET salt = ?, hash = ? WHERE id = ?;';
connection.query(query, [salt, hash, fields.id], (err) => {
if (err) throw err;
//clear all session data for this user (a 'feature')
let query = 'DELETE FROM sessions WHERE sessions.accountId IN (SELECT accounts.id FROM accounts WHERE email = ?);';
connection.query(query, [fields.email], (err) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId = ?;';
connection.query(query, [fields.id], (err) => {
if (err) throw err;
//create the new session
let rand = Math.floor(Math.random() * 100000);
let query = 'INSERT INTO sessions (accountId, token) VALUES ((SELECT accounts.id FROM accounts WHERE email = ?), ?);';
connection.query(query, [fields.email, rand], (err) => {
let query = 'INSERT INTO sessions (accountId, token) VALUES (?, ?);';
connection.query(query, [fields.id, rand], (err) => {
if (err) throw err;
//send json containing the account info
res.status(200).json({
token: rand,
msg: log('Password changed!', fields.email)
msg: log('Password changed!', fields.id)
});
res.end();
});
@@ -268,7 +276,7 @@ const passwordChangeRequest = (connection) => (req, res) => {
});
});
});
}
};
const passwordRecoverRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -280,14 +288,14 @@ const passwordRecoverRequest = (connection) => (req, res) => {
//prevent too many clicks
if (isThrottled(fields.email)) {
res.status(400).write(log('recover throttled', fields.email));
res.status(400).write(log('Recover throttled', fields.email));
res.end();
return;
}
throttle(fields.email);
//validate email, username and password
//validate email
if (!validateEmail(fields.email)) {
res.status(400).write(log('Invalid recover data', fields.email));
res.end();
@@ -312,11 +320,15 @@ const passwordRecoverRequest = (connection) => (req, res) => {
connection.query(query, [results[0].id, rand], (err) => {
if (err) throw err;
//TODO: prettier recovery email
//build the recovery email
let addr = `http://${process.env.WEB_ADDRESS}/passwordreset?email=${fields.email}&token=${rand}`;
let msg = 'Hello! Please visit the following address to set a new password (if you didn\'t request a password recovery, ignore this email): ';
let msgHtml = `<html><body><p>${msg}<a href='${addr}'>${addr}</a></p></body></html>`;
//BUGFIX: is gmail being cruel?
let sentinel = false;
//send the verification email
sendmail({
from: `passwordrecover@${process.env.WEB_ADDRESS}`,
@@ -327,18 +339,25 @@ const passwordRecoverRequest = (connection) => (req, res) => {
}, (err, reply) => {
//final check
if (err) {
res.status(400).write(log('Something went wrong (did you use a valid email?)', err));
if (!sentinel) {
let msg = log('Something went wrong (did you use a valid email?)', err);
res.status(400).write(msg);
res.end();
}
} else {
let msg = log('Recovery email sent!', fields.email);
res.status(200).json({ msg: msg });
res.end();
return;
}
res.status(200).write(log('Recovery email sent!', fields.email));
res.end();
sentinel = true;
});
});
});
});
}
};
const passwordResetRequest = (connection) => (req, res) => {
//formidable handles forms
@@ -348,7 +367,7 @@ const passwordResetRequest = (connection) => (req, res) => {
form.parse(req, (err, fields) => {
if (err) throw err;
//validate email, username and password
//validate email and password
if (!validateEmail(fields.email) || fields.password.length < 8 || fields.password !== fields.retype) {
res.status(400).write(log('Invalid reset data (invalid email/password)', fields.email));
res.end();
@@ -383,7 +402,7 @@ const passwordResetRequest = (connection) => (req, res) => {
connection.query(query, [fields.email], (err) => {
if (err) throw err;
res.status(200).write(log('Password updated!', fields.email));
res.status(200).json({ msg: log('Password updated!', fields.email) });
res.end();
return;
});
@@ -392,7 +411,7 @@ const passwordResetRequest = (connection) => (req, res) => {
});
});
});
}
};
module.exports = {
signupRequest: signupRequest,
+41 -44
View File
@@ -8,35 +8,33 @@ let CronJob = require('cron').CronJob;
let { log } = require('../common/utilities.js');
const attackRequest = (connection) => (req, res) => {
//verify the attacker's credentials
let query = 'SELECT accountId FROM sessions WHERE accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [req.body.attacker, req.body.token], (err, results) => {
//verify the attacker's credentials (only the attacker can launch an attack)
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [req.body.id, req.body.attacker, req.body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid attack credentials', req.body.attacker, req.body.defender, req.body.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid attack credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
let attackerId = results[0].accountId;
//verify that the defender exists
let query = 'SELECT id FROM accounts WHERE username = ?;';
//verify that the defender's profile exists
let query = 'SELECT accountId FROM profiles WHERE accountId IN (SELECT id FROM accounts WHERE username = ?);';
connection.query(query, [req.body.defender], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid defender credentials', req.body.attacker, req.body.defender));
res.status(400).write(log('Invalid defender credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
let defenderId = results[0].id;
let defenderId = results[0].accountId;
//verify that the attacker has enough soldiers
let query = 'SELECT soldiers FROM profiles WHERE accountId = ?;';
connection.query(query, [attackerId], (err, results) => {
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results[0].soldiers <= 0) {
@@ -52,54 +50,57 @@ const attackRequest = (connection) => (req, res) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('You are already attacking someone', req.body.attacker, req.body.defender));
res.status(400).write(log('You are already attacking someone', req.body.id, req.body.attacker, req.body.token));
res.end();
return;
}
//create the pending attack value
//create the pending attack record
let query = 'INSERT INTO pendingCombat (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 60 * ? SECOND), ?, ?, ?);';
connection.query(query, [attackingUnits, attackerId, defenderId, attackingUnits], (err) => {
connection.query(query, [attackingUnits, req.body.id, defenderId, attackingUnits], (err) => {
if (err) throw err;
res.status(200).json({
status: 'attacking',
defender: req.body.defender
attacker: req.body.attacker,
defender: req.body.defender,
msg: log('Attacking', req.body.attacker, req.body.defender)
});
res.end();
});
});
});
});
});
};
log(`attacking ${req.body.defender}`, req.body.attacker, req.body.defender)
});
});
});
});
});
}
const attackStatusRequest = (connection) => (req, res) => {
isAttacking(connection, req.body.username, (err, attacking, defender) => {
const attackStatusRequest = (connection) => (req, res) => { //TODO: proper credentials
isAttacking(connection, req.body.attacker, (err, attacking, defender) => {
if (err) throw err;
res.status(200).json({
status: log(attacking ? 'attacking' : 'idle', req.body.username, defender),
defender: defender
status: attacking ? 'attacking' : 'idle',
attacker: req.body.attacker,
defender: defender,
msg: null
});
res.end();
});
}
};
const combatLogRequest = (connection) => (req, res) => {
let query = 'SELECT pastCombat.*, atk.username AS attackerUsername, def.username AS defenderUsername FROM pastCombat JOIN accounts AS atk ON pastCombat.attackerId = atk.id JOIN accounts AS def ON pastCombat.defenderId = def.id WHERE atk.username = ? OR def.username = ? ORDER BY eventTime DESC LIMIT ?, ?;';
let query = 'SELECT pastCombat.*, atk.username AS attacker, def.username AS defender FROM pastCombat JOIN accounts AS atk ON pastCombat.attackerId = atk.id JOIN accounts AS def ON pastCombat.defenderId = def.id WHERE atk.username = ? OR def.username = ? ORDER BY eventTime DESC LIMIT ?, ?;';
connection.query(query, [req.body.username, req.body.username, req.body.start, req.body.length], (err, results) => {
if (err) throw err;
res.status(200).json(results);
log('Combat log sent', req.body.username, req.body.start, req.body.length, JSON.stringify(results));
log('Combat log sent', req.body.username, req.body.start, req.body.length);
});
}
};
const runCombatTick = (connection) => {
//once per second
let combatTick = new CronJob('* * * * * *', () => {
//find each pending combat
let query = 'SELECT * FROM pendingCombat WHERE eventTime < CURRENT_TIMESTAMP();';
@@ -140,6 +141,7 @@ const runCombatTick = (connection) => {
}
//determine the victor
//TODO: add equipment effectiveness
let rand = Math.random() * (pendingCombat.attackingUnits + defendingUnits * (undefended ? 0.25 : 1));
let victor = rand <= pendingCombat.attackingUnits ? 'attacker' : 'defender';
@@ -147,23 +149,18 @@ const runCombatTick = (connection) => {
let spoilsGold = Math.floor(results[0].gold * (victor === 'attacker' ? 0.1 : 0.02));
let casualtiesVictor = Math.floor((pendingCombat.attackingUnits >= 10 ? pendingCombat.attackingUnits - 10 : 0) * (victor === 'attacker' ? 0.05 : 0.1));
//NOTE: there is a negative gold bug somewhere
if (spoilsGold <= 0) {
log('WARNING: spoilsGold <= 0', pendingCombat.attackerId, pendingCombat.defenderId, spoilsGold);
}
//save the combat
let query = 'INSERT INTO pastCombat (eventTime, attackerId, defenderId, attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);';
connection.query(query, [pendingCombat.eventTime, pendingCombat.attackerId, pendingCombat.defenderId, pendingCombat.attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor], (err) => {
if (err) throw err;
//update the attacker profile
let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE id = ?;';
let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, casualtiesVictor, pendingCombat.attackerId], (err) => {
if (err) throw err;
//update the defender profile
let query = 'UPDATE profiles SET gold = gold - ? WHERE id = ?;';
let query = 'UPDATE profiles SET gold = gold - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, pendingCombat.defenderId], (err) => {
if (err) throw err;
@@ -185,12 +182,12 @@ const runCombatTick = (connection) => {
});
combatTick.start();
}
};
const isNormalInteger = (str) => {
let n = Math.floor(Number(str));
return n !== Infinity && String(n) == str && n >= 0;
}
};
const isAttacking = (connection, user, cb) => {
let query;
@@ -200,7 +197,7 @@ const isAttacking = (connection, user, cb) => {
} else if (typeof(user) === 'string') {
query = 'SELECT * FROM pendingCombat WHERE attackerId IN (SELECT id FROM accounts WHERE username = ?);';
} else {
return cb('Unknown argument type for user');
return cb(`Unknown argument type for user: ${typeof(user)}`);
}
connection.query(query, [user], (err, results) => {
@@ -217,7 +214,7 @@ const isAttacking = (connection, user, cb) => {
});
}
});
}
};
module.exports = {
attackRequest: attackRequest,
@@ -225,4 +222,4 @@ module.exports = {
combatLogRequest: combatLogRequest,
runCombatTick: runCombatTick,
isAttacking: isAttacking
}
};
+2 -2
View File
@@ -42,8 +42,8 @@ function handleDisconnect() {
//finally
return connection;
}
};
module.exports = {
connectToDatabase: handleDisconnect
}
};
+4 -2
View File
@@ -10,7 +10,9 @@ let equipmentStatistics = require('./equipment_statistics.json');
const statisticsRequest = () => (req, res) => {
res.status(200).json(equipmentStatistics);
res.end();
}
};
//TODO: incomplete
const listRequest = (connection) => (req, res) => {
//verify identity
@@ -37,7 +39,7 @@ const listRequest = (connection) => (req, res) => {
res.end();
});
});
}
};
module.exports = {
statisticsRequest: statisticsRequest,
+9 -9
View File
@@ -1,15 +1,15 @@
{
"Weapons": {
"Stick": { "cost": 50, "combatBoost": 0.02, "scientists": 1 },
"Dagger": { "cost": 75, "combatBoost": 0.03, "scientists": 2 },
"Sword": { "cost": 100, "combatBoost": 0.04, "scientists": 3 },
"Longsword": { "cost": 150, "combatBoost": 0.05, "scientists": 4 },
"Frying Pan": { "cost": 200, "combatBoost": 0.06, "scientists": 5 }
"Stick": { "cost": 50, "combatBoost": 0.02, "scientistsRequired": 1 },
"Dagger": { "cost": 75, "combatBoost": 0.03, "scientistsRequired": 2 },
"Sword": { "cost": 100, "combatBoost": 0.04, "scientistsRequired": 3 },
"Longsword": { "cost": 150, "combatBoost": 0.05, "scientistsRequired": 4 },
"Frying Pan": { "cost": 200, "combatBoost": 0.06, "scientistsRequired": 5 }
},
"Armour": {
"Leather": { "cost": 75, "combatBoost": 0.02, "scientists": 2 },
"Gambeson": { "cost": 100, "combatBoost": 0.03, "scientists": 3 },
"Chainmail": { "cost": 150, "combatBoost": 0.04, "scientists": 4 },
"Platemail": { "cost": 200, "combatBoost": 0.05, "scientists": 5 }
"Leather": { "cost": 75, "combatBoost": 0.02, "scientistsRequired": 2 },
"Gambeson": { "cost": 100, "combatBoost": 0.03, "scientistsRequired": 3 },
"Chainmail": { "cost": 150, "combatBoost": 0.04, "scientistsRequired": 4 },
"Platemail": { "cost": 200, "combatBoost": 0.05, "scientistsRequired": 5 }
}
}
+4 -3
View File
@@ -15,6 +15,7 @@ app.use(bodyParser.json());
//handle the news request
let news = require('./news.js');
app.get('/newsrequest', news.newsRequest());
app.post('/newsrequest', news.newsRequest());
//database
@@ -46,9 +47,9 @@ app.post('/attackstatusrequest', combat.attackStatusRequest(connection));
app.post('/combatlogrequest', combat.combatLogRequest(connection));
combat.runCombatTick(connection);
let equipment = require('./equipment.js');
app.post('/equipmentstatisticsrequest', equipment.statisticsRequest());
app.post('/equipmentlistrequest', equipment.listRequest(connection));
//let equipment = require('./equipment.js');
//app.post('/equipmentstatisticsrequest', equipment.statisticsRequest());
//app.post('/equipmentlistrequest', equipment.listRequest(connection));
//static directories
app.use('/styles', express.static(path.resolve(__dirname + '/../public/styles')) );
+9 -5
View File
@@ -5,18 +5,20 @@ require('dotenv').config();
let fs = require('fs');
let path = require('path');
let { log } = require('../common/utilities.js');
const newsRequest = () => (req, res) => {
let fpath = path.join(__dirname, '..', 'public', 'news');
let fileNames = fs.readdirSync(fpath);
//set the maximum
let max = parseInt(req.body.max);
if (max > fileNames.length) {
let max = parseInt(req.body.length) || 99;
if (isNaN(max) || max > fileNames.length) {
max = fileNames.length;
}
//build the object to send
let json = {} //TODO: caching
let json = {}; //TODO: caching
//send each file as json
for (let i = 0; i < max; i++) {
@@ -26,8 +28,10 @@ const newsRequest = () => (req, res) => {
//actually send the data
res.json(json);
res.end();
}
log('News sent', max, fileNames, JSON.stringify(json));
};
module.exports = {
newsRequest: newsRequest
}
};
+98 -135
View File
@@ -2,7 +2,6 @@
require('dotenv').config();
//libraries
let formidable = require('formidable');
let CronJob = require('cron').CronJob;
let { isAttacking } = require('./combat.js');
@@ -12,87 +11,73 @@ let { log } = require('../common/utilities.js');
//profile creation & requesting
const profileCreateRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form TODO: form? That was a bad idea
form.parse(req, (err, fields) => {
if (err) throw err;
//separate this section so it can be used elsewhere too
return profileCreateRequestInner(connection, req, res, fields);
});
}
return profileCreateRequestInner(connection, req, res, req.body);
};
function profileCreateRequestInner(connection, req, res, fields) {
function profileCreateRequestInner(connection, req, res, body) {
let query = 'SELECT accountId FROM profiles WHERE accountId IN (SELECT accounts.id FROM accounts WHERE username = ?);';
connection.query(query, [fields.username], (err, results) => {
connection.query(query, [body.username], (err, results) => {
if (err) throw err;
if (results.length === 1) {
res.status(400).write(log('That profile already exists', fields.username));
res.status(400).write(log('That profile already exists', body.username));
res.end();
return;
}
//check ID, username and token match
//check ID, username and token match (only the profile's owner can create it)
let query = 'SELECT accountId FROM sessions WHERE accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [fields.username, fields.token], (err, results) => {
connection.query(query, [body.username, body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1 || results[0].accountId != fields.id) {
res.status(400).write(log('Invalid profile creation credentials', fields.username, fields.id, fields.token));
if (results.length !== 1 || results[0].accountId != body.id) {
res.status(400).write(log('Invalid profile creation credentials', body.username, body.id, body.token));
res.end();
return;
}
//create the profile
let query = 'INSERT INTO profiles (accountId) SELECT accounts.id FROM accounts WHERE username = ?;';
connection.query(query, [fields.username], (err) => {
connection.query(query, [body.username], (err) => {
if (err) throw err;
log('Profile created', fields.username, fields.id, fields.token);
log('Profile created', body.username, body.id, body.token);
return profileRequestInner(connection, req, res, fields);
return profileRequestInner(connection, req, res, body);
});
});
});
}
const profileRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//separate this section so it can be used elsewhere too
return profileRequestInner(connection, req, res, fields);
});
};
function profileRequestInner(connection, req, res, fields) {
const profileRequest = (connection) => (req, res) => {
//separate this section so it can be used elsewhere too
return profileRequestInner(connection, req, res, req.body);
};
function profileRequestInner(connection, req, res, body) {
//find the profile
let query = 'SELECT * FROM profiles WHERE accountId IN (SELECT accounts.id FROM accounts WHERE username = ?);';
connection.query(query, [fields.username], (err, results) => {
connection.query(query, [body.username], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
//pass it off to the profile creation process, IF the user is requesting their own profile
let query = 'SELECT id FROM accounts WHERE id = ? AND id IN (SELECT accountId FROM sessions WHERE token = ?);';
connection.query(query, [fields.id, fields.token], (err, results) => {
connection.query(query, [body.id, body.token], (err, results) => {
if (err) throw err;
if (results.length === 1) {
return profileCreateRequestInner(connection, req, res, fields);
return profileCreateRequestInner(connection, req, res, body);
} else {
res.status(400).write(log('Profile not found', fields.username, fields.id, fields.token));
res.status(400).write(log('Profile not found', body.username, body.id, body.token));
res.end();
}
});
} else {
//results.length === 1
res.status(200).json({
username: fields.username,
username: body.username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -100,38 +85,31 @@ function profileRequestInner(connection, req, res, fields) {
scientists: results[0].scientists
});
res.end();
log('Profile sent', fields.username, fields.id, fields.token);
log('Profile sent', body.username, body.id, body.token);
}
});
}
};
//actual actions to be taken
const recruitRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//verify the credentials
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid recruit credentials - 1', req.body.id, req.body.token));
res.end();
return;
}
//verify enough time has passed since the last successful recruit action
let query = 'SELECT TIMESTAMPDIFF(HOUR, (SELECT lastRecruitTime FROM profiles WHERE accountId = ?), CURRENT_TIMESTAMP());';
connection.query(query, [fields.id], (err, results) => {
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid database state', fields.username, fields.id, fields.token));
res.status(400).write(log('Invalid database state', req.body.id, req.body.token));
res.end();
return;
}
@@ -139,32 +117,32 @@ const recruitRequest = (connection) => (req, res) => {
let timespans = results[0][Object.keys(results[0])];
//not enough time has passed
if (timespans < 22) {
res.status(400).write(log('Not enough time has passed', fields.username, fields.id, fields.token));
if (timespans < 20) {
res.status(400).write(log('Not enough time has passed', req.body.id, req.body.token));
res.end();
return;
}
//update the profile with the new data (gaining 1 recruit)
let query = 'UPDATE profiles SET recruits = recruits + 1, lastRecruitTime = CURRENT_TIMESTAMP() WHERE accountId = ?;';
connection.query(query, [fields.id], (err) => {
connection.query(query, [req.body.id], (err) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
//send the new profile data as JSON
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
res.status(400).write(log('Invalid recruit credentials - 2', req.body.id, req.body.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -172,53 +150,46 @@ const recruitRequest = (connection) => (req, res) => {
scientists: results[0].scientists
});
res.end();
log('Recruit successful', fields.username, fields.id, fields.token);
log('Recruit successful', results[0].username, req.body.id, req.body.token);
});
});
});
});
});
}
};
const trainRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//verify the credentials (NOTE: duplication)
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid train credentials', fields.username, fields.id, fields.token));
if (results[0].total !== 1) {
res.status(400).write(log('Invalid train credentials - 1', req.body.id, req.body.token));
res.end();
return;
}
//verify the role argument
if (fields.role !== 'soldier' && fields.role !== 'spy' && fields.role !== 'scientist') {
res.status(400).write(log('Invalid train parameters', fields.username, fields.role, fields.id, fields.token));
if (req.body.role !== 'soldier' && req.body.role !== 'spy' && req.body.role !== 'scientist') {
res.status(400).write(log('Invalid train parameters', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//can't train while attacking
isAttacking(connection, fields.id, (err, attacking) => {
isAttacking(connection, req.body.id, (err, attacking) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('Can\'t train while attacking', fields.id));
res.status(400).write(log('Can\'t train while attacking', req.body.id));
res.end();
return;
}
//determine the cost of the training TODO: make these global for the client too
let cost = 0;
switch(fields.role) {
switch(req.body.role) {
case 'soldier':
cost = 100;
break;
@@ -234,41 +205,41 @@ const trainRequest = (connection) => (req, res) => {
//verify that the user has a high enough gold and recruit balance
let query = 'SELECT recruits, gold FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results[0].recruits <= 0) {
res.status(400).write(log('Not enough recruits', fields.username, results[0].recruits, fields.id, fields.token));
res.status(400).write(log('Not enough recruits', results[0].recruits, req.body.id, req.body.token));
res.end();
return;
}
if (results[0].gold < cost) {
res.status(400).write(log('Not enough gold', fields.username, results[0].gold, fields.id, fields.token));
res.status(400).write(log('Not enough gold', results[0].gold, req.body.id, req.body.token));
res.end();
return;
}
//update the profile with new values
//update the profile with new values (NOTE: extra protection for network latency)
let query = 'UPDATE profiles SET gold = gold - ?, recruits = recruits - 1, soldiers = soldiers + ?, spies = spies + ?, scientists = scientists + ? WHERE accountId = ? AND gold >= ? AND recruits > 0;';
connection.query(query, [cost, fields.role === 'soldier' ? 1 : 0, fields.role === 'spy' ? 1 : 0, fields.role === 'scientist' ? 1 : 0, fields.id, cost], (err) => {
connection.query(query, [cost, req.body.role === 'soldier' ? 1 : 0, req.body.role === 'spy' ? 1 : 0, req.body.role === 'scientist' ? 1 : 0, req.body.id, cost], (err) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid recruit credentials', fields.username, fields.id, fields.token));
res.status(400).write(log('Invalid recruit credentials - 2', req.body.id, req.body.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -276,108 +247,101 @@ const trainRequest = (connection) => (req, res) => {
scientists: results[0].scientists
});
res.end();
log('Train executed', fields.username, fields.role, fields.id, fields.token);
log('Train executed', results[0].username, req.body.role, req.body.id, req.body.token);
});
});
});
});
});
});
}
};
const untrainRequest = (connection) => (req, res) => {
//formidable handles forms
let form = formidable.IncomingForm();
//parse form
form.parse(req, (err, fields) => {
if (err) throw err;
//verify the credentials (NOTE: duplication)
let query = 'SELECT accountId FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [fields.id, fields.token], (err, results) => {
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials', fields.username, fields.role, fields.id, fields.token));
res.status(400).write(log('Invalid untrain credentials - 1', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//verify the role argument
if (fields.role !== 'soldier' && fields.role !== 'spy' && fields.role !== 'scientist') {
res.status(400).write(log('Invalid untrain parameters', fields.username, fields.role, fields.id, fields.token));
if (req.body.role !== 'soldier' && req.body.role !== 'spy' && req.body.role !== 'scientist') {
res.status(400).write(log('Invalid untrain parameters', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//can't untrain while attacking
isAttacking(connection, fields.id, (err, attacking) => {
isAttacking(connection, req.body.id, (err, attacking) => {
if (err) throw err;
if (attacking) {
res.status(400).write(log('Can\'t untrain while attacking', fields.id));
res.status(400).write(log('Can\'t untrain while attacking', req.body.id, req.body.token));
res.end();
return;
}
//verify that the user has a high enough balance
let query = 'SELECT soldiers, spies, scientists FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (fields.role === 'soldier' && results[0].soldiers <= 0) {
res.status(400).write(log('Not enough soldiers', fields.username, results[0].soldiers, fields.id, fields.token));
if (req.body.role === 'soldier' && results[0].soldiers <= 0) {
res.status(400).write(log('Not enough soldiers', results[0].soldiers, req.body.id, req.body.token));
res.end();
return;
}
if (fields.role === 'spy' && results[0].spies <= 0) {
res.status(400).write(log('Not enough spies', fields.username, results[0].spies, fields.id, fields.token));
if (req.body.role === 'spy' && results[0].spies <= 0) {
res.status(400).write(log('Not enough spies', results[0].spies, req.body.id, req.body.token));
res.end();
return;
}
if (fields.role === 'scientist' && results[0].scientists <= 0) {
res.status(400).write(log('Not enough scientists', fields.username, results[0].scientists, fields.id, fields.token));
if (req.body.role === 'scientist' && results[0].scientists <= 0) {
res.status(400).write(log('Not enough scientists', results[0].scientists, req.body.id, req.body.token));
res.end();
return;
}
//hacky
let role = null;
if (fields.role === 'soldier') {
role = 'soldiers';
} else if (fields.role === 'spy') {
role = 'spies';
} else if (fields.role === 'scientist') {
role = 'scientists';
let roleName = null;
if (req.body.role === 'soldier') {
roleName = 'soldiers';
} else if (req.body.role === 'spy') {
roleName = 'spies';
} else if (req.body.role === 'scientist') {
roleName = 'scientists';
} else {
res.status(400).write(log('Unknown role found', fields.role));
res.status(400).write(log('Unknown role received', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//update the profile with new values
let query = `UPDATE profiles SET recruits = recruits + 1, soldiers = soldiers - ?, spies = spies - ?, scientists = scientists - ? WHERE accountId = ? AND ${role} > 0;`;
connection.query(query, [fields.role === 'soldier' ? 1 : 0, fields.role === 'spy' ? 1 : 0, fields.role === 'scientist' ? 1 : 0, fields.id], (err) => {
//update the profile with new values (NOTE: extra protection for network latency)
let query = `UPDATE profiles SET recruits = recruits + 1, soldiers = soldiers - ?, spies = spies - ?, scientists = scientists - ? WHERE accountId = ? AND ${roleName} > 0;`;
connection.query(query, [roleName === 'soldiers' ? 1 : 0, roleName === 'spies' ? 1 : 0, roleName === 'scientists' ? 1 : 0, req.body.id], (err) => {
if (err) throw err;
//send the new profile data as JSON (NOTE: possible duplication)
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [fields.id], (err, results) => {
let query = 'SELECT username, profiles.* FROM profiles JOIN accounts ON accounts.id = profiles.accountId WHERE accounts.id = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
//check just in case
if (results.length !== 1) {
res.status(400).write(log('Invalid untrain credentials', fields.username, fields.role, fields.id, fields.token));
res.status(400).write(log('Invalid untrain credentials - 2', req.body.role, req.body.id, req.body.token));
res.end();
return;
}
//results.length === 1
res.status(200).json({
username: fields.username, //TODO: join here
username: results[0].username,
gold: results[0].gold,
recruits: results[0].recruits,
soldiers: results[0].soldiers,
@@ -385,14 +349,13 @@ const untrainRequest = (connection) => (req, res) => {
scientists: results[0].scientists
});
res.end();
log('Untrain executed', fields.username, fields.role, fields.id, fields.token);
log('Untrain executed', results[0].username, roleName, req.body.id, req.body.token);
});
});
});
});
});
});
}
};
const ladderRequest = (connection) => (req, res) => {
let query = 'SELECT username, soldiers, recruits, gold FROM accounts JOIN profiles ON accounts.id = profiles.accountId ORDER BY soldiers DESC, recruits DESC, gold DESC LIMIT ?, ?;';
@@ -402,7 +365,7 @@ const ladderRequest = (connection) => (req, res) => {
res.status(200).json(results);
log('Ladder sent', req.body.start, req.body.length, results);
});
}
};
const runGoldTick = (connection) => {
let goldTickJob = new CronJob('0 */30 * * * *', () => {
@@ -415,7 +378,7 @@ const runGoldTick = (connection) => {
});
goldTickJob.start();
}
};
module.exports = {
// profileCreate: profileCreate, //NOTE: Not actually used
@@ -425,4 +388,4 @@ module.exports = {
untrainRequest: untrainRequest,
ladderRequest: ladderRequest,
runGoldTick: runGoldTick
}
};
+3
View File
@@ -1,3 +1,6 @@
DROP TABLE equipment;
DROP TABLE pastCombat;
DROP TABLE pendingCombat;
DROP TABLE profiles;
DROP TABLE passwordRecover;
DROP TABLE sessions;
@@ -1,8 +1,8 @@
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export const SESSIONCHANGE = 'SESSIONCHANGE';
export const SESSION_CHANGE = 'SESSION_CHANGE';
export function login(id, email, username, token) {
export const login = (id, email, username, token) => {
return {
type: LOGIN,
id: id,
@@ -12,15 +12,15 @@ export function login(id, email, username, token) {
};
}
export function logout() {
export const logout = () => {
return {
type: LOGOUT
};
}
export function sessionChange(token) {
export const sessionChange = (token) => {
return {
type: SESSIONCHANGE,
type: SESSION_CHANGE,
token: token
};
}
-8
View File
@@ -1,8 +0,0 @@
export const SET_ATTACK_DISABLED = 'SET_ATTACK_DISABLED';
export function setAttackDisabled(disabled) {
return {
type: SET_ATTACK_DISABLED,
disabled: disabled
}
}
+61
View File
@@ -0,0 +1,61 @@
export const STORE_PROFILE = 'STORE_PROFILE';
export const STORE_USERNAME = 'STORE_USERNAME';
export const STORE_GOLD = 'STORE_GOLD';
export const STORE_RECRUITS = 'STORE_RECRUITS';
export const STORE_SOLDIERS = 'STORE_SOLDIERS';
export const STORE_SPIES = 'STORE_SPIES';
export const STORE_SCIENTISTS = 'STORE_SCIENTISTS';
export const storeProfile = (username, gold, recruits, soldiers, spies, scientists) => {
return {
type: STORE_PROFILE,
username: username,
gold: gold,
recruits: recruits,
soldiers: soldiers,
spies: spies,
scientists: scientists
};
}
export const storeUsername = (username) => {
return {
type: STORE_USERNAME,
username: username
};
}
export const storeGold = (gold) => {
return {
type: STORE_GOLD,
gold: gold
};
}
export const storeRecruits = (recruits) => {
return {
type: STORE_RECRUITS,
recruits: recruits
};
}
export const storeSoldiers = (soldiers) => {
return {
type: STORE_SOLDIERS,
soldiers: soldiers
};
}
export const storeSpies = (spies) => {
return {
type: STORE_SPIES,
spies: spies
};
}
export const storeScientists = (scientists) => {
return {
type: STORE_SCIENTISTS,
scientists: scientists
};
}
+2
View File
@@ -59,6 +59,7 @@ export default class App extends React.Component {
{ GA.init() && <GA.RouteTracker /> }
<Switch>
<LazyRoute exact path='/' component={() => import('./pages/home.jsx')} />
<LazyRoute path='/signup' component={() => import('./pages/signup.jsx')} />
<LazyRoute path='/login' component={() => import('./pages/login.jsx')} />
<LazyRoute path='/passwordchange' component={() => import('./pages/password_change.jsx')} />
@@ -67,6 +68,7 @@ export default class App extends React.Component {
<LazyRoute path='/profile' component={() => import('./pages/profile.jsx')} />
<LazyRoute path='/ladder' component={() => import('./pages/ladder.jsx')} />
<LazyRoute path='/combatlog' component={() => import('./pages/combat_log.jsx')} />
<LazyRoute path='*' component={() => import('./pages/page_not_found.jsx')} />
</Switch>
+147
View File
@@ -0,0 +1,147 @@
import React from 'react';
import { connect } from 'react-redux';
import queryString from 'query-string';
import PropTypes from 'prop-types';
//panels
import CommonLinks from '../panels/common_links.jsx';
import PagedCombatLog from '../panels/paged_combat_log.jsx';
class CombatLog extends React.Component {
constructor(props) {
super(props);
let params = queryString.parse(props.location.search);
this.state = {
params: params,
start: parseInt(params.log) || 0,
length: parseInt(params.length) || 20,
fetch: null,
warning: ''
};
}
componentDidMount() {
if (!this.props.loggedIn) {
this.props.history.replace('/login');
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (JSON.stringify(this.state) !== JSON.stringify(prevState)) {
this.state.fetch();
}
}
render() {
let warningStyle = {
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
let ButtonHeader = this.buttonHeader.bind(this);
return (
<div className='panel'>
<div className='sidePanelPage'>
<div className='sidePanel'>
<CommonLinks />
</div>
<div className='mainPanel'>
<div className='warning' style={warningStyle}>
<p>{this.state.warning}</p>
</div>
<ButtonHeader />
<PagedCombatLog
setWarning={this.setWarning.bind(this)}
username={this.props.username}
start={this.state.start}
length={this.state.length}
getFetch={this.getFetch.bind(this)}
onReceived={this.onReceived.bind(this)}
/>
<ButtonHeader />
</div>
</div>
</div>
);
}
buttonHeader() {
return (
<div className='table'>
<div className='row'>
<button className='col' onClick={ this.decrement.bind(this) }>{'< Back'}</button>
<div className='col' />
<div className='col' />
<button className='col' onClick={ this.increment.bind(this) }>{'Next >'}</button>
</div>
</div>
);
}
increment() {
let start = this.state.start + this.state.length;
this.props.history.push(`${this.props.location.pathname}?log=${start}`);
}
decrement() {
let start = Math.max(0, this.state.start - this.state.length);
//don't decrement too far
if (start === this.state.start) {
return;
}
this.props.history.push(`${this.props.location.pathname}?log=${start}`);
}
//bound callbacks
getFetch(fn) {
this.setState({ fetch: fn });
}
onReceived(data) {
if (data.length === 0) {
let start = Math.max(0, this.state.start - this.state.length);
//don't decrement too far
if (start === this.state.start) {
return;
}
this.props.history.replace(`${this.props.location.pathname}?log=${start}`);
}
}
setWarning(s) {
this.setState({ warning: s });
}
};
CombatLog.propTypes = {
username: PropTypes.string.isRequired
};
const mapStoreToProps = (store) => {
return {
username: store.account.username,
loggedIn: store.account.id !== 0
};
};
const mapDispatchToProps = (dispatch) => {
return {
//
};
};
CombatLog = connect(mapStoreToProps, mapDispatchToProps)(CombatLog);
export default CombatLog;
+20 -21
View File
@@ -1,23 +1,28 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter, Link } from 'react-router-dom';
//panels
import CommonLinks from '../panels/common_links.jsx';
import Blurb from '../panels/blurb.jsx';
import NewsPanel from '../panels/news_panel.jsx';
import News from '../panels/news.jsx';
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
//
warning: '', //TODO: unified warning?
fetch: null
};
}
//rendering function
componentDidUpdate(prevProps, prevState, snapshot) {
this.state.fetch();
}
render() {
//return the home page
let warningStyle = {
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
return (
<div className='page'>
<div className='sidePanelPage'>
@@ -26,29 +31,23 @@ class Home extends React.Component {
</div>
<div className='mainPanel'>
<div className='warning' style={warningStyle}>
<p>{this.state.warning}</p>
</div>
<h1 className='centered'>About</h1>
<Blurb />
<h1 className='centered'>News</h1>
<NewsPanel />
<News setWarning={this.setWarning.bind(this)} getFetch={ (fn) => this.setState({ fetch: fn }) } />
</div>
</div>
</div>
);
}
}
function mapStoreToProps(store) {
return {
id: store.account.id
setWarning(s) {
this.setState({ warning: s });
}
}
};
function mapDispatchToProps(dispatch) {
return {
//
}
}
Home = connect(mapStoreToProps, mapDispatchToProps)(Home);
export default withRouter(Home);
export default Home;
+8 -4
View File
@@ -1,6 +1,5 @@
import React from 'react';
import queryString from 'query-string';
import { withRouter } from 'react-router-dom';
import CommonLinks from '../panels/common_links.jsx';
import PagedLadder from '../panels/paged_ladder.jsx';
@@ -38,7 +37,12 @@ class Ladder extends React.Component {
<div className='mainPanel'>
<h1 className='centered'>Game Ladder</h1>
<ButtonHeader />
<PagedLadder start={this.state.start} length={this.state.length} getFetch={this.getFetch.bind(this)} onReceived={this.onReceived.bind(this)} />
<PagedLadder
start={this.state.start}
length={this.state.length}
getFetch={this.getFetch.bind(this)}
onReceived={this.onReceived.bind(this)}
/>
<ButtonHeader />
</div>
</div>
@@ -93,6 +97,6 @@ class Ladder extends React.Component {
this.props.history.replace(`${this.props.location.pathname}?rank=${start}`);
}
}
}
};
export default withRouter(Ladder);
export default Ladder;
+1 -1
View File
@@ -15,7 +15,7 @@ class Login extends React.Component {
render() {
return (
<div className='page constrained'>
<LoginPanel onSubmit={() => this.props.history.push('/profile')} />
<LoginPanel onSuccess={(msg) => this.props.history.push('/profile')} />
<Link to='/' className='centered'>Return Home</Link>
</div>
);
+6 -3
View File
@@ -4,13 +4,16 @@ import { withRouter, Link } from 'react-router-dom';
class PageNotFound extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.state = {
//
};
}
render() {
let style = {
justifyContent: 'center'
}
};
return (
<div className='page centered' style={style}>
<h1>Page Not Found</h1>
@@ -18,6 +21,6 @@ class PageNotFound extends React.Component {
</div>
);
}
}
};
export default withRouter(PageNotFound);
+12 -13
View File
@@ -9,13 +9,12 @@ class PasswordChange extends React.Component {
constructor(props) {
super(props);
this.state = {
changeSent: false,
changeMsg: ''
changed: ''
}
}
componentDidMount() {
if (!this.props.id) {
if (!this.props.loggedIn) {
this.props.history.push('/');
}
}
@@ -23,13 +22,13 @@ class PasswordChange extends React.Component {
render() {
let Panel;
if (!this.state.changeSent) {
if (!this.state.changed) {
Panel = () => {
return (<PasswordChangePanel onPasswordChange={(msg) => this.setState({ changeSent: true, changeMsg: msg }) } />);
return (<PasswordChangePanel onSuccess={(msg) => this.setState({ changed: msg }) } />);
}
} else {
Panel = () => {
return (<p>{this.state.changeMsg}</p>);
return (<p>{this.state.changed}</p>);
}
}
@@ -42,17 +41,17 @@ class PasswordChange extends React.Component {
}
};
function mapStoreToProps(store) {
const mapStoreToProps = (store) => {
return {
id: store.account.id
}
}
loggedIn: store.account.id !== 0
};
};
function mapDispatchToProps(dispatch) {
const mapDispatchToProps = (dispatch) => {
return {
//
}
}
};
};
PasswordChange = connect(mapStoreToProps, mapDispatchToProps)(PasswordChange);
+4 -5
View File
@@ -8,21 +8,20 @@ class PasswordRecover extends React.Component {
constructor(props) {
super(props);
this.state = {
recoverSent: false,
recoverMsg: ''
recovered: ''
}
}
render() {
let Panel;
if (!this.state.recoverSent) {
if (!this.state.recovered) {
Panel = () => {
return (<PasswordRecoverPanel onEmailSent={(msg) => this.setState( {recoverSent: true, recoverMsg: msg} )} />);
return (<PasswordRecoverPanel onSuccess={(msg) => this.setState( {recovered: msg} )} />);
}
} else {
Panel = () => {
return (<p>{this.state.recoverMsg}</p>);
return (<p>{this.state.recovered}</p>);
}
}
+3 -5
View File
@@ -1,6 +1,5 @@
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import queryString from 'query-string';
//panels
@@ -10,8 +9,7 @@ class PasswordReset extends React.Component {
constructor(props) {
super(props);
this.state = {
reset: false,
resetMsg: '',
reset: '',
params: queryString.parse(props.location.search)
}
}
@@ -21,11 +19,11 @@ class PasswordReset extends React.Component {
if (!this.state.reset) {
Panel = () => {
return (<PasswordResetPanel email={this.state.params.email} token={this.state.params.token} onPasswordReset={(msg) => this.setState( {reset: true, resetMsg: msg} )}/>);
return (<PasswordResetPanel email={this.state.params.email} token={this.state.params.token} onSuccess={ (msg) => this.setState({reset: msg}) } />);
}
} else {
Panel = () => {
return (<p>{this.state.resetMsg}</p>);
return (<p>{this.state.reset}</p>);
}
}
+120 -147
View File
@@ -1,49 +1,25 @@
import React from 'react';
import { connect } from 'react-redux';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import queryString from 'query-string';
//actions
import { storeProfile } from '../../actions/profile.js';
//panels
import CommonLinks from '../panels/common_links.jsx';
import AttackButton from '../panels/attack_button.jsx';
import Equipment from '../panels/equipment.jsx';
import CombatLog from '../panels/combat_log.jsx';
class Profile extends React.Component {
constructor(props) {
super(props);
let params = queryString.parse(props.location.search);
this.state = {
params: params,
username: '',
gold: 0,
recruits: 0,
soldiers: 0,
spies: 0,
scientists: 0,
warning: '',
//combat log
start: params.log,
//equipment
fetchStatistics: null,
fetchEquipment: null
params: queryString.parse(props.location.search),
warning: '', //TODO: unified warning?
};
this.sendRequest('/profilerequest', this.state.params.username ? this.state.params.username : this.props.username);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (JSON.stringify(this.state) !== JSON.stringify(prevState)) {
// if (this.state.fetchStatistics) this.state.fetchStatistics();
// if (this.state.fetchEquipment) this.state.fetchEquipment();
}
this.sendRequest('/profilerequest', {username: this.state.params.username ? this.state.params.username : this.props.account.username});
}
render() {
@@ -54,8 +30,8 @@ class Profile extends React.Component {
//side panel stuff
let SidePanel;
if (this.props.id) {
if (this.props.username === this.state.username) {
if (this.props.account.id) {
if (this.props.account.username === this.props.profile.username) {
SidePanel = this.MyProfileSidePanel.bind(this);
} else {
SidePanel = this.NotMyProfileSidePanel.bind(this);
@@ -67,13 +43,12 @@ class Profile extends React.Component {
//main panel
let MainPanel;
if (this.props.id) {
if (this.props.account.id) {
//logged in
if (this.state.username === this.props.username) {
if (this.props.account.username === this.props.profile.username) {
MainPanel = this.MyProfileMainPanel.bind(this);
} else {
//not logged in
if (this.state.username !== '') {
if (this.props.profile.username) {
MainPanel = this.NotMyProfileMainPanel.bind(this);
} else {
MainPanel = this.ProfileNotFoundMainPanel.bind(this);
@@ -81,7 +56,7 @@ class Profile extends React.Component {
}
} else {
//not logged in
if (this.state.username !== '') {
if (this.props.profile.username) {
MainPanel = this.LoggedOutMainPanel.bind(this);
} else {
MainPanel = this.ProfileNotFoundMainPanel.bind(this);
@@ -107,23 +82,17 @@ class Profile extends React.Component {
}
//gameplay functions
sendRequest(url, username = this.props.username, role = '') { //NOTE: merged all requests here
//request this profile's info, using my credentials
let formData = new FormData();
formData.append('id', this.props.id);
formData.append('token', this.props.token);
formData.append('username', username);
formData.append('role', role);
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
this.storeProfile(
this.props.storeProfile(
json.username,
json.gold,
json.recruits,
@@ -136,27 +105,18 @@ class Profile extends React.Component {
this.setWarning(xhr.responseText);
}
}
}
};
//send
xhr.open('POST', url, true);
xhr.send(formData);
}
storeProfile(username, gold, recruits, soldiers, spies, scientists) {
this.setState({
username: username,
gold: gold,
recruits: recruits,
soldiers: soldiers,
spies: spies,
scientists: scientists
});
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
id: this.props.account.id,
token: this.props.account.token,
...args
}));
}
//panel functions
MyProfileSidePanel() {
//return the side panel
return (
<div className='sidePanel'>
<CommonLinks />
@@ -171,52 +131,50 @@ class Profile extends React.Component {
<div className='table noCollapse'>
<div className='row'>
<p className='col'>Username:</p>
<p className='col'>{this.state.username}</p>
<p className='col'>{this.props.profile.username}</p>
<div className='col'></div>
<div className='col'></div>
</div>
<div className='row'>
<p className='col'>Gold:</p>
<p className='col'>{this.state.gold}</p>
<div className='col' style={{flex: '2 1 1.5%'}}>(+1 gold for each recruit every half hour)</div>
<p className='col'>{this.props.profile.gold}</p>
<p className='col' style={{flex: '2 1 1%'}}>(+1 gold for each recruit every half hour)</p>
</div>
<div className='row'>
<p className='col'>Recruits:</p>
<p className='col'>{this.state.recruits}</p>
<button className='col' style={{flex: '2 1 1.5%'}} onClick={() => this.sendRequest('/recruitrequest')}>Recruit More Units</button>
<p className='col'>{this.props.profile.recruits}</p>
<button className='col' style={{flex: '2 1 1%'}} onClick={ () => this.sendRequest('/recruitrequest') }>Recruit More Units</button>
</div>
<div className='row'>
<p className='col'>Soldiers:</p>
<p className='col'>{this.state.soldiers}</p>
<button className='col' onClick={() => this.sendRequest('/trainrequest', this.props.username, 'soldier')}>Train Soldier (100 gold)</button>
<button className='col' onClick={() => this.sendRequest('/untrainrequest', this.props.username, 'soldier')}>Untrain Soldier</button>
<p className='col'>{this.props.profile.soldiers}</p>
<button className='col' onClick={ () => this.sendRequest('/trainrequest', {role: 'soldier'}) }>Train Soldier (100 gold)</button>
<button className='col' onClick={ () => this.sendRequest('/untrainrequest', {role: 'soldier'}) }>Untrain Soldier</button>
</div>
<div className='row'>
<p className='col'>Spies:</p>
<p className='col'>{this.state.spies}</p>
<button className='col' onClick={() => this.sendRequest('/trainrequest', this.props.username, 'spy')}>Train Spy (200 gold)</button>
<button className='col' onClick={() => this.sendRequest('/untrainrequest', this.props.username, 'spy')}>Untrain Spy</button>
<p className='col'>{this.props.profile.spies}</p>
<button className='col' onClick={ () => this.sendRequest('/trainrequest', {role: 'spy'}) }>Train Spy (200 gold)</button>
<button className='col' onClick={ () => this.sendRequest('/untrainrequest', {role: 'spy'}) }>Untrain Spy</button>
</div>
<div className='row'>
<p className='col'>Scientists:</p>
<p className='col'>{this.state.scientists}</p>
<button className='col' onClick={() => this.sendRequest('/trainrequest', this.props.username, 'scientist')}>Train Scientist (120 gold)</button>
<button className='col' onClick={() => this.sendRequest('/untrainrequest', this.props.username, 'scientist')}>Untrain Scientist</button>
<p className='col'>{this.props.profile.scientists}</p>
<button className='col' onClick={ () => this.sendRequest('/trainrequest', {role: 'scientist'}) }>Train Scientist (120 gold)</button>
<button className='col' onClick={ () => this.sendRequest('/untrainrequest', {role: 'scientist'}) }>Untrain Scientist</button>
</div>
</div>
<br />
<h1 className='centered'>Equipment</h1>
<Equipment username={this.props.username} token={this.props.token} scientists={1} getFetchSattistics={ (fn) => this.setState({ fetchStatistics: fn }) } getFetchEquipment={ (fn) => this.setState({ fetchEquipment: fn}) } />
<br />
<h1 className='centered'>Combat Log</h1>
<CombatLog username={this.props.username} start={this.state.start} length={this.state.length} />
</div>
);
}
@@ -225,7 +183,12 @@ class Profile extends React.Component {
//return the side panel
return (
<div className='sidePanel'>
<CommonLinks onClickProfile={() => {e.preventDefault(); this.sendRequest('/profilerequest', this.props.username); this.setWarning(''); this.props.history.push('/profile');}} />
<CommonLinks onClickProfile={(e) => {
e.preventDefault();
this.sendRequest('/profilerequest', {username: this.props.account.username});
this.setWarning('');
this.props.history.push('/profile');
}} />
</div>
);
}
@@ -233,47 +196,60 @@ class Profile extends React.Component {
NotMyProfileMainPanel() {
return (
<div className='panel'>
<h1 className='centered'>{this.state.username}'s Kingdom</h1>
<h1 className='centered'>{this.props.profile.username}'s Kingdom</h1>
<div className='table noCollapse'>
<div className='row'>
<p className='col'>Username:</p>
<p className='col'>{this.state.username}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.username}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Gold:</p>
<p className='col'>{this.state.gold}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.gold}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Recruits:</p>
<p className='col'>{this.state.recruits}</p>
<AttackButton className='col' style={{flex: '2 1 1.5%'}} setWarning={this.setWarning.bind(this)} attacker={this.props.username} defender={this.state.username} token={this.props.token} />
<p className='col'>{this.props.profile.recruits}</p>
<AttackButton
className='col'
style={{flex: '2 1 1%'}}
setWarning={this.setWarning.bind(this)}
attacker={this.props.account.username}
defender={this.props.profile.username}
token={this.props.account.token}
/>
</div>
<div className='row'>
<p className='col'>Soldiers:</p>
<p className='col'>{this.state.soldiers}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.soldiers}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Spies:</p>
<p className='col'>{this.state.spies}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.spies}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Scientists:</p>
<p className='col'>{this.state.scientists}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.scientists}</p>
<div className='col' />
<div className='col' />
</div>
</div>
</div>
@@ -291,48 +267,54 @@ class Profile extends React.Component {
LoggedOutMainPanel() {
return (
<div className='panel'>
<h1 className='centered'>{this.state.username}'s Kingdom</h1>
<h1 className='centered'>{this.props.profile.username}'s Kingdom</h1>
<div className='table noCollapse'>
<div className='row'>
<p className='col'>Username:</p>
<p className='col'>{this.state.username}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.username}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Gold:</p>
<p className='col'>{this.state.gold}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.gold}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Recruits:</p>
<p className='col'>{this.state.recruits}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.recruits}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Soldiers:</p>
<p className='col'>{this.state.soldiers}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.soldiers}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Spies:</p>
<p className='col'>{this.state.spies}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.spies}</p>
<div className='col' />
<div className='col' />
</div>
<div className='row'>
<p className='col'>Scientists:</p>
<p className='col'>{this.state.scientists}</p>
<div className='col'></div>
<div className='col'></div>
<p className='col'>{this.props.profile.scientists}</p>
<div className='col' />
<div className='col' />
</div>
</div>
</div>
@@ -341,38 +323,29 @@ class Profile extends React.Component {
ProfileNotFoundMainPanel() {
return (
<div className='page' />
<div className='page'>
<p className='centered'>Profile Not Found!</p>
</div>
);
}
setWarning(s) {
this.setState({
warning: s
});
this.setState({ warning: s });
}
}
Profile.propTypes = {
id: PropTypes.number.isRequired,
email: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.number.isRequired
};
function mapStoreToProps(store) {
const mapStoreToProps = (store) => {
return {
id: store.account.id,
email: store.account.email,
username: store.account.username,
token: store.account.token
}
}
account: store.account,
profile: store.profile,
};
};
function mapDispatchToProps(dispatch) {
const mapDispatchToProps = (dispatch) => {
return {
//
}
}
storeProfile: (username, gold, recruits, soldiers, spies, scientists) => dispatch(storeProfile(username, gold, recruits, soldiers, spies, scientists))
};
};
Profile = connect(mapStoreToProps, mapDispatchToProps)(Profile);
+4 -5
View File
@@ -8,8 +8,7 @@ class Signup extends React.Component {
constructor(props) {
super(props);
this.state = {
signupSent: false,
signupMsg: ''
signedUp: ''
}
//TODO: referral links
@@ -18,13 +17,13 @@ class Signup extends React.Component {
render() {
let Panel;
if (!this.state.signupSent) {
if (!this.state.signedUp) {
Panel = () => {
return (<SignupPanel onSignup={(msg) => this.setState( {signupSent: true, signupMsg: msg} )} />);
return (<SignupPanel onSuccess={ (msg) => this.setState({signedUp: msg}) } />);
}
} else {
Panel = () => {
return (<p>{this.state.signupMsg}</p>);
return (<p className='centered'>{this.state.signedUp}</p>);
}
}
+45 -43
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
class AttackButton extends React.Component {
@@ -8,85 +9,86 @@ class AttackButton extends React.Component {
message: ''
};
this.sendAttackingStatusRequest();
this.sendRequest('/attackstatusrequest', {attacker: this.props.attacker});
}
render() {
if (this.state.message !== '') {
if (this.state.message) {
return (
<p className={this.props.className} style={this.props.style}>{this.state.message}</p>
);
} else {
//inject something extra
let onClick = (e) => {
this.sendRequest('/attackrequest', {attacker: this.props.attacker, defender: this.props.defender});
if (this.props.onClick) {
this.props.onClick(e);
}
};
return (
<button className={this.props.className} style={this.props.style} onClick={this.sendAttackRequest.bind(this)}>Attack</button>
<button className={this.props.className} style={this.props.style} onClick={onClick}>Attack</button>
);
}
}
sendAttackRequest() {
//gameplay functions
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', '/attackrequest', true);
xhr.open('POST', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
//on success
if (json.status === 'attacking') {
this.setState({ message: `Your soldiers are attacking ${json.defender}` });
}
} else if (xhr.status === 400) {
if (this.props.setWarning) {
}
else if (xhr.status === 400 && this.props.setWarning) {
this.props.setWarning(xhr.responseText);
}
}
}
}
};
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
attacker: this.props.attacker,
defender: this.props.defender,
token: this.props.token
}));
if (this.props.onClick) {
this.props.onClick();
}
this.props.setDisabled(true);
}
sendAttackingStatusRequest() {
let xhr = new XMLHttpRequest();
xhr.open('POST', '/attackstatusrequest', true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
if (json.status === 'attacking') {
this.setState({ message: `Your soldiers are attacking ${json.defender}` });
}
}
}
}
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
username: this.props.attacker
id: this.props.id,
token: this.props.token,
...args
}));
}
};
AttackButton.propTypes = {
id: PropTypes.number.isRequired,
token: PropTypes.number.isRequired,
attacker: PropTypes.string.isRequired,
defender: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object,
onClick: PropTypes.func,
setWarning: PropTypes.func,
attacker: PropTypes.string.isRequired,
defender: PropTypes.string.isRequired,
token: PropTypes.number.isRequired
setWarning: PropTypes.func
};
const mapStoreToProps = (store) => {
return {
id: store.account.id,
token: store.account.token
};
};
const mapDispatchToProps = (dispatch) => {
return {
//
};
};
AttackButton = connect(mapStoreToProps, mapDispatchToProps)(AttackButton);
export default AttackButton;
+1 -1
View File
@@ -21,5 +21,5 @@ export default class Blurb extends React.Component {
</div>
);
}
}
};
-89
View File
@@ -1,89 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import PagedCombatLog from './paged_combat_log.jsx';
class CombatLog extends React.Component {
constructor(props) {
super(props);
this.state = {
start: parseInt(props.start) || 0,
length: parseInt(props.length) || 20,
fetch: null
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (JSON.stringify(this.state) !== JSON.stringify(prevState)) {
this.state.fetch();
}
}
render() {
let ButtonHeader = this.buttonHeader.bind(this);
return (
<div className='panel'>
<ButtonHeader />
<PagedCombatLog username={this.props.username} start={this.state.start} length={this.state.length} getFetch={this.getFetch.bind(this)} onReceived={this.onReceived.bind(this)} />
<ButtonHeader />
</div>
);
}
buttonHeader() {
return (
<div className='table'>
<div className='row'>
<button className='col' onClick={this.decrement.bind(this)}>{'< Back'}</button>
<div className='col' />
<div className='col' />
<button className='col' onClick={this.increment.bind(this)}>{'Next >'}</button>
</div>
</div>
);
}
increment() {
let start = this.state.start + this.state.length;
this.props.history.push(`${this.props.location.pathname}?log=${start}`);
}
decrement() {
let start = Math.max(0, this.state.start - this.state.length);
//don't decrement too far
if (start === this.state.start) {
return;
}
this.props.history.push(`${this.props.location.pathname}?log=${start}`);
}
//bound callbacks
getFetch(fn) {
this.setState({ fetch: fn });
}
onReceived(data) {
if (data.length === 0) {
let start = Math.max(0, this.state.start - this.state.length);
//don't decrement too far
if (start === this.state.start) {
return;
}
this.props.history.replace(`${this.props.location.pathname}?log=${start}`);
}
}
}
CombatLog.propTypes = {
username: PropTypes.string.isRequired
};
export default withRouter(CombatLog);
+11 -6
View File
@@ -8,14 +8,16 @@ import Logout from './logout.jsx';
class CommonLinks extends React.Component {
constructor(props) {
super(props);
this.state = {
//
}
}
render() {
//render extra stuff
//render any extra stuff
let Extra;
if (this.props.extra) {
Extra = this.props.extra;
} else {
@@ -29,20 +31,21 @@ class CommonLinks extends React.Component {
<p><Link to='/' onClick={this.props.onClickHome}>Return Home</Link></p>
<p><Link to='/profile' onClick={this.props.onClickProfile}>Your Kingdom</Link></p>
<p><Link to='/ladder' onClick={this.props.onClickLadder}>Game Ladder</Link></p>
<p><Link to='/combatlog' onClick={this.props.onClickCombatLog}>Combat Log</Link></p>
<p><Link to='/passwordchange' onClick={this.props.onClickPasswordChange}>Change Password</Link></p>
<Extra />
<Logout onClick={() => this.props.history.push('/')} />
<Logout onClick={ () => this.props.history.push('/') } />
</div>
);
} else { //if not logged in
return (
<div className='panel'>
<p><Link to='/' onClick={this.props.onClickHome}>Return Home</Link></p>
<p><Link to='/signup' onClick={this.props.onClickSignup}>Sign Up</Link></p>
<p><Link to='/login' onClick={this.props.onClickLogin}>Login</Link></p>
<p><Link to='/passwordrecover' onClick={this.props.onClickPasswordRecover}>Recover Password</Link></p>
<p><Link to='/' onClick={this.props.onClickHome}>Return Home</Link></p>
<p><Link to='/ladder' onClick={this.props.onClickLadder}>Game Ladder</Link></p>
<Extra />
@@ -50,9 +53,11 @@ class CommonLinks extends React.Component {
);
}
}
}
};
CommonLinks.propTypes = {
loggedIn: PropTypes.bool.isRequired,
onClickSignup: PropTypes.func,
onClickLogin: PropTypes.func,
onClickPasswordRecover: PropTypes.func,
@@ -66,13 +71,13 @@ function mapStoreToProps(store) {
return {
loggedIn: store.account.id !== undefined && store.account.id !== 0
}
}
};
function mapDispatchToProps(dispatch) {
return {
//
}
}
};
CommonLinks = connect(mapStoreToProps, mapDispatchToProps)(CommonLinks);
+2
View File
@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
//TODO: incomplete
class Equipment extends React.Component {
constructor(props) {
super(props);
+3 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
export default class Footer extends React.Component {
class Footer extends React.Component {
render() {
return (
<footer>
@@ -8,5 +8,6 @@ export default class Footer extends React.Component {
</footer>
);
}
}
};
export default Footer;
+33 -28
View File
@@ -1,6 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { login } from '../../actions/accounts.js';
import PropTypes from 'prop-types';
import { login } from '../../actions/account.js';
import { validateEmail } from '../../../common/utilities.js';
class Login extends React.Component {
@@ -14,7 +16,7 @@ class Login extends React.Component {
}
render() {
let warningStyle = {
let warningStyle = { //TODO: lift the warning out to the page?
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
@@ -26,15 +28,15 @@ class Login extends React.Component {
<p>{this.state.warning}</p>
</div>
<form action='/loginrequest' method='post' onSubmit={(e) => this.submit(e)}>
<form action='/loginrequest' method='post' onSubmit={ this.submit.bind(this) } >
<div>
<label>Email:</label>
<input type='text' name='email' value={this.state.email} onChange={this.updateEmail.bind(this)} />
<input type='text' name='email' value={this.state.email} onChange={ this.updateEmail.bind(this) } />
</div>
<div>
<label>Password:</label>
<input type='password' name='password' value={this.state.password} onChange={this.updatePassword.bind(this)} />
<input type='password' name='password' value={this.state.password} onChange={ this.updatePassword.bind(this) } />
</div>
<button type='submit' disabled={!this.state.email}>Login</button>
@@ -50,19 +52,26 @@ class Login extends React.Component {
return;
}
//build the XHR
//build the XHR (around an existing form object)
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
this.props.login(json.id, json.email, json.username, json.token);
if (this.props.onSubmit) {
this.props.onSubmit();
this.props.login(
json.id,
json.email,
json.username,
json.token
);
if (this.props.onSuccess) {
this.props.onSuccess(json.msg); //NOTE: could use this as a redirect to a special offer or sonmething
}
}
@@ -94,43 +103,39 @@ class Login extends React.Component {
}
setWarning(s) {
this.setState({
warning: s
});
this.setState({ warning: s });
}
clearInput() {
this.setState({
email: '',
password: '',
warning: ''
});
this.setState({ email: '', password: '', warning: '' });
}
updateEmail(evt) {
this.setState({
email: evt.target.value
});
this.setState({ email: evt.target.value });
}
updatePassword(evt) {
this.setState({
password: evt.target.value
});
this.setState({ password: evt.target.value });
}
}
};
function mapStoreToProps(store) {
Login.propTypes = {
login: PropTypes.func.isRequired,
onSubmit: PropTypes.func
};
const mapStoreToProps = (store) => {
return {
//
}
}
};
function mapDispatchToProps(dispatch) {
const mapDispatchToProps = (dispatch) => {
return {
login: (id, email, username, token) => dispatch(login(id, email, username, token))
}
}
};
Login = connect(mapStoreToProps, mapDispatchToProps)(Login);
+21 -14
View File
@@ -1,58 +1,65 @@
import React from 'react';
import { connect } from 'react-redux';
import { logout } from '../../actions/accounts.js';
import PropTypes from 'prop-types';
import { logout } from '../../actions/account.js';
class Logout extends React.Component {
constructor(props) {
super(props);
this.state = {
//
}
}
render() {
return (
<button className='logoutButton' type='submit' onClick={(e) => this.submit(e)}>Logout</button>
<button className='logoutButton' type='submit' onClick={(e) => { e.preventDefault(); this.sendRequest('/logoutrequest') }} >Logout</button>
);
}
submit(e) {
e.preventDefault();
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', '/logoutrequest', true);
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
email: this.props.email,
token: this.props.token
id: this.props.id,
token: this.props.token,
...args
}));
//Don't wait for a response
this.props.logout();
if (this.props.onClick) {
this.props.onClick();
}
}
}
};
Logout.propTypes = {
email: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
token: PropTypes.number.isRequired,
logout: PropTypes.func.isRequired,
onClick: PropTypes.func
}
};
function mapStoreToProps(store) {
return {
email: store.account.email,
id: store.account.id,
token: store.account.token
}
}
};
function mapDispatchToProps(dispatch) {
return {
logout: () => { dispatch(logout()) }
}
}
};
Logout = connect(mapStoreToProps, mapDispatchToProps)(Logout);
+62
View File
@@ -0,0 +1,62 @@
import React from 'react';
import ReactMarkdown from 'react-markdown/with-html';
import PropTypes from 'prop-types';
class News extends React.Component {
constructor(props) {
super(props);
this.state = {
//
};
if (props.getFetch) {
props.getFetch( () => this.sendRequest('/newsrequest', {length: this.props.length || 10}) );
}
}
render() {
return (
<div className='panel'>
{Object.keys(this.state).map((key) => <div key={key}>
<ReactMarkdown source={this.state[key]} escapeHtml={false} />
<hr className='newsLine' />
</div>)}
</div>
);
}
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
//on success
this.setState(json);
}
else if (xhr.status === 400 && this.props.setWarning) {
this.props.setWarning(xhr.responseText);
}
}
};
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
//NOTE: No id or token needed for the news
...args
}));
}
};
News.propTypes = {
length: PropTypes.number,
setWarning: PropTypes.func,
getFetch: PropTypes.func
};
export default News;
-44
View File
@@ -1,44 +0,0 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
class NewsPanel extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {}
};
this.fetchNews(this.props.length || 10);
}
render() {
return (
<div>
{Object.keys(this.state.data).map((key) => <div key={key}>
<ReactMarkdown source={this.state.data[key]} escapeHTML={false} />
<hr className='newsLine' />
</div> )}
</div>
);
}
fetchNews(max) {
//build the XHR
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
this.setState({data: JSON.parse(xhr.responseText)});
}
}
}
xhr.open('POST', '/newsrequest', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({ max: max }));
}
};
export default NewsPanel;
+59 -21
View File
@@ -1,20 +1,31 @@
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
class PagedCombatLog extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {}
//
}
if (props.getFetch) {
props.getFetch(this.fetchCombatLog.bind(this));
props.getFetch(() => this.sendRequest('/combatlogrequest', {username: props.username, start: props.start, length: props.length}));
}
}
render() {
//make the enemy name a link
const PrettyName = (props) => {
if (props.name === this.props.username) {
return (<p {...props}>{props.name}</p>);
} else {
return (<p {...props}><Link to={`/profile?username=${props.name}`}>{props.name}</Link></p>);
}
};
return (
<div className='table'>
<div className='row'>
@@ -28,44 +39,51 @@ class PagedCombatLog extends React.Component {
<p className='col centered minWidth'>Gold Transferred</p>
<p className='col centered minWidth'>Victor Casualties</p>
</div>
{Object.keys(this.state.data).map((key) => <div key={key} className={'row'}>
<p className='col centered minWidth'>{ this.parseDate(this.state.data[key].eventTime) }</p>
<p className='col centered minWidth'><Link to={`/profile?username=${this.state.data[key].attackerUsername}`} className={'col'}>{this.state.data[key].attackerUsername}</Link></p>
<p className='col centered minWidth'><Link to={`/profile?username=${this.state.data[key].defenderUsername}`} className={'col'}>{this.state.data[key].defenderUsername}</Link></p>
<p className='col centered minWidth'>{this.state.data[key].attackingUnits}</p>
<p className='col centered minWidth'>{this.state.data[key].defendingUnits}</p>
<p className='col centered minWidth'>{this.state.data[key].undefended ? 'yes' : 'no'}</p>
<p className='col centered minWidth'>{this.state.data[key].victor}</p>
<p className='col centered minWidth'>{this.state.data[key].spoilsGold}</p>
<p className='col centered minWidth'>{this.state.data[key].casualtiesVictor}</p>
{Object.keys(this.state).map((key) => <div key={key} className={'row'}>
<p className='col centered minWidth'>{ this.parseDate(this.state[key].eventTime) }</p>
<PrettyName className='col centered minWidth' name={this.state[key].attacker} />
<PrettyName className='col centered minWidth' name={this.state[key].defender} />
<p className='col centered minWidth'>{this.state[key].attackingUnits}</p>
<p className='col centered minWidth'>{this.state[key].defendingUnits}</p>
<p className='col centered minWidth'>{this.state[key].undefended ? 'yes' : 'no'}</p>
<p className='col centered minWidth'>{this.state[key].victor}</p>
<p className='col centered minWidth'>{this.state[key].spoilsGold}</p>
<p className='col centered minWidth'>{this.state[key].casualtiesVictor}</p>
</div>)}
</div>
);
}
fetchCombatLog(username = this.props.username, start = this.props.start, length = this.props.length) {
//gameplay functions
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let data = JSON.parse(xhr.responseText);
this.setState({data: data});
let json = JSON.parse(xhr.responseText);
//on success
this.setState(json);
if (this.props.onReceived) {
this.props.onReceived(data);
this.props.onReceived(json);
}
}
else if (xhr.status === 400 && this.props.setWarning) {
this.props.setWarning(xhr.responseText);
}
}
};
xhr.open('POST', '/combatlogrequest', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
username: username,
start: start,
length: length
id: this.props.id,
token: this.props.token,
...args
}));
}
@@ -75,14 +93,34 @@ class PagedCombatLog extends React.Component {
let date = new Date(eventTime);
return `${date.getDate()} ${month[date.getMonth()]}`;
}
}
};
PagedCombatLog.propTypes = {
id: PropTypes.number.isRequired,
token: PropTypes.number.isRequired,
username: PropTypes.string.isRequired,
start: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
setWarning: PropTypes.func,
getFetch: PropTypes.func,
onReceived: PropTypes.func
};
const mapStoreToProps = (store) => {
return {
id: store.account.id,
token: store.account.token
};
};
const mapDispatchToProps = (dispatch) => {
return {
//
};
};
PagedCombatLog = connect(mapStoreToProps, mapDispatchToProps)(PagedCombatLog);
export default withRouter(PagedCombatLog);
+23 -17
View File
@@ -6,11 +6,11 @@ class PagedLadder extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {}
//
}
if (props.getFetch) {
props.getFetch(this.fetchLadder.bind(this));
props.getFetch( () => this.sendRequest('/ladderrequest', {start: this.props.start || 0, length: this.props.length || 20}) );
}
}
@@ -23,45 +23,51 @@ class PagedLadder extends React.Component {
<p className='col centered'>Recruits</p>
<p className='col centered'>Gold</p>
</div>
{Object.keys(this.state.data).map((key) => <div key={key} className={'row centered'}>
<p className={'col centered'}><Link to={`/profile?username=${this.state.data[key].username}`}>{this.state.data[key].username}</Link></p>
<p className={'col centered'}>{this.state.data[key].soldiers}</p>
<p className={'col centered'}>{this.state.data[key].recruits}</p>
<p className={'col centered'}>{this.state.data[key].gold}</p>
{Object.keys(this.state).map((key) => <div key={key} className={'row'}>
<p className={'col centered'}><Link to={`/profile?username=${this.state[key].username}`}>{this.state[key].username}</Link></p>
<p className={'col centered'}>{this.state[key].soldiers}</p>
<p className={'col centered'}>{this.state[key].recruits}</p>
<p className={'col centered'}>{this.state[key].gold}</p>
</div> )}
</div>
);
}
fetchLadder(start = this.props.start, length = this.props.length) {
sendRequest(url, args = {}) { //send a unified request, using my credentials
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let data = JSON.parse(xhr.responseText);
this.setState({data: data});
let json = JSON.parse(xhr.responseText);
//on success
this.setState(json);
if (this.props.onReceived) {
this.props.onReceived(data);
this.props.onReceived(json);
}
}
else if (xhr.status === 400 && this.props.setWarning) {
this.props.setWarning(xhr.responseText);
}
}
};
xhr.open('POST', '/ladderrequest', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
start: start,
length: length
//NOTE: No id or token needed for the news
...args
}));
}
}
};
PagedLadder.propTypes = {
start: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
start: PropTypes.number,
length: PropTypes.number,
setWarning: PropTypes.func,
getFetch: PropTypes.func,
onReceived: PropTypes.func
};
+40 -35
View File
@@ -1,10 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import { sessionChange } from '../../actions/accounts.js';
import PropTypes from 'prop-types';
import { sessionChange } from '../../actions/account.js';
class PasswordChange extends React.Component {
constructor(props) {
super(props);
this.state = {
password: '',
retype: '',
@@ -13,7 +16,7 @@ class PasswordChange extends React.Component {
}
render() {
let warningStyle = {
let warningStyle = { //TODO: lift the warning out to the page?
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
@@ -25,7 +28,7 @@ class PasswordChange extends React.Component {
<p>{this.state.warning}</p>
</div>
<form action='/passwordchangerequest' method='post' onSubmit={(e) => this.submit(e)}>
<form action='/passwordchangerequest' method='post' onSubmit={this.submit.bind(this)}>
<div>
<label>Password:</label>
<input type='password' name='password' value={this.state.password} onChange={this.updatePassword.bind(this)} />
@@ -49,23 +52,25 @@ class PasswordChange extends React.Component {
return;
}
//build the XHR
//build the XHR (around an existing form object)
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
formData.append('email', this.props.email);
formData.append('id', this.props.id);
formData.append('token', this.props.token);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
//on success
this.props.sessionChange(json.token);
//DEBUGGING
if (this.props.onPasswordChange) {
this.props.onPasswordChange(json.msg);
if (this.props.onSuccess) {
this.props.onSuccess(json.msg);
}
}
@@ -78,6 +83,8 @@ class PasswordChange extends React.Component {
//send the XHR
xhr.open('POST', form.action, true);
xhr.send(formData);
this.clearInput();
}
validateInput(e) {
@@ -94,45 +101,43 @@ class PasswordChange extends React.Component {
return true;
}
setWarning(s) {
this.setState({
warning: s
});
}
clearInput() {
this.setState({
password: '',
retype: '',
warning: ''
});
this.setState({ password: '', retype: '', warning: '' });
}
updatePassword(evt) {
this.setState({
password: evt.target.value
});
this.setState({ password: evt.target.value });
}
updateRetype(evt) {
this.setState({
retype: evt.target.value
});
this.setState({ retype: evt.target.value });
}
}
function mapStoreToProps(store) {
setWarning(s) {
this.setState({ warning: s });
}
};
PasswordChange.propTypes = {
id: PropTypes.number.isRequired,
token: PropTypes.number.isRequired,
sessionChange: PropTypes.func.isRequired,
onSuccess: PropTypes.func
};
const mapStoreToProps = (store) => {
return {
email: store.account.email,
id: store.account.id,
token: store.account.token
}
}
};
};
function mapDispatchToProps(dispatch) {
const mapDispatchToProps = (dispatch) => {
return {
sessionChange: (token) => { dispatch(sessionChange(token)); }
}
}
sessionChange: (token) => dispatch(sessionChange(token))
};
};
PasswordChange = connect(mapStoreToProps, mapDispatchToProps)(PasswordChange);
+18 -17
View File
@@ -1,9 +1,11 @@
import React from 'react';
import { validateEmail } from '../../../common/utilities.js';
import { validateEmail } from '../../../common/utilities.js'; //TODO: move utilities to a better position
import PropTypes from 'prop-types';
class PasswordRecover extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
warning: ''
@@ -23,7 +25,7 @@ class PasswordRecover extends React.Component {
<p>{this.state.warning}</p>
</div>
<form action='/passwordrecoverrequest' method='post' onSubmit={(e) => this.submit(e)}>
<form action='/passwordrecoverrequest' method='post' onSubmit={this.submit.bind(this)}>
<div>
<label>Email:</label>
<input type='text' name='email' value={this.state.email} onChange={this.updateEmail.bind(this)} />
@@ -42,17 +44,19 @@ class PasswordRecover extends React.Component {
return;
}
//build the XHR
//build the XHR (around an existing form object)
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
//DEBUGGING
if (this.props.onEmailSent) {
this.props.onEmailSent(xhr.responseText);
let json = JSON.parse(xhr.responseText);
if (this.props.onSuccess) {
this.props.onSuccess(json.msg);
}
}
@@ -79,23 +83,20 @@ class PasswordRecover extends React.Component {
}
setWarning(s) {
this.setState({
warning: s
});
this.setState({ warning: s });
}
clearInput() {
this.setState({
email: '',
warning: ''
});
this.setState({ email: '', warning: '' });
}
updateEmail(evt) {
this.setState({
email: evt.target.value
});
this.setState({ email: evt.target.value });
}
}
};
PasswordRecover.propTypes = {
onSuccess: PropTypes.func
};
export default PasswordRecover;
+17 -21
View File
@@ -1,11 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import { sessionChange } from '../../actions/accounts.js';
import { sessionChange } from '../../actions/account.js';
import PropTypes from 'prop-types';
class PasswordReset extends React.Component {
constructor(props) {
super(props);
this.state = {
password: '',
retype: '',
@@ -26,7 +27,7 @@ class PasswordReset extends React.Component {
<p>{this.state.warning}</p>
</div>
<form action='/passwordresetrequest' method='post' onSubmit={(e) => this.submit(e)}>
<form action='/passwordresetrequest' method='post' onSubmit={this.submit.bind(this)}>
<div>
<label>Password:</label>
<input type='password' name='password' value={this.state.password} onChange={this.updatePassword.bind(this)} />
@@ -53,16 +54,19 @@ class PasswordReset extends React.Component {
//build the XHR
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
formData.append('email', this.props.email);
formData.append('token', this.props.token);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
if (this.props.onPasswordReset) {
this.props.onPasswordReset(xhr.responseText);
let json = JSON.parse(xhr.responseText);
if (this.props.onSuccess) {
this.props.onSuccess(json.msg);
}
}
@@ -92,35 +96,27 @@ class PasswordReset extends React.Component {
}
setWarning(s) {
this.setState({
warning: s
});
this.setState({ warning: s });
}
clearInput() {
this.setState({
password: '',
retype: '',
warning: ''
});
this.setState({ password: '', retype: '', warning: '' });
}
updatePassword(evt) {
this.setState({
password: evt.target.value
});
this.setState({ password: evt.target.value });
}
updateRetype(evt) {
this.setState({
retype: evt.target.value
});
this.setState({ retype: evt.target.value });
}
}
};
PasswordReset.propTypes = {
email: PropTypes.string.isRequired,
token: PropTypes.number.isRequired
token: PropTypes.number.isRequired,
onSuccess: PropTypes.func
};
export default PasswordReset;
+15 -28
View File
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
class Signup extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
username: '',
@@ -27,7 +28,7 @@ class Signup extends React.Component {
<p>{this.state.warning}</p>
</div>
<form action='/signuprequest' method='post' onSubmit={(e) => this.submit(e)}>
<form action='/signuprequest' method='post' onSubmit={this.submit.bind(this)}>
<div>
<label>Email:</label>
<input type='text' name='email' value={this.state.email} onChange={this.updateEmail.bind(this)} />
@@ -64,14 +65,16 @@ class Signup extends React.Component {
//build the XHR
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
if (this.props.onSignup) {
this.props.onSignup(xhr.responseText);
console.log('trying to...');
let json = JSON.parse(xhr.responseText);
if (this.props.onSuccess) {
this.props.onSuccess(json.msg);
}
}
@@ -114,48 +117,32 @@ class Signup extends React.Component {
}
setWarning(s) {
this.setState({
warning: s
});
this.setState({ warning: s });
}
clearInput() {
this.setState({
email: '',
username: '',
password: '',
retype: '',
warning: ''
});
this.setState({ email: '', username: '', password: '', retype: '', warning: '' });
}
updateEmail(evt) {
this.setState({
email: evt.target.value
});
this.setState({ email: evt.target.value });
}
updateUsername(evt) {
this.setState({
username: evt.target.value
});
this.setState({ username: evt.target.value });
}
updatePassword(evt) {
this.setState({
password: evt.target.value
});
this.setState({ password: evt.target.value });
}
updateRetype(evt) {
this.setState({
retype: evt.target.value
});
this.setState({ retype: evt.target.value });
}
}
};
Signup.propTypes = {
onSignup: PropTypes.func
onSuccess: PropTypes.func
};
export default Signup;
@@ -1,4 +1,4 @@
import { LOGIN, LOGOUT, SESSIONCHANGE } from "../actions/accounts.js";
import { LOGIN, LOGOUT, SESSION_CHANGE } from "../actions/account.js";
const initialStore = {
id: 0,
@@ -7,7 +7,7 @@ const initialStore = {
token: 0
};
export function accountReducer(store = initialStore, action) {
export const accountReducer = (store = initialStore, action) => {
switch(action.type) {
case LOGIN: {
let newStore = JSON.parse(JSON.stringify(initialStore));
@@ -23,7 +23,7 @@ export function accountReducer(store = initialStore, action) {
case LOGOUT:
return initialStore;
case SESSIONCHANGE: {
case SESSION_CHANGE: {
let newStore = JSON.parse(JSON.stringify(store));
newStore.token = action.token;
@@ -33,6 +33,6 @@ export function accountReducer(store = initialStore, action) {
default:
return store;
}
};
}
-18
View File
@@ -1,18 +0,0 @@
import { SET_ATTACK_DISABLED } from '../actions/combat.js';
const initialStore = {
attackDisabled: false
};
export function combatReducer(store = initialStore, action) {
switch(action.type) {
case SET_ATTACK_DISABLED: {
let newStore = JSON.parse(JSON.stringify(initialStore));
newStore.attackDisabled = action.disabled;
return newStore;
}
default:
return store;
}
}
+59
View File
@@ -0,0 +1,59 @@
import {
STORE_PROFILE,
STORE_USERNAME,
STORE_GOLD,
STORE_RECRUITS,
STORE_SOLDIERS,
STORE_SPIES,
STORE_SCIENTISTS
} from '../actions/profile.js';
const initialStore = {
username: '',
gold: 0,
recruits: 0,
soldiers: 0,
spies: 0,
scientists: 0
};
export const profileReducer = (store = initialStore, action) => {
let newStore = JSON.parse(JSON.stringify(store));
switch(action.type) {
case STORE_PROFILE:
newStore.username = action.username;
newStore.gold = action.gold;
newStore.recruits = action.recruits;
newStore.soldiers = action.soldiers;
newStore.spies = action.spies;
newStore.scientists = action.scientists;
break;
case STORE_USERNAME:
newStore.username = action.username;
break;
case STORE_GOLD:
newStore.gold = action.gold;
break;
case STORE_RECRUITS:
newStore.recruits = action.recruits;
break;
case STORE_SOLDIERS:
newStore.soldiers = action.soldiers;
break;
case STORE_SPIES:
newStore.spies = action.spies;
break;
case STORE_SCIENTISTS:
newStore.scientists = action.scientists;
break;
};
return newStore;
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { combineReducers } from 'redux';
import { accountReducer } from './accounts.js';
import { combatReducer } from './combat.js';
import { accountReducer } from './account.js';
import { profileReducer } from './profile.js';
//compile all reducers together
export default combineReducers({
account: accountReducer,
/* combat: combatReducer */
profile: profileReducer
});
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = {
]
},
optimization: {
minimize: false,
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {