Start implementing MySQL data fetching.
gitea/icedream/vizon-countdown-website/develop There was a failure building this commit Details

- Create Drawing component to display drawing information.
- Add SQL file to create view to filter for amount of correctly matched numbers.
- Use pooled connections for MySQL connections.
- Implement API endpoint "status", not done yet, needs to query from above mentioned view.
- Add more npm scripts that interact with docker/docker-compose.
- Allow web container to write back to source folder.
develop
Icedream 2017-08-23 14:59:12 +02:00
parent 9bea67e1b1
commit 202c35b517
Signed by: icedream
GPG Key ID: 1573F6D8EFE4D0CF
8 changed files with 317 additions and 18 deletions

View File

@ -28,7 +28,7 @@ services:
build: docker/node
command: sh ./docker/web.sh
volumes:
- ".:/src:ro"
- ".:/src"
- "web_npm_cache:/var/cache/npm"
- "/src/node_modules"
ports:

View File

@ -1,8 +1,32 @@
const path = require('path');
const mysql = require('mysql');
const express = require('express');
const moment = require('moment-timezone');
const fs = require('fs');
const bodyParser = require('body-parser');
/* Database setup */
const dbPool = mysql.createPool({
host: process.env.MYSQL_HOST || 'localhost',
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || undefined,
database: process.env.MYSQL_DATABASE || 'vizon',
socketPath: process.env.MYSQL_SOCKET_PATH || undefined,
timezone: process.env.TZ || 'local',
debug: process.env.NODE_ENV === 'development',
});
const tableNames = {
drawings: process.env.MYSQL_TABLE_DRAWINGS || 'vizon_drawings',
users: process.env.MYSQL_TABLE_USERS || 'vizon_users',
rankings: 'vizon_web_rankings',
};
/* Frontend server setup */
const frontendDir = path.resolve(__dirname, 'dist');
const app = express();
@ -29,6 +53,76 @@ fs.stat(frontendDir, (err) => {
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Additional middleware which will set headers that we need on each request.
app.use((req, res, next) => {
// Disable caching so we'll always get the latest comments.
res.setHeader('Cache-Control', 'no-cache');
next();
});
function calculateRankings(id: Number) {
db.query('SELECT * FROM `vizon_web_rankings` WHERE `vizon_drawings_id`=?', [id], (err, results, fields) => {
if (err) {
console.error('Failed to query rankings from database:', err);
res.status(500).json({
error: 'Database query failed',
});
}
});
}
app.get('/api/status', (req, res) => {
dbPool.getConnection((err, db) => {
if (err) {
console.error('Failed to connect to database:', err);
res.status(500).json({
error: 'Database connection failed',
});
}
db.query('SELECT * FROM ?? ORDER BY ?? DESC LIMIT 0, 1', [
tableNames.drawings,
'id',
], (qerr, results, fields) => {
if (qerr) {
console.error('Failed to request drawings:', err);
res.status(500).json({
error: 'Database query failed',
});
return;
}
const row = results[0];
const id = row.id;
const date = moment(row.drawing_date);
const numbers = [
row.first,
row.second,
row.third,
row.fourth,
row.fifth,
row.sixth,
].map(i => parseInt(i, 10));
const ranking = [
// @TODO - calculate ranking from mysql tables {drawings} and {bets}
];
res.json({
lastDrawing: {
id,
date,
numbers,
ranking,
},
});
});
});
});
app.listen(app.get('port'), () => {
console.log(`Server started: http://localhost:${app.get('port')}/`);
});

42
package-lock.json generated
View File

@ -1643,6 +1643,11 @@
"integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=",
"dev": true
},
"bignumber.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.2.tgz",
"integrity": "sha1-LR3DfuWWiGfs6pC22k0W5oYI0h0="
},
"binary-extensions": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz",
@ -2474,8 +2479,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "2.2.2",
@ -5518,8 +5522,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "3.0.2",
@ -6691,14 +6694,12 @@
"moment": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=",
"dev": true
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
},
"moment-timezone": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz",
"integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=",
"dev": true,
"requires": {
"moment": "2.18.1"
}
@ -6730,6 +6731,17 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
"dev": true
},
"mysql": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.14.1.tgz",
"integrity": "sha512-ZPXqQeYH7L1QPDyC77Rcp32cNCQnNjz8Y4BbF17tOjm5yhSfjFa3xS4PvuxWJtEEmwVc4ccI7sSntj4eyYRq0A==",
"requires": {
"bignumber.js": "4.0.2",
"readable-stream": "2.3.3",
"safe-buffer": "5.1.1",
"sqlstring": "2.2.0"
}
},
"nan": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz",
@ -8940,8 +8952,7 @@
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
"dev": true
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"progress": {
"version": "2.0.0",
@ -9291,7 +9302,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@ -9774,8 +9784,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"sass-graph": {
"version": "2.2.4",
@ -10471,6 +10480,11 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"sqlstring": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.2.0.tgz",
"integrity": "sha1-wxNcTqirzX5+50GklmqJHYak8ZE="
},
"sshpk": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
@ -10554,7 +10568,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
@ -11128,8 +11141,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utila": {
"version": "0.4.0",

View File

@ -10,6 +10,9 @@
"docker-compose": "docker-compose -f docker-compose.local.yml",
"docker:down:clean": "npm run -s docker:down -- --rmi all -v",
"docker:down": "npm run -s docker-compose -- down",
"docker:logs": "npm run -s docker-compose -- logs",
"docker:logs:follow": "npm run -s docker:logs -- -f",
"docker:restart": "npm run -s docker-compose -- restart",
"docker:up:daemon": "npm run -s docker:up -- -d",
"docker:up": "npm run -s docker-compose -- up --build",
"docker": "npm run -s docker:up",
@ -51,7 +54,6 @@
"eslint-plugin-react": "^7.2.1",
"eslint": "^4.5.0",
"file-loader": "^0.11.2",
"moment-timezone": "^0.5.13",
"normalize-scss": "^7.0.0",
"nwb-sass": "^0.8.1",
"nwb": "^0.18.10",
@ -68,6 +70,8 @@
},
"dependencies": {
"body-parser": "^1.17.2",
"express": "^4.15.4"
"express": "^4.15.4",
"moment-timezone": "^0.5.13",
"mysql": "^2.14.1"
}
}

