es6 classes for React; closes #112

This commit is contained in:
Radek Stepan 2016-06-08 15:05:07 -04:00
parent b57bc76edd
commit c3896a3598
28 changed files with 1660 additions and 1187 deletions

View File

@ -1,3 +1,7 @@
{
"presets": [ "react", "es2015" ]
"presets": [
"react",
"es2015",
"stage-0"
]
}

View File

@ -1,6 +1,6 @@
{
"name": "burnchart",
"version": "3.2.1",
"version": "3.2.2",
"description": "GitHub Burndown Chart as a Service",
"author": "Radek Stepan <dev@radekstepan.com> (http://radekstepan.com)",
"license": "AGPL-3.0",
@ -21,6 +21,7 @@
"babel": "^6.3.26",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.4.3",
"babelify": "^7.2.0",
"browserify": "^13.0.0",

View File

@ -1060,5 +1060,5 @@ ul li {
color: #C1041C;
}
#app.theme--monza #page #content #projects table tr td.action {
background: #760211;
color: #760211;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,8 +27,8 @@ export default {
},
// Request pertaining.
"request": {
// Default timeout of 5s.
"timeout": 5e3
// Default timeout of 10s.
"timeout": 1e4
},
// The app theme; 'monza' is the default red theme.
"theme": "monza"

View File

@ -39,13 +39,13 @@ let find = ({ to, params, query }) => {
_.find(routes, (name, url) => {
if (name != to) return;
let matches = url.match(re);
// Do not match on the number of params.
if (_.keys(params).length != (matches || []).length) return;
// Do not match on the name of params.
if (!_.every(matches, m => m.slice(1) in params)) return;
// Fill in the params.
$url = url.replace(re, m => params[m.slice(1)]);

View File

@ -8,33 +8,42 @@ import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
import S from './Space.jsx';
export default React.createClass({
export default class AddProjectForm extends React.Component {
displayName: 'AddProjectForm.jsx',
displayName: 'AddProjectForm.jsx'
constructor(props) {
super(props);
// Blank input.
this.state = { 'val': '' };
// Bindings.
this._onChange = this._onChange.bind(this);
this._onAdd = this._onAdd.bind(this);
}
// Sign user in.
_onSignIn() {
actions.emit('user.signin');
},
}
_onChange(evt, { newValue }) {
this.setState({ 'val': newValue });
},
}
// Get a list of repo suggestions.
_onGetList({ value }) {
actions.emit('projects.search', value);
},
}
// What should be the value of the suggestion.
_getListValue(value) {
return value;
},
}
// How do we render the repo?
_renderListValue(value) {
return value;
},
}
// Add the project.
_onAdd() {
@ -46,12 +55,7 @@ export default React.createClass({
actions.emit('projects.add', { owner, name });
// Redirect to the dashboard.
App.navigate({ 'to': 'projects' });
},
// Blank input.
getInitialState() {
return { 'val': '' };
},
}
render() {
let user;
@ -106,11 +110,6 @@ export default React.createClass({
</div>
</div>
);
},
// Focus input field on mount.
componentDidMount() {
if ('el' in this.refs) this.refs.el.focus();
}
});
}

View File

