diff --git a/.bootstraprc b/.bootstraprc
new file mode 100644
index 0000000..b9ef34d
--- /dev/null
+++ b/.bootstraprc
@@ -0,0 +1,5 @@
+{
+ "bootstrapVersion": 4,
+ "useFlexbox": true,
+ "preBootstrapCustomizations": "app/assets/_bs-variables.scss"
+}
diff --git a/.eslintrc b/.eslintrc
index a63e357..ffe9416 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1 +1 @@
-{"rules": {"indent": ["error", "tab"], "no-tabs": "off", "react/jsx-indent": ["error", "tab"]}, "extends": "airbnb", "env": {"browser": true, "node": true}}
+{"rules": {"indent": ["error", "tab"], "no-tabs": "off", "react/jsx-indent": ["error", "tab"], "react/jsx-indent-props": ["error", "tab"]}, "extends": "airbnb", "env": {"browser": true, "node": true}}
diff --git a/Gruntfile.js b/Gruntfile.js
index db09d1c..5603698 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -9,6 +9,9 @@ module.exports = function (grunt) {
filename: 'bundle.js',
path: './dist/app/'
},
+ resolve: {
+ extensions: ['.js', '.jsx'],
+ },
module: {
loaders: [
{
@@ -38,6 +41,12 @@ module.exports = function (grunt) {
cwd: './app/',
src: ['assets/**'],
dest: './dist/app/assets/'
+ },
+ {
+ expand: true,
+ cwd: './node_modules/bootstrap/',
+ src: ['dist/**'],
+ dest: './dist/app/assets/bootstrap/'
}
]
}
@@ -58,15 +67,33 @@ module.exports = function (grunt) {
{
expand: true,
cwd: './server/',
- src: ['*.js'],
+ src: ['**/*.js'],
dest: './dist/server/'
}
]
}
},
eslint: {
- app: ['./app/*.js*'],
- server: ['./server/*.js']
+ app: {
+ files: [
+ {
+ expand: true,
+ cwd: './app/',
+ src: ['**/*.js*'],
+ dest: './dist/app/'
+ }
+ ]
+ },
+ server: {
+ files: [
+ {
+ expand: true,
+ cwd: './server/',
+ src: ['**/*.js'],
+ dest: './dist/server/'
+ }
+ ]
+ }
}
});
diff --git a/app/components/button.jsx b/app/components/button.jsx
new file mode 100644
index 0000000..fab9e07
--- /dev/null
+++ b/app/components/button.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class Button extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+Button.propTypes = {
+ children: React.PropTypes.node.isRequired,
+ onClick: React.PropTypes.func,
+ color: React.PropTypes.string,
+};
+
+Button.defaultProps = {
+ onClick: () => {},
+ color: 'primary',
+};
diff --git a/app/components/layouts/column.jsx b/app/components/layouts/column.jsx
new file mode 100644
index 0000000..9ba75c7
--- /dev/null
+++ b/app/components/layouts/column.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class Column extends React.Component {
+ constructor() {
+ super();
+ this.className = this.className.bind(this);
+ }
+
+ className() {
+ if (this.props.width) {
+ if (this.props.breakpoint) {
+ return `col-${this.props.breakpoint}-${this.props.width}`;
+ }
+ return `col-${this.props.width}`;
+ }
+ return 'col';
+ }
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+}
+
+Column.propTypes = {
+ children: React.PropTypes.node.isRequired,
+ width: React.PropTypes.number,
+ breakpoint: React.PropTypes.string,
+};
+
+Column.defaultProps = {
+ width: null,
+ breakpoint: null,
+};
diff --git a/app/components/layouts/container.jsx b/app/components/layouts/container.jsx
new file mode 100644
index 0000000..9ee0c67
--- /dev/null
+++ b/app/components/layouts/container.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class Container extends React.Component {
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+}
+
+Container.propTypes = {
+ children: React.PropTypes.node.isRequired,
+};
diff --git a/app/components/layouts/row.jsx b/app/components/layouts/row.jsx
new file mode 100644
index 0000000..aded209
--- /dev/null
+++ b/app/components/layouts/row.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class Row extends React.Component {
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+}
+
+Row.propTypes = {
+ children: React.PropTypes.node.isRequired,
+};
+
diff --git a/app/index.html b/app/index.html
index 0a00260..a2fa9e0 100644
--- a/app/index.html
+++ b/app/index.html
@@ -1,10 +1,12 @@
Chronos
+
+
diff --git a/app/index.jsx b/app/index.jsx
index 38ce17c..bfd1769 100644
--- a/app/index.jsx
+++ b/app/index.jsx
@@ -1,7 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
+import { BrowserRouter as Router, Route } from 'react-router-dom';
+
+import Navigation from './navigation';
+import PageLogin from './pages/login';
+import PageMain from './pages/main';
ReactDOM.render(
- Hello, world!
,
+
+
+
+
+
+
+ ,
document.getElementById('root'),
);
diff --git a/app/navigation.jsx b/app/navigation.jsx
new file mode 100644
index 0000000..7b6af0d
--- /dev/null
+++ b/app/navigation.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import Container from './components/layouts/container';
+import Button from './components/button';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class Navigation extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/pages/login.jsx b/app/pages/login.jsx
new file mode 100644
index 0000000..2697c23
--- /dev/null
+++ b/app/pages/login.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import Container from '../components/layouts/container';
+import Button from '../components/button';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class PageLogin extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/app/pages/main.jsx b/app/pages/main.jsx
new file mode 100644
index 0000000..6e40081
--- /dev/null
+++ b/app/pages/main.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import Container from '../components/layouts/container';
+import Row from '../components/layouts/row';
+import Column from '../components/layouts/column';
+
+// eslint-disable-next-line react/prefer-stateless-function
+export default class PageMain extends React.Component {
+ render() {
+ return (
+
+
+
+ Hello, world!
+
+
+ Hello, world!
+
+
+
+ );
+ }
+}
diff --git a/package.json b/package.json
index cd0abb2..9ee9f16 100644
--- a/package.json
+++ b/package.json
@@ -34,9 +34,12 @@
"webpack": "^2.2.1"
},
"dependencies": {
+ "bootstrap": "^4.0.0-alpha.6",
"express": "^4.14.1",
"mysql": "^2.13.0",
"react": "^15.4.2",
- "react-dom": "^15.4.2"
+ "react-dom": "^15.4.2",
+ "react-router-dom": "^4.0.0-beta.6",
+ "semver": "^5.3.0"
}
}
diff --git a/server/api.js b/server/api.js
index 74367c7..76123a8 100644
--- a/server/api.js
+++ b/server/api.js
@@ -9,5 +9,12 @@ export default class API {
this.router.get('/', (req, res) => {
res.end('API v1');
});
+
+ this.router.all('/*', this.constructor.auth);
+ }
+
+ static auth(req, res, next) {
+ res.end('not implemented');
+ next();
}
}
diff --git a/server/database.js b/server/database.js
index a53807a..89d16d0 100644
--- a/server/database.js
+++ b/server/database.js
@@ -1,4 +1,5 @@
import mysql from 'mysql';
+import semver from 'semver';
import { fatal, getVersion } from './utils';
const DATABASE = 'chronos';
@@ -12,7 +13,7 @@ export default class Database {
}
query(query, values) {
- console.log('QUERY:', query);
+ console.log('QUERY:', query.replace(/[\n\t]+/g, ' ').replace(/^ /g, '').replace(/ $/g, ''));
return new Promise((resolve, reject) => {
this.connection.query(query, values, (err, results, fields) => {
if (err) {
@@ -23,134 +24,141 @@ export default class Database {
});
}
- checkAndMigrate() {
- this.query(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`, [])
- .then(() => this.query(`USE ${DATABASE}`, []))
- .then(() => this.query('SELECT value FROM options WHERE option = "version"', (err, result) => {
- if (err || result === undefined || getVersion() !== result[0].value) {
- return this.migrate(result).catch(e => fatal(e));
- }
- return true;
- }))
+ async checkAndMigrate() {
+ return this.query(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`)
+ .then(() => this.query(`USE ${DATABASE}`))
+ .then(() => this.query('SELECT * FROM options WHERE option = ?', ['VERSION'])
+ .then(result => this.migrate(result[0] ? result[0].value : null))
+ .catch(() => this.migrate()))
.catch(err => fatal(err));
}
async migrate(oldVersion) {
- if (!oldVersion) {
- // create tables
- const tables = [
- `CREATE TABLE options (
- option VARCHAR(12) NOT NULL,
- value VARCHAR(64) NOT NULL,
- PRIMARY KEY (option)
- )`,
- `CREATE TABLE school (
- id INT NOT NULL AUTO_INCREMENT,
- name VARCHAR(64) NOT NULL,
- domain VARCHAR(32),
- PRIMARY KEY (id)
- )`,
- `CREATE TABLE auth (
- school INT NOT NULL,
- id INT NOT NULL,
- type CHAR(3) NOT NULL,
- oid_meta VARCHAR(128),
- oid_cid VARCHAR(64),
- oid_csecret VARCHAR(64),
- PRIMARY KEY (school, id),
- FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE holiday (
- id INT NOT NULL AUTO_INCREMENT,
- name VARCHAR(64) NOT NULL,
- start DATETIME NOT NULL,
- end DATETIME NOT NULL,
- PRIMARY KEY (id)
- )`,
- `CREATE TABLE observes (
- school INT NOT NULL,
- holiday INT NOT NULL,
- FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE,
- FOREIGN KEY (holiday) REFERENCES holiday(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE user (
- school INT NOT NULL,
- id INT AUTO_INCREMENT NOT NULL,
- name VARCHAR(64),
- email VARCHAR(64),
- oid_id VARCHAR(64),
- pwd_hash VARCHAR(64),
- role CHAR(3),
- PRIMARY KEY (id),
- FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE group_ (
- id INT AUTO_INCREMENT NOT NULL,
- name VARCHAR(64) NOT NULL,
- type CHAR(3),
- PRIMARY KEY (id)
- )`,
- `CREATE TABLE group_mentor (
- id INT NOT NULL,
- level TINYINT NOT NULL,
- year YEAR NOT NULL,
- PRIMARY KEY (id),
- FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE member (
- user INT NOT NULL,
- group_ INT NOT NULL,
- FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
- FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE event_once (
- group_ INT NOT NULL,
- id INT AUTO_INCREMENT NOT NULL,
- name VARCHAR(64) NOT NULL,
- start DATETIME NOT NULL,
- end DATETIME NOT NULL,
- PRIMARY KEY (id),
- FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE attachment (
- event_once INT NOT NULL,
- id INT NOT NULL,
- PRIMARY KEY (event_once, id),
- FOREIGN KEY (event_once) REFERENCES event_once(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE event_weekly (
- group_ INT NOT NULL,
- id INT AUTO_INCREMENT NOT NULL,
- name VARCHAR(64) NOT NULL,
- day TINYINT NOT NULL,
- starttime TIME NOT NULL,
- endtime TIME NOT NULL,
- PRIMARY KEY (id),
- FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- `CREATE TABLE ignored (
- user INT NOT NULL,
- event_weekly INT NOT NULL,
- FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
- FOREIGN KEY (event_weekly) REFERENCES event_weekly(id) ON DELETE CASCADE ON UPDATE CASCADE
- )`,
- ];
-
- for (let i = 0; i < tables.length; i += 1) {
- // eslint-disable-next-line no-await-in-loop
- await this.query(tables[i], []);
- }
-
- await this.query(`
- INSERT INTO options (option, value)
- VALUES ('version', '${getVersion()}')
- `);
-
-
+ if (semver.satisfies(oldVersion, '~1')) {
+ // database is up-to-date
return true;
+ } else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration
+ // database needs to be updated
+ return this.query(`DROP DATABASE ${DATABASE}`)
+ .then(() => this.checkAndMigrate());
}
- // for now
- return this.query(`DROP DATABASE ${DATABASE}`, [])
- .then(() => this.checkAndMigrate());
+ // database does not exist
+
+ // create tables
+ const tables = [
+ `CREATE TABLE options (
+ option VARCHAR(12) NOT NULL,
+ value VARCHAR(64) NOT NULL,
+ PRIMARY KEY (option)
+ )`,
+ `CREATE TABLE school (
+ id INT NOT NULL AUTO_INCREMENT,
+ name VARCHAR(64) NOT NULL,
+ domain VARCHAR(32),
+ PRIMARY KEY (id)
+ )`,
+ `CREATE TABLE auth (
+ school INT NOT NULL,
+ id INT NOT NULL,
+ type CHAR(3) NOT NULL,
+ oid_meta VARCHAR(128),
+ oid_cid VARCHAR(64),
+ oid_csecret VARCHAR(64),
+ PRIMARY KEY (school, id),
+ FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE holiday (
+ id INT NOT NULL AUTO_INCREMENT,
+ name VARCHAR(64) NOT NULL,
+ start DATETIME NOT NULL,
+ end DATETIME NOT NULL,
+ PRIMARY KEY (id)
+ )`,
+ `CREATE TABLE observes (
+ school INT NOT NULL,
+ holiday INT NOT NULL,
+ FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (holiday) REFERENCES holiday(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE user (
+ school INT NOT NULL,
+ id INT AUTO_INCREMENT NOT NULL,
+ name VARCHAR(64),
+ email VARCHAR(64),
+ oid_id VARCHAR(64),
+ pwd_hash VARCHAR(64),
+ role CHAR(3),
+ PRIMARY KEY (id),
+ FOREIGN KEY (school) REFERENCES school(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE group_ (
+ id INT AUTO_INCREMENT NOT NULL,
+ name VARCHAR(64) NOT NULL,
+ type CHAR(3),
+ PRIMARY KEY (id)
+ )`,
+ `CREATE TABLE group_mentor (
+ id INT NOT NULL,
+ level TINYINT NOT NULL,
+ year YEAR NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE member (
+ user INT NOT NULL,
+ group_ INT NOT NULL,
+ FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE event_once (
+ group_ INT NOT NULL,
+ id INT AUTO_INCREMENT NOT NULL,
+ name VARCHAR(64) NOT NULL,
+ start DATETIME NOT NULL,
+ end DATETIME NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE attachment (
+ event_once INT NOT NULL,
+ id INT NOT NULL,
+ PRIMARY KEY (event_once, id),
+ FOREIGN KEY (event_once) REFERENCES event_once(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE event_weekly (
+ group_ INT NOT NULL,
+ id INT AUTO_INCREMENT NOT NULL,
+ name VARCHAR(64) NOT NULL,
+ day TINYINT NOT NULL,
+ starttime TIME NOT NULL,
+ endtime TIME NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (group_) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ `CREATE TABLE ignored (
+ user INT NOT NULL,
+ event_weekly INT NOT NULL,
+ FOREIGN KEY (user) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (event_weekly) REFERENCES event_weekly(id) ON DELETE CASCADE ON UPDATE CASCADE
+ )`,
+ ];
+ for (let i = 0; i < tables.length; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ await this.query(tables[i]);
+ }
+
+ // set version number
+ await this.query(`
+ INSERT INTO options (option, value)
+ VALUES ('version', '${getVersion()}')
+ `);
+
+ // add first school
+ await this.query(`
+ INSERT INTO school (name, domain)
+ VALUES (?, ?)
+ `, ['NUS High School', 'nushigh.edu.sg']);
+
+ return true;
}
}