View File

@ -0,0 +1,91 @@
CREATE OR REPLACE VIEW `vizon_web_rankings` AS
SELECT
bets.id,
vizon_drawings_id,
COUNT(*) AS richtige
FROM
(
SELECT
vizon_users_id AS id,
vizon_drawings_id,
FIRST AS NUMBER
FROM
vizon_bets
UNION ALL
SELECT
vizon_users_id,
vizon_drawings_id,
SECOND
FROM
vizon_bets
UNION ALL
SELECT
vizon_users_id,
vizon_drawings_id,
third
FROM
vizon_bets
UNION ALL
SELECT
vizon_users_id,
vizon_drawings_id,
fourth
FROM
vizon_bets
UNION ALL
SELECT
vizon_users_id,
vizon_drawings_id,
fifth
FROM
vizon_bets
UNION ALL
SELECT
vizon_users_id,
vizon_drawings_id,
sixth
FROM
vizon_bets
) bets,
(
SELECT
id,
FIRST AS NUMBER
FROM
vizon_drawings
UNION ALL
SELECT
id,
SECOND
FROM
vizon_drawings
UNION ALL
SELECT
id,
third
FROM
vizon_drawings
UNION ALL
SELECT
id,
fourth
FROM
vizon_drawings
UNION ALL
SELECT
id,
fifth
FROM
vizon_drawings
UNION ALL
SELECT
id,
sixth
FROM
vizon_drawings
) drawings
WHERE
bets.vizon_drawings_id = drawings.id AND bets.number = drawings.number
GROUP BY
bets.id,
vizon_drawings_id

View File

@ -4,6 +4,7 @@ import moment from 'moment-timezone';
import 'react-fontawesome';
import WebFont from 'webfontloader';
import Countdown from './Countdown';
import Drawing from './Drawing';
import Header from './Header';
import Footer from './Footer';
import getUpcomingDate from './getUpcomingDate';
@ -79,6 +80,19 @@ class App extends React.Component {
The next VIzon draw is on {nextUpcomingDate.format('dddd')}, {nextUpcomingDate.format('L LT z')}.
</p>
<Countdown date={nextUpcomingDate} />
<p>
Last Drawing:
</p>
<Drawing
numbers={[1, 2, 3, 4, 5, 6]}
ranking={[
{ rank: '1st', winners: 0 },
{ rank: '2nd', winners: 0 },
{ rank: '3rd', winners: 0 },
{ rank: 'Consolation', winners: 4 },
]}
/>
</div>
<Footer>

52
src/Drawing.jsx Normal file
View File

@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './Drawing.sass';
const Numbers = ({ numbers }) => (
<div className={style.numbers}>
{
numbers.map((number, index) => (
<div key={index} className={style.number}>
{number}
</div>
))
}
</div>
);
Numbers.propTypes = {
numbers: PropTypes.arrayOf(PropTypes.number).isRequired,
};
export default class Drawing extends React.Component {
static propTypes = {
ranking: PropTypes.arrayOf(PropTypes.shape({
rank: PropTypes.string,
winners: PropTypes.int,
})).isRequired,
numbers: PropTypes.arrayOf(PropTypes.number).isRequired,
}
static defaultProps = {
}
render() {
const { ranking, numbers } = this.props;
return (
<div className={style.drawing}>
<Numbers numbers={numbers} />
<div className={style.ranking}>
{
ranking.map(({ rank, winners }) => (
<div key={rank} className={style.rank}>
<div className={style.rankName}>{rank}</div>
<div className={style.winners}>{winners > 0 ? winners : 'No'} winners</div>
</div>
))
}
</div>
</div>
);
}
}

32
src/Drawing.sass Normal file
View File

@ -0,0 +1,32 @@
.drawing
.numbers
display: flex
flex-direction: row
width: 100%
.number
flex: 1 0 auto
text-align: center
font-size: 2.5em
font-weight: bold
.ranking
display: table
width: 100%
.rank
display: table-row
.rankName, .winners
display: table-cell
.rankName
text-align: right
width: 50%
padding-right: .25em
.winners
text-align: left
width: 50%
padding-left: .25em