a crude header

This commit is contained in:
Radek Stepan 2016-01-12 18:10:06 +01:00
parent bcc3e52ef9
commit 5047977f47
16 changed files with 260 additions and 156 deletions

View File

@ -9,6 +9,8 @@
"deep-diff": "^0.3.3",
"lesshat": "^3.0.2",
"lodash": "^3.10.1",
"marked": "^0.3.5",
"moment": "^2.11.1",
"normalize.less": "^1.0.0",
"object-assign": "^4.0.1",
"object-path": "^0.9.2",

View File

@ -14,7 +14,7 @@ delete RouterMixin.handleClick;
// Values are function names below.
let routes = {
'/': 'projects',
'/new/project': 'add',
'/new/project': 'addProject',
'/:owner/:name': 'milestones',
'/:owner/:name/:milestone': 'chart',
'/demo': 'demo'
@ -87,7 +87,7 @@ export default React.createClass({
},
// Add a project.
add() {
addProject() {
return <AddProjectPage />;
},

View File

@ -1,43 +0,0 @@
import React from 'react';
import actions from '../actions/appActions.js';
export default React.createClass({
displayName: 'Comment.jsx',
// Save the input field value in our state.
_onChange(evt) {
this.setState({ 'value': evt.target.value });
},
_onAdd() {
// Emit the event.
actions.emit('articles.comment', {
id: this.props.id,
value: this.state.value
});
// Clear the input.
this.setState({ value: null });
},
getInitialState() {
return { value: null };
},
render() {
return (
<div>
<input
type="text"
value={this.state.value}
onChange={this._onChange}
placeholder="Comment..."
/>
<button onClick={this._onAdd}>Add</button>
</div>
);
}
});

View File

@ -0,0 +1,72 @@
import React from 'react';
import actions from '../actions/appActions.js';
import Icon from './Icon.jsx';
import Link from './Link.jsx';
export default React.createClass({
displayName: 'Header.jsx',
// Sign user in.
_onSignIn() {
actions.emit('user.signin');
},
// Sign user out.
_onSignOut() {
actions.emit('user.signout');
},
render() {
let props = this.props;
// Switch loading icon with app icon.
let icon = [ 'fire', 'spinner' ][ +props.system.loading ];
// Sign-in/out.
let user;
if (props.user.uid) {
user = (
<div className="right">
<a onClick={this._onSignOut}>
<Icon name="signout" /> Sign Out {props.user.github.displayName}
</a>
</div>
);
} else {
user = (
<div className="right">
<a className="button" onClick={this._onSignIn}>
<Icon name="github"/> Sign In
</a>
</div>
);
}
return (
<div id="head">
{user}
<Link route={{ to: 'projects' }} id="icon">
<Icon name={icon} />
</Link>
<ul>
<li>
<Link route={{ to: 'addProject' }}>
<Icon name="plus" /> Add a Project
</Link>
</li>
<li>
<Link route={{ to: 'demo' }}>
<Icon name="computer" /> See Examples
</Link>
</li>
</ul>
</div>
);
}
});

View File

@ -0,0 +1,46 @@
import React from 'react';
import Format from '../mixins/Format.js';
// Fontello icon hex codes.
let codes = {
'spyglass': '\e801', // Font Awesome - search
'plus': '\e804', // Font Awesome - plus-circled
'settings': '\e800', // Font Awesome - cog
'rocket': '\e80a', // Font Awesome - rocket
'computer': '\e807', // Font Awesome - desktop
'help': '\e80f', // Font Awesome - lifebuoy
'signout': '\e809', // Font Awesome - logout
'github': '\e802', // Font Awesome - github
'warning': '\e80c', // Entypo - attention
'direction': '\e803', // Entypo - address
'megaphone': '\e808', // Entypo - megaphone
'heart': '\e80e', // Typicons - heart
'sort': '\e806', // Typicons - sort-alphabet
'spinner': '\e80b', // MFG Labs - spinner1
'fire': '\e805' // Maki - fire-station
};
export default React.createClass({
displayName: 'Icon.jsx',
mixins: [ Format ],
render() {
let name = this.props.name;
if (name && name in codes) {
let code = this._hexToDec(codes[name]);
return (
<span
className={'icon ' + name}
dangerouslySetInnerHTML={{ '__html': '&#' + code + ';' }}
/>
);
}
return false;
}
});

View File

@ -16,7 +16,11 @@ export default React.createClass({
let link = App.link(route.to, route.params, route.query);
return (
<a href={'#!' + link} onClick={this._route.bind(this, link)}>
<a
{...this.props}
href={'#!' + link}
onClick={this._route.bind(this, link)}
>
{this.props.children}
</a>
);

View File

@ -0,0 +1,12 @@
import React from 'react';
export default React.createClass({
displayName: 'Header.jsx',
// TODO.
render() {
return false;
}
});

42
src/js/mixins/Format.js Normal file
View File

@ -0,0 +1,42 @@
import _ from 'lodash';
import moment from 'moment';
import marked from 'marked';
export default {
// Time from now.
// TODO: Memoize.
_fromNow(jsonDate) {
return moment(jsonDate, moment.ISO_8601).fromNow();
},
// When is a milestone due?
_due(jsonDate) {
if (!jsonDate) {
return '&nbsp;';
} else {
return [ 'due', this.fromNow(jsonDate) ].join(' ');
}
},
// Markdown formatting.
// TODO: works?
_markdown(...args) {
marked.apply(args);
},
// Format milestone title.
_title(text) {
if (text.toLowerCase().indexOf('milestone') > -1) {
return text;
} else {
return [ 'Milestone', text ].join(' ');
}
},
// Hex to decimal.
_hexToDec(hex) {
return parseInt(hex, 16);
}
};

View File

@ -1,43 +0,0 @@
import React from 'react';
import _ from 'lodash';
import Page from '../mixins/Page.js';
import Link from '../components/Link.jsx';
import Comment from '../components/Comment.jsx';
export default React.createClass({
displayName: 'ArticlePage.jsx',
mixins: [ Page ],
render() {
let store = this.state,
id = this.props.id;
// Find the article.
let article = _.find(store.articles, a => a.id == id);
// Any comments?
let comments;
if (article.comments) {
comments = (
<div>
{article.comments.map((t, i) => <div key={i}>{t}</div>)}
</div>
);
}
return (
<div>
<div>{article.title}</div>
<div>Lorem ipsum &hellip;</div>
<Link route={{ to: 'blog' }}>Back</Link>
{comments}
<Comment id={article.id} />
</div>
);
}
});

View File

@ -1,30 +0,0 @@
import React from 'react';
import Page from '../mixins/Page.js';
import Link from '../components/Link.jsx';
export default React.createClass({
displayName: 'BlogPage.jsx',
mixins: [ Page ],
render() {
let store = this.state;
// Map through the articles.
let articles = store.articles.map(a => {
return (
<div key={a.id}>
<Link route={{ to: 'article', params: { id: a.id } }}>
{a.title}
</Link>
</div>
);
});
return <div>{articles}</div>;
}
});

View File

@ -2,6 +2,9 @@ import React from 'react';
import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
export default React.createClass({
displayName: 'AddProjectPage.jsx',
@ -9,7 +12,20 @@ export default React.createClass({
mixins: [ Page ],
render() {
return <div>Add</div>;
return (
<div>
<Notify />
<Header {...this.state} />
<div id="page" />
<div id="footer">
<div className="wrap">
&copy; 2012-2016 <a href="https:/radekstepan.com" target="_blank">Radek Stepan</a>
</div>
</div>
</div>
);
}
});

View File

@ -2,6 +2,9 @@ import React from 'react';
import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
export default React.createClass({
displayName: 'ChartPage.jsx',
@ -9,7 +12,20 @@ export default React.createClass({
mixins: [ Page ],
render() {
return <div>Chart</div>;
return (
<div>
<Notify />
<Header {...this.state} />
<div id="page" />
<div id="footer">
<div className="wrap">
&copy; 2012-2016 <a href="https:/radekstepan.com" target="_blank">Radek Stepan</a>
</div>
</div>
</div>
);
}
});

View File

@ -2,6 +2,9 @@ import React from 'react';
import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
export default React.createClass({
displayName: 'MilestonesPage.jsx',
@ -9,7 +12,20 @@ export default React.createClass({
mixins: [ Page ],
render() {
return <div>Milestones</div>;
return (
<div>
<Notify />
<Header {...this.state} />
<div id="page" />
<div id="footer">
<div className="wrap">
&copy; 2012-2016 <a href="https:/radekstepan.com" target="_blank">Radek Stepan</a>
</div>
</div>
</div>
);
}
});

View File

@ -2,6 +2,9 @@ import React from 'react';
import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
export default React.createClass({
displayName: 'ProjectsPage.jsx',
@ -9,7 +12,20 @@ export default React.createClass({
mixins: [ Page ],
render() {
return <div>Projects</div>;
return (
<div>
<Notify />
<Header {...this.state} />
<div id="page" />
<div id="footer">
<div className="wrap">
&copy; 2012-2016 <a href="https:/radekstepan.com" target="_blank">Radek Stepan</a>
</div>
</div>
</div>
);
}
});

View File

@ -9,25 +9,13 @@ class AppStore extends Store {
// Initial payload.
constructor() {
super({
articles: [
{
id: 1,
title: 'Winklevoss Twins Aim to Take Bitcoin Mainstream with a Regulated Exchange',
url: 'nytimes.com'
}, {
id: 2,
title: 'Gotham Air: Manhattan to JFK in 6 minutes for $99',
url: 'gothamair.com'
}, {
id: 3,
title: 'Gitlet: Git implemented in JavaScript',
url: 'maryrosecook.com'
}
]
system: {
loading: true
},
user: {}
});
// Listen to all app actions
// articles.comment -> onArticlesComment
actions.onAny((obj, event) => {
let fn = ('on.' + event).replace(/[.]+(\w|$)/g, (m, p) => {
return p.toUpperCase();
@ -36,24 +24,13 @@ class AppStore extends Store {
(fn in this) && this[fn](obj);
});
}
// Add article comment action listener.
onArticlesComment(obj) {
let key;
// Find the article.
let article = _.find(this.get('articles'), (a, i) => {
if (a.id == obj.id) {
key = [ 'articles', i, 'comments' ];
return true;
}
});
// Init new or add to array.
if ('comments' in article) {
this.set(key.concat([ article.comments.length ]), obj.value);
} else {
this.set(key, [ obj.value ]);
}
onUserSignin() {
console.log('in');
}
onUserSignOut() {
console.log('out');
}
}

View File

@ -174,6 +174,7 @@ ul {
a {
color: #e0808d;
font-weight: bold;
.user-select(none);
&.active, &:hover {
color: #fff;