(select a file on the left)
src/components/label.js
import React from 'react'
import ampersandMixin from 'ampersand-react-mixin'
export default React.createClass({
mixins: [ampersandMixin],
displayName: 'Label',
getInitialState () {
const {name, color} = this.props.label
return {
name: name,
color: color
}
},
onEditClick () {
this.props.label.editing = true
},
onCancelClick () {
const {label} = this.props
if (label.saved) {
label.editing = false
this.setState(this.getInitialState())
} else {
label.collection.remove(label)
}
},
onNameChange (event) {
this.setState({
name: event.target.value
})
},
onColorChange (event) {
this.setState({
color: event.target.value.slice(1)
})
},
onSubmitForm (event) {
event.preventDefault()
const {label} = this.props
if (label.saved) {
label.update(this.state)
} else {
label.save(this.state)
}
label.editing = false
},
onDeleteClick (event) {
this.props.label.destroy()
},
render () {
const {label} = this.props
const {name, color} = this.state
const cssColor = '#' + color
let content
if (label.editing) {
content = (
<form onSubmit={this.onSubmitForm} className='label'>
<span style={{backgroundColor: cssColor}} className='label-color avatar avatar-small avatar-rounded'> </span>
<input onChange={this.onNameChange} value={name} name='name'/>
<input onChange={this.onColorChange} value={'#' + color} name='color'/>
<button type='submit' className='button button-small'>Save</button>
<button onClick={this.onCancelClick} type='button' className='button button-small button-unstyled'>cancel</button>
</form>
)
} else {
content = (
<div className='label'>
<span style={{backgroundColor: cssColor}} className='label-color'> </span>
<span>{label.name}</span>
<span onClick={this.onEditClick} className='octicon octicon-pencil'></span>
<span onClick={this.onDeleteClick} className='octicon octicon-x'></span>
</div>
)
}
return <div>{content}</div>
}
})
src/components/nav-helper.js
import app from 'ampersand-app'
import React from 'react'
import localLinks from 'local-links'
export default React.createClass({
displayName: 'NavHelper',
onClick (event) {
const pathname = localLinks.getLocalPathname(event)
if (pathname) {
event.preventDefault()
app.router.history.navigate(pathname)
}
},
render () {
return (
<div {...this.props} onClick={this.onClick}>
{this.props.children}
</div>
)
}
})
src/helpers/github-mixin.js
import app from 'ampersand-app'
export default {
ajaxConfig () {
return {
headers: {
Authorization: 'token ' + app.me.token
}
}
}
}
src/models/label-collection.js
import Collection from 'ampersand-rest-collection'
import Label from './label'
import githubMixin from '../helpers/github-mixin'
export default Collection.extend(githubMixin, {
url () {
return this.parent.url() + '/labels'
},
model: Label
})
src/models/label.js
import app from 'ampersand-app'
import Model from 'ampersand-model'
import xhr from 'xhr'
import githubMixin from '../helpers/github-mixin'
export default Model.extend(githubMixin, {
idAttribute: 'name',
props: {
name: 'string',
color: 'string'
},
session: {
editing: {
type: 'boolean',
default: false
},
saved: {
type: 'boolean',
default: true
}
},
isNew () {
return !this.saved
},
update (newAttributes) {
const old = this.attributes
xhr({
url: this.url(),
json: newAttributes,
headers: {
Authorization: 'token ' + app.me.token
},
method: 'PATCH'
}, (err, resp, body) => {
if (err) {
this.set(old)
console.error('check yo wifi')
}
})
this.set(newAttributes)
}
})
src/models/me.js
import Model from 'ampersand-model'
import RepoCollection from './repo-collection'
import githubMixin from '../helpers/github-mixin'
export default Model.extend(githubMixin, {
url: 'https://api.github.com/user',
initialize () {
this.token = window.localStorage.token
this.on('change:token', this.onChangeToken)
},
props: {
id: 'number',
login: 'string',
avatar_url: 'string'
},
session: {
token: 'string'
},
collections: {
repos: RepoCollection
},
onChangeToken () {
window.localStorage.token = this.token
this.fetchInitialData()
},
fetchInitialData () {
if (this.token) {
this.fetch()
this.repos.fetch()
}
}
})
src/models/repo-collection.js
import Collection from 'ampersand-rest-collection'
import Repo from './repo'
import githubMixin from '../helpers/github-mixin'
export default Collection.extend(githubMixin, {
url: 'https://api.github.com/user/repos',
model: Repo,
getByFullName (fullName) {
let model = this.findWhere({full_name: fullName})
if (!model) {
model = new Repo({full_name: fullName})
}
model.fetch()
return model
}
})
src/models/repo.js
import Model from 'ampersand-model'
import githubMixin from '../helpers/github-mixin'
import LabelCollection from './label-collection'
export default Model.extend(githubMixin, {
url () {
return 'https://api.github.com/repos/' + this.full_name
},
props: {
id: 'number',
name: 'string',
full_name: 'string'
},
collections: {
labels: LabelCollection
},
derived: {
app_url: {
deps: ['full_name'],
fn () {
return 'repo/' + this.full_name
}
}
},
fetch () {
Model.prototype.fetch.apply(this, arguments)
this.labels.fetch()
}
})
src/pages/message.js
import React from 'react'
export default React.createClass({
displayName: 'MessagePage',
render () {
return (
<div>
<h1>{this.props.title}</h1>
<p>{this.props.body}</p>
</div>
)
}
})
src/pages/public.js
import React from 'react'
import NavHelper from '../components/nav-helper'
export default React.createClass({
displayName: 'PublicPage',
render () {
return (
<NavHelper className='container'>
<header role='banner'>
<h1>Labelr</h1>
</header>
<div>
<p>We label stuff for you, because, we can™</p>
<a href='/login' className='button button-large'>
<span className='mega-octicon octicon-mark-github'></span> Login with GitHub
</a>
</div>
</NavHelper>
)
}
})
src/pages/repo-detail.js
import React from 'react'
import ampersandMixin from 'ampersand-react-mixin'
import Label from '../components/label'
export default React.createClass({
mixins: [ampersandMixin],
displayName: 'RepoDetailPage',
onAddClick () {
this.props.labels.add({
name: '',
color: '',
editing: true,
saved: false
}, {at: 0})
},
render () {
const {repo, labels} = this.props
return (
<div className='container'>
<h1>{repo.full_name} Labels</h1>
<p>
<button onClick={this.onAddClick} className='button'>Add a label</button>
</p>
<ul>
{labels.map((label) =>
<Label key={label.name} label={label}/>
)}
</ul>
</div>
)
}
})
src/pages/repos.js
import React from 'react'
import ampersandMixin from 'ampersand-react-mixin'
export default React.createClass({
mixins: [ampersandMixin],
displayName: 'ReposPage',
render () {
const {repos} = this.props
return (
<div>
<h1>Repos page</h1>
<div>
{repos.map((repo) => {
return (
<div key={repo.id}>
<a href={repo.app_url}><span className='octicon octicon-repo'></span> {repo.full_name}</a>
</div>
)
})}
</div>
</div>
)
}
})
src/styles/main.styl
@import 'yeticss'
header
padding-top: 50px
.label
height: 40px
.label-color
width: 24px
height: 24px
border: 1px solid grey
border-radius: 20px
display: inline-block
> span
margin-left: 10px
margin-right: 10px
src/app.js
import app from 'ampersand-app'
import styles from './styles/main.styl'
import icons from '../node_modules/octicons/octicons/octicons.css'
import Router from './router'
import Me from './models/me'
window.app = app
app.extend({
init () {
this.me = new Me()
this.me.fetchInitialData()
this.router = new Router()
this.router.history.start()
}
})
app.init()
src/config.js
export default {
'localhost': {
clientId: 'f8dd69187841cdd22a26',
gatekeeperUrl: 'https://labelr-localhost.herokuapp.com/authenticate'
},
'labelr.surge.sh': {
clientId: '9cc77faf5ffc6f6f9b9a',
gatekeeperUrl: 'https://labelr-production.herokuapp.com/authenticate'
}
}[window.location.hostname]
src/layout.js
import React from 'react'
import NavHelper from './components/nav-helper'
import ampersandMixin from 'ampersand-react-mixin'
export default React.createClass({
mixins: [ampersandMixin],
displayName: 'Layout',
render () {
const {me} = this.props
return (
<NavHelper>
<nav className='top-nav top-nav-light cf' role='navigation'>
<input id='menu-toggle' className='menu-toggle' type='checkbox'/>
<label htmlFor='menu-toggle'>Menu</label>
<ul className='list-unstyled list-inline cf'>
<li>Labelr</li>
<li><a href='/repos'>Repos</a></li>
<li className='pull-right'>{me.login} <a href='/logout'>Logout</a></li>
</ul>
</nav>
<div className='container'>
{this.props.children}
</div>
</NavHelper>
)
}
})
src/router.js
import app from 'ampersand-app'
import React from 'react'
import qs from 'qs'
import xhr from 'xhr'
import uuid from 'node-uuid'
import Router from 'ampersand-router'
import PublicPage from './pages/public'
import ReposPage from './pages/repos'
import RepoDetailPage from './pages/repo-detail'
import MessagePage from './pages/message'
import Layout from './layout'
import config from './config'
export default Router.extend({
renderPage (page, opts = {layout: true}) {
if (opts.layout) {
page = (
<Layout me={app.me}>
{page}
</Layout>
)
}
React.render(page, document.body)
},
routes: {
'': 'public',
'repos': 'repos',
'login': 'login',
'logout': 'logout',
'repo/:owner/:name': 'repoDetail',
'auth/callback?:query': 'authCallback',
'*fourohfour': 'fourOhFour'
},
public () {
this.renderPage(<PublicPage/>, {layout: false})
},
repos () {
this.renderPage(<ReposPage repos={app.me.repos}/>)
},
repoDetail (owner, name) {
const repo = app.me.repos.getByFullName(owner + '/' + name)
this.renderPage(<RepoDetailPage repo={repo} labels={repo.labels}/>)
},
login () {
const state = uuid()
window.localStorage.state = state
window.location = 'https://github.com/login/oauth/authorize?' + qs.stringify({
client_id: config.clientId,
redirect_uri: window.location.origin + '/auth/callback',
scope: 'user,repo',
state: state
})
},
logout () {
window.localStorage.clear()
window.location = '/'
},
authCallback (query) {
query = qs.parse(query)
if (query.state === window.localStorage.state) {
delete window.localStorage.state
xhr({
url: config.gatekeeperUrl + '/' + query.code,
json: true
}, (err, resp, body) => {
if (err) {
console.error('something went wrong')
} else {
app.me.token = body.token
this.redirectTo('/repos')
}
})
this.renderPage(<MessagePage title='Fetching data from GitHub'/>)
}
},
fourOhFour () {
this.renderPage(<MessagePage title='Page not found'/>)
}
})
package.json
{
"name": "labelr",
"version": "1.0.0",
"description": "A really awesome way to manage labels for github issues.",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"prebuild": "rm -rf public && mkdir public",
"build": "NODE_ENV=production webpack",
"deploy": "surge -p public -d labelr.surge.sh",
"yolo": "npm run build && npm run deploy"
},
"author": "Henrik Joreteg <henrik@andyet.net>",
"license": "MIT",
"dependencies": {
"ampersand-app": "^1.0.4",
"ampersand-model": "^5.0.3",
"ampersand-react-mixin": "^0.1.3",
"ampersand-rest-collection": "^4.0.0",
"ampersand-router": "^3.0.2",
"autoprefixer-stylus": "^0.5.0",
"babel": "^5.1.13",
"babel-core": "^5.1.13",
"babel-loader": "^5.0.0",
"css-loader": "^0.12.0",
"file-loader": "^0.8.1",
"hjs-webpack": "^2.0.1",
"local-links": "^1.4.0",
"node-uuid": "^1.4.3",
"octicons": "^2.2.0",
"qs": "^2.4.1",
"react": "^0.13.2",
"react-hot-loader": "^1.2.5",
"style-loader": "^0.12.1",
"stylus-loader": "^1.1.0",
"surge": "^0.11.1",
"url-loader": "^0.5.5",
"webpack": "^1.8.11",
"webpack-dev-server": "^1.8.2",
"xhr": "^2.0.1",
"yeticss": "^6.0.5"
}
}
webpack.config.js
var getConfig = require('hjs-webpack')
module.exports = getConfig({
in: 'src/app.js',
out: 'public',
isDev: process.env.NODE_ENV !== 'production',
html: function (context) {
return {
'200.html': context.defaultTemplate(),
'index.html': context.defaultTemplate()
}
}
})