1
0
Fork 0

Added bootstrap v4.0.0-beta.6 and more scaffolding

master
Ambrose Chua 2017-03-03 21:09:40 +08:00
parent 0559058dc2
commit 826e5d99bf
15 changed files with 355 additions and 130 deletions

5
.bootstraprc Normal file
View File

@ -0,0 +1,5 @@
{
"bootstrapVersion": 4,
"useFlexbox": true,
"preBootstrapCustomizations": "app/assets/_bs-variables.scss"
}

View File

@ -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}}

View File

@ -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/'
}
]
}
}
});

23
app/components/button.jsx Normal file
View File

@ -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 className={`btn btn-${this.props.color}`} onClick={this.props.onClick}>
{ this.props.children }
</button>
);
}
}
Button.propTypes = {
children: React.PropTypes.node.isRequired,
onClick: React.PropTypes.func,
color: React.PropTypes.string,
};
Button.defaultProps = {
onClick: () => {},
color: 'primary',
};

View File

@ -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 (
<div className={this.className()}>
{ this.props.children }
</div>
);
}
}
Column.propTypes = {
children: React.PropTypes.node.isRequired,
width: React.PropTypes.number,
breakpoint: React.PropTypes.string,
};
Column.defaultProps = {
width: null,
breakpoint: null,
};

View File

@ -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 (
<div className="container">
{ this.props.children }
</div>
);
}
}
Container.propTypes = {
children: React.PropTypes.node.isRequired,
};

View File

@ -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 (
<div className="row">
{ this.props.children }
</div>
);
}
}
Row.propTypes = {
children: React.PropTypes.node.isRequired,
};

View File

@ -1,10 +1,12 @@
<head>
<meta charset="UTF-8">
<title>Chronos</title>
<link rel="stylesheet" href="assets/bootstrap/dist/css/bootstrap.min.css" />
</head>
<body>
<div id="root">
</div>
<script src="bundle.js"></script>
<script src="assets/bootstrap/dist/js/bootstrap.min.js"></script>
</body>

View File

@ -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(
<h1>Hello, world!</h1>,
<Router>
<div>
<Navigation />
<Route exact path="/" component={PageMain} />
<Route path="/login" component={PageLogin} />
</div>
</Router>,
document.getElementById('root'),
);

28
app/navigation.jsx Normal file
View File

@ -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 (
<nav className="navbar navbar-inverse bg-inverse sticky-top">
<Container>
<div className="d-flex flex-row">
<span className="navbar-brand">Chronos</span>
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<Link className="nav-link" to="/login">Login</Link>
</li>
</ul>
<div className="form-inline">
<Button color="secondary" onClick={() => alert('test')}>Logout</Button>
</div>
</div>
</Container>
</nav>
);
}
}

17
app/pages/login.jsx Normal file
View File

@ -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 (
<Container>
<Button>
Login
</Button>
</Container>
);
}
}

23
app/pages/main.jsx Normal file
View File

@ -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 (
<Container>
<Row>
<Column width="3" breakpoint="lg">
<h1>Hello, world! </h1>
</Column>
<Column>
<h1>Hello, world! </h1>
</Column>
</Row>
</Container>
);
}
}

View File

@ -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"
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}