diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..a63e357
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1 @@
+{"rules": {"indent": ["error", "tab"], "no-tabs": "off", "react/jsx-indent": ["error", "tab"]}, "extends": "airbnb", "env": {"browser": true, "node": true}}
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..db09d1c
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,81 @@
+module.exports = function (grunt) {
+
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ webpack: {
+ app: {
+ entry: './app/index.jsx',
+ output: {
+ filename: 'bundle.js',
+ path: './dist/app/'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader',
+ query: {
+ sourceMap: true,
+ presets: ['env', 'react']
+ }
+ }
+ ]
+ }
+ }
+ },
+ copy: {
+ app: {
+ files: [
+ {
+ expand: true,
+ cwd: './app/',
+ src: ['index.html'],
+ dest: './dist/app/'
+ },
+ {
+ expand: true,
+ cwd: './app/',
+ src: ['assets/**'],
+ dest: './dist/app/assets/'
+ }
+ ]
+ }
+ },
+ babel: {
+ options: {
+ sourceMap: true,
+ presets: [
+ ['env', {
+ targets: {
+ node: 'current'
+ }
+ }]
+ ]
+ },
+ server: {
+ files: [
+ {
+ expand: true,
+ cwd: './server/',
+ src: ['*.js'],
+ dest: './dist/server/'
+ }
+ ]
+ }
+ },
+ eslint: {
+ app: ['./app/*.js*'],
+ server: ['./server/*.js']
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-webpack');
+ grunt.loadNpmTasks('grunt-babel');
+ grunt.loadNpmTasks('grunt-eslint');
+ grunt.loadNpmTasks('grunt-contrib-copy');
+
+ grunt.registerTask('all', ['eslint', 'webpack', 'copy', 'babel']);
+ grunt.registerTask('default', ['all']);
+
+};
diff --git a/app/assets/core.css b/app/assets/core.css
new file mode 100644
index 0000000..e69de29
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..0a00260
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,10 @@
+
+
+ Chronos
+
+
+
+
+
+
+
diff --git a/app/index.jsx b/app/index.jsx
new file mode 100644
index 0000000..38ce17c
--- /dev/null
+++ b/app/index.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+ReactDOM.render(
+ Hello, world!
,
+ document.getElementById('root'),
+);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..cd0abb2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "Chronos",
+ "version": "0.1.0",
+ "description": "A school event planner and timetable",
+ "scripts": {
+ "start": "node dist/server/index.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ambrosechua/chronos.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "proprietary",
+ "bugs": {
+ "url": "https://github.com/ambrosechua/chronos/issues"
+ },
+ "homepage": "https://github.com/ambrosechua/chronos#readme",
+ "devDependencies": {
+ "babel-loader": "^6.3.2",
+ "babel-preset-env": "^1.1.10",
+ "babel-preset-react": "^6.23.0",
+ "eslint": "^3.16.1",
+ "eslint-config-airbnb": "^14.1.0",
+ "eslint-plugin-import": "^2.2.0",
+ "eslint-plugin-jsx-a11y": "^4.0.0",
+ "eslint-plugin-react": "^6.10.0",
+ "grunt": "^1.0.1",
+ "grunt-babel": "^6.0.0",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-eslint": "^19.0.0",
+ "grunt-webpack": "^2.0.1",
+ "webpack": "^2.2.1"
+ },
+ "dependencies": {
+ "express": "^4.14.1",
+ "mysql": "^2.13.0",
+ "react": "^15.4.2",
+ "react-dom": "^15.4.2"
+ }
+}
diff --git a/server/api.js b/server/api.js
new file mode 100644
index 0000000..74367c7
--- /dev/null
+++ b/server/api.js
@@ -0,0 +1,13 @@
+import Router from 'express';
+
+export default class API {
+ constructor() {
+ this.router = Router({
+ strict: true,
+ });
+
+ this.router.get('/', (req, res) => {
+ res.end('API v1');
+ });
+ }
+}
diff --git a/server/database.js b/server/database.js
new file mode 100644
index 0000000..a53807a
--- /dev/null
+++ b/server/database.js
@@ -0,0 +1,156 @@
+import mysql from 'mysql';
+import { fatal, getVersion } from './utils';
+
+const DATABASE = 'chronos';
+
+export default class Database {
+ constructor(options) {
+ this.connection = mysql.createConnection(options);
+ this.connection.connect();
+
+ this.checkAndMigrate();
+ }
+
+ query(query, values) {
+ console.log('QUERY:', query);
+ return new Promise((resolve, reject) => {
+ this.connection.query(query, values, (err, results, fields) => {
+ if (err) {
+ reject(err);
+ }
+ resolve(results, fields);
+ });
+ });
+ }
+
+ 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;
+ }))
+ .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()}')
+ `);
+
+
+ return true;
+ }
+ // for now
+ return this.query(`DROP DATABASE ${DATABASE}`, [])
+ .then(() => this.checkAndMigrate());
+ }
+}
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 0000000..d9af39b
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,17 @@
+import path from 'path';
+import express from 'express';
+import Database from './database';
+import API from './api';
+
+const app = express();
+const database = new Database({
+ host: 'localhost',
+ user: 'root',
+ password: '',
+});
+const api = new API(database);
+
+app.use('/', express.static(path.join(__dirname, '..', 'app')));
+app.use('/api/v1', api.router);
+
+app.listen(8080);
diff --git a/server/utils.js b/server/utils.js
new file mode 100644
index 0000000..fc6a58c
--- /dev/null
+++ b/server/utils.js
@@ -0,0 +1,13 @@
+const getVersion = () => {
+ if (process.env.npm_package_version) {
+ return process.env.npm_package_version;
+ }
+ throw new Error('Unable to obtain running version');
+};
+
+const fatal = (e) => {
+ console.error(e);
+ process.exit(1);
+};
+
+export { getVersion, fatal };