@ -7,13 +7,17 @@ d3Tip(d3);
import lines from '../modules/chart/lines.js';
import axes from '../modules/chart/axes.js';
export default React.createClass({
export default class Chart extends React.Component {
displayName: 'Chart.jsx',
displayName: 'Chart.jsx'
constructor(props) {
super(props);
}
render() {
return <div id="chart" ref="el" style={this.props.style} />;
},
}
componentDidMount() {
let { data } = this.props;
@ -154,4 +158,4 @@ export default React.createClass({
.on('mouseout', tooltip.hide);
}
});
}

View File

@ -7,13 +7,17 @@ import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
import Link from './Link.jsx';
export default React.createClass({
export default class EditProjects extends React.Component {
displayName: 'EditProjects.jsx',
displayName: 'EditProjects.jsx'
constructor(props) {
super(props);
}
_onDelete(project) {
actions.emit('projects.delete', project);
},
}
render() {
let { projects } = this.props;
@ -55,4 +59,4 @@ export default React.createClass({
);
}
});
}

View File

@ -1,8 +1,12 @@
import React from 'react';
export default React.createClass({
export default class Footer extends React.Component {
displayName: 'Footer.jsx',
displayName: 'Footer.jsx'
constructor(props) {
super(props);
}
render() {
return (
@ -14,4 +18,4 @@ export default React.createClass({
);
}
});
}

View File

@ -6,24 +6,28 @@ import Notify from './Notify.jsx';
import Icon from './Icon.jsx';
import Link from './Link.jsx';
export default React.createClass({
export default class Header extends React.Component {
displayName: 'Header.jsx',
displayName: 'Header.jsx'
constructor(props) {
super(props);
}
// Sign user in.
_onSignIn() {
actions.emit('user.signin');
},
}
// Sign user out.
_onSignOut() {
actions.emit('user.signout');
},
}
// Add example projects.
_onDemo() {
actions.emit('projects.demo');
},
}
render() {
// From app store.
@ -79,4 +83,4 @@ export default React.createClass({
);
}
});
}

View File

@ -5,14 +5,18 @@ import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
import Link from './Link.jsx';
export default React.createClass({
export default class Hero extends React.Component {
displayName: 'Hero.jsx',
displayName: 'Hero.jsx'
constructor(props) {
super(props);
}
// Add example projects.
_onDemo() {
actions.emit('projects.demo');
},
}
render() {
return (
@ -35,4 +39,4 @@ export default React.createClass({
);
}
});
}

View File

@ -3,7 +3,7 @@ import React from 'react';
import format from '../modules/format.js';
// Fontello icon hex codes.
let codes = {
const codes = {
'delete': '\e800', // Font Awesome - trash-empty
'settings': '\e801', // Font Awesome - cog
'pencil': '\e802', // Font Awesome - pencil
@ -20,18 +20,22 @@ let codes = {
'megaphone': '\e80d', // Entypo - megaphone
'sort': '\e80e', // Typicons - sort-alphabet
'spinner': '\e80f', // MFG Labs - spinner1
'fire': '\e810' // Maki - fire-station
'fire': '\e810' // Maki - fire-station
};
export default React.createClass({
export default class Icon extends React.Component {
displayName: 'Icon.jsx',
displayName: 'Icon.jsx'
constructor(props) {
super(props);
}
render() {
let name = this.props.name;
const name = this.props.name;
if (name && name in codes) {
let code = format.hexToDec(codes[name]);
const code = format.hexToDec(codes[name]);
return (
<span
className={`icon ${name}`}
@ -43,4 +47,4 @@ export default React.createClass({
return false;
}
});
}

View File

@ -2,15 +2,19 @@ import React from 'react';
import App from '../App.jsx';
export default React.createClass({
export default class Link extends React.Component {
displayName: 'Link.jsx',
displayName: 'Link.jsx'
constructor(props) {
super(props);
}
// Navigate to a route.
_navigate(link, evt) {
App.navigate(link);
evt.preventDefault();
},
}
render() {
let route = this.props.route;
@ -27,4 +31,4 @@ export default React.createClass({
);
}
});
}

View File

@ -9,14 +9,18 @@ import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
import Link from './Link.jsx';
export default React.createClass({
export default class Milestones extends React.Component {
displayName: 'Milestones.jsx',
displayName: 'Milestones.jsx'
constructor(props) {
super(props);
}
// Cycle through milestones sort order.
_onSort() {
actions.emit('projects.sort');
},
}
render() {
let { projects, project } = this.props;
@ -119,4 +123,4 @@ export default React.createClass({
}
}
});
}

View File

@ -5,17 +5,14 @@ import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
let Notify = React.createClass({
class Notify extends React.Component {
displayName: 'Notify.jsx',
displayName: 'Notify.jsx'
// Close notification.
_onClose() {
actions.emit('system.notify');
},
constructor(props) {
super(props);
getDefaultProps() {
return {
this.props = {
// No text.
'text': null,
// Grey style.
@ -25,7 +22,12 @@ let Notify = React.createClass({
// Just announcing.
'icon': 'megaphone'
};
},
}
// Close notification.
_onClose() {
actions.emit('system.notify');
}
render() {
let { text, system, type, icon, ttl } = this.props;
@ -51,9 +53,9 @@ let Notify = React.createClass({
}
}
});
}
export default React.createClass({
export default class NotifyWrapper extends React.Component {
// TODO: animate in
render() {
@ -73,4 +75,4 @@ export default React.createClass({
);
}
});
}

View File

@ -1,12 +1,16 @@
import React from 'react';
// Inserts a space before rendering text.
export default React.createClass({
export default class Space extends React.Component {
displayName: 'Space.jsx',
displayName: 'Space.jsx'
constructor(props) {
super(props);
}
render() {
return <span>&nbsp;</span>;
}
});
}

View File

@ -2,18 +2,20 @@ import React from 'react';
import actions from '../../actions/appActions.js';
import Page from '../../lib/PageMixin.js';
import Page from '../../lib/PageClass.js';
import Notify from '../Notify.jsx';
import Header from '../Header.jsx';
import Footer from '../Footer.jsx';
import AddProjectForm from '../AddProjectForm.jsx';
export default React.createClass({
export default class AddProjectPage extends Page {
displayName: 'AddProjectPage.jsx',
displayName: 'AddProjectPage.jsx'
mixins: [ Page ],
constructor(props) {
super(props);
}
render() {
return (
@ -35,4 +37,4 @@ export default React.createClass({
);
}
});
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import Page from '../../lib/PageMixin.js';
import Page from '../../lib/PageClass.js';
import format from '../../modules/format.js';
@ -10,11 +10,13 @@ import Header from '../Header.jsx';
import Footer from '../Footer.jsx';
import Chart from '../Chart.jsx';
export default React.createClass({
export default class ChartPage extends Page {
displayName: 'ChartPage.jsx',
displayName: 'ChartPage.jsx'
mixins: [ Page ],
constructor(props) {
super(props);
}
render() {
let content;
@ -70,4 +72,4 @@ export default React.createClass({
);
}
});
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import Page from '../../lib/PageMixin.js';
import Page from '../../lib/PageClass.js';
import Notify from '../Notify.jsx';
import Header from '../Header.jsx';
@ -9,17 +9,18 @@ import Footer from '../Footer.jsx';
import Milestones from '../Milestones.jsx';
import Chart from '../Chart.jsx';
export default React.createClass({
export default class MilestonePage extends Page {
displayName: 'MilestonesPage.jsx',
displayName: 'MilestonesPage.jsx'
mixins: [ Page ],
constructor(props) {
super(props);
}
render() {
let content;
if (!this.state.app.system.loading) {
let projects = this.state.projects;
// Create the all milestones payload.
let data;
_.find(projects.list, (obj) => {
@ -83,4 +84,4 @@ export default React.createClass({
);
}
});
}

View File

@ -1,16 +1,18 @@
import React from 'react';
import Page from '../../lib/PageMixin.js';
import Page from '../../lib/PageClass.js';
// TODO: implement
export default React.createClass({
export default class NotFoundPage extends Page {
displayName: 'NotFoundPage.jsx',
displayName: 'NotFoundPage.jsx'
mixins: [ Page ],
constructor(props) {
super(props);
}
render() {
return <div>Page {this.props.path} not found</div>;
}
});
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import _ from 'lodash';
import Page from '../../lib/PageMixin.js';
import Page from '../../lib/PageClass.js';
import Notify from '../Notify.jsx';
import Header from '../Header.jsx';
@ -9,23 +10,23 @@ import Milestones from '../Milestones.jsx';
import EditProjects from '../EditProjects.jsx';
import Hero from '../Hero.jsx';
export default React.createClass({
export default class ProjectsPage extends Page {
displayName: 'ProjectsPage.jsx',
displayName: 'ProjectsPage.jsx'
mixins: [ Page ],
constructor(props) {
super(props);
// Start the page in a view mode.
// NOTE probably move into its own component so we don't merge state.
_.merge(this.state, { 'edit': false });
// Bindings.
this._onToggleMode = this._onToggleMode.bind(this);
}
// Toggle between edit and view mode.
_onToggleMode() {
this.setState({ 'edit': !this.state.edit });
},
getInitialState() {
return {
// Start the page in a view mode.
'edit': false
};
},
}
render() {
let content;
@ -66,4 +67,4 @@ export default React.createClass({
);
}
});
}

View File

@ -21,12 +21,12 @@ export default class EventEmitter {
// Add a listener on this path/regex.
on(path, cb) {
if (!_.isRegExp(path)) path = new RegExp(`^${path}$`);
this.list.push({ pattern: path, cb: cb });
this.list.push({ 'pattern': path, cb: cb });
}
// Add a listener to all events.
onAny(cb) {
this.list.push({ pattern: /./, cb: cb });
this.list.push({ 'pattern': /./, cb: cb });
}
// Assume we can have multiple.

56
src/js/lib/PageClass.js Normal file
View File

@ -0,0 +1,56 @@
import React from 'react';
import _ from 'lodash';
import stores from '../stores';
export default class Page extends React.Component {
constructor(props) {
super(props);
// State contains our store data.
// NOTE top-level page components shouldn't modify the state.
this.state = this._getData();
// Bindings.
this._onChange = this._onChange.bind(this);
}
// Get the POJO of the store.
_getData(store) {
let obj = {};
if (store) {
obj[store] = stores[store].get();
} else {
// Get all stores.
for (let key in stores) {
obj[key] = stores[key].get();
}
}
return obj;
}
// Update the state when store changes.
_onChange(store, val, key) {
if (!this._isMounted) return;
this.setState(this._getData(store));
}
// Listen to all events (data changes).
componentDidMount() {
this._isMounted = true;
for (let key in stores) {
stores[key].onAny(_.partial(this._onChange, key));
}
}
// Stop listening to store changes.
componentWillUnmount() {
this._isMounted = false;
for (let key in stores) {
stores[key].clean(this._onChange);
}
}
}

View File

@ -1,48 +0,0 @@
import _ from 'lodash';
import stores from '../stores';
export default {
// Get the POJO of the store.
_getData(store) {
let obj = {};
if (store) {
obj[store] = stores[store].get();
} else {
// Get all stores.
let key;
for (key in stores) {
obj[key] = stores[key].get();
}
}
return obj;
},
_onChange(store, val, key) {
if (this.isMounted()) { // not ideal
this.setState(this._getData(store));
}
},
getInitialState() {
return this._getData();
},
// Listen to all events (data changes).
componentDidMount() {
let key;
for (key in stores) {
stores[key].onAny(_.partial(this._onChange, key));
}
},
componentWillUnmount() {
let key;
for (key in stores) {
stores[key].clean(this._onChange);
}
}
};

View File

@ -73,7 +73,7 @@
}
&.action {
background: darken(@strong_color, 15%);
color: darken(@strong_color, 15%);
}
}
}