1
0
mirror of https://github.com/ascribe/onion.git synced 2024-11-15 01:25:17 +01:00

Merge branch 'piece-detail-setup'

Conflicts:
	js/components/ascribe_table/table.js
	js/components/ascribe_table/table_item_subtable.js
This commit is contained in:
vrde 2015-05-29 10:50:49 +02:00
commit d4e55f649d
47 changed files with 1273 additions and 133 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ node_modules
npm-debug.log npm-debug.log
build/app.js build/app.js
.DS_Store

View File

@ -18,9 +18,7 @@ Install some nice extensions for Chrom(e|ium):
git clone git@bitbucket.org:ascribe/onion.git git clone git@bitbucket.org:ascribe/onion.git
cd onion cd onion
npm install npm install
npm run watch gulp serve
python -mSimpleHTTPServer
``` ```

View File

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'ascribe'; font-family: 'ascribe';
src:url('fonts/ascribe.eot?-6bb2dq'); src:url('ascribe.eot?-6bb2dq');
src:url('fonts/ascribe.eot?#iefix-6bb2dq') format('embedded-opentype'), src:url('ascribe.eot?#iefix-6bb2dq') format('embedded-opentype'),
url('fonts/ascribe.woff?-6bb2dq') format('woff'), url('ascribe.woff?-6bb2dq') format('woff'),
url('fonts/ascribe.ttf?-6bb2dq') format('truetype'), url('ascribe.ttf?-6bb2dq') format('truetype'),
url('fonts/ascribe.svg?-6bb2dq#ascribe') format('svg'); url('ascribe.svg?-6bb2dq#ascribe') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -187,3 +187,7 @@
content: "\eae9"; content: "\eae9";
} }
.btn-glyph-ascribe{
font-size: 18px;
padding: 4px 12px 0 10px
}

View File

@ -12,15 +12,17 @@
.ascribe-table-header-row { .ascribe-table-header-row {
border-bottom: 2px solid rgba(2, 182, 163, 0.5); border-bottom: 2px solid rgba(2, 182, 163, 0.5);
border-top: 2px solid rgba(2, 182, 163, 0.5); border-top: 2px solid rgba(2, 182, 163, 0.5);
padding: 0;
} }
.ascribe-table-header-column { .ascribe-table-header-column {
display: table; display: table;
height:4em; height:3em;
padding: 0;
} }
.ascribe-table-header-column > span { .ascribe-table-header-column > span {
display:table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-weight: 600; font-weight: 600;
@ -31,10 +33,10 @@
.ascribe-table-header-column > span > .glyphicon { .ascribe-table-header-column > span > .glyphicon {
font-size: .5em; font-size: .5em;
} }
/*
.ascribe-table-item:nth-child(even) { .ascribe-table-item:nth-child(even) {
background-color: #F5F5F5; background-color: #F5F5F5;
} }*/
/*.ascribe-table-item:hover { /*.ascribe-table-item:hover {
background-color: #EEEEEE; background-color: #EEEEEE;
@ -44,15 +46,168 @@
display: table; display: table;
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
font-size: 1.2em; font-size: 1.2em;
height:4em; height:3em;
} }
.ascribe-table-item-column > * { .ascribe-table-item-column > * {
display:table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
} }
.btn-ascribe, .btn-ascribe:hover, .btn-ascribe:active, .btn-ascribe:focus { .ascribe-table-item-selected {
background-color: rgba(2, 182, 163, 0.5); background-color: rgba(2, 182, 163, 0.5);
border-color: rgba(2, 182, 163, 0.5); }
.ascribe-table-item-selectable {
cursor: default;
}
.piece-list-toolbar {
height:3em;
}
.no-margin {
margin-right: 0;
margin-left: 0;
}
.btn-ascribe, .btn-ascribe-inv {
border: 1px solid #444;
line-height: 2em;
margin-right: 1px;
margin-left: 0 !important;
font-family: sans-serif !important;
border-radius: 0 !important;
}
.btn-ascribe, .btn-ascribe-inv:active, .btn-ascribe-inv:hover {
color: #222 !important;
background-color: #FFF;
}
.btn-ascribe:active, .btn-ascribe:hover, .btn-ascribe-inv {
color: #FFF !important;
background-color: #444;
}
.btn-ascribe-inv:disabled, .btn-ascribe-inv:focus {
color: #444 !important;
background-color: #BBB !important;
border: 1px solid #444 !important;
}
.btn-ascribe-sm {
font-size: 12px;
line-height: 1.3em;
}
.btn-ascribe-green, .btn-ascribe-green-inv {
border: 1px solid #48DACB;
line-height: 2em;
margin-left: 0 !important;
font-family: sans-serif !important;
border-radius: 0 !important;
}
.btn-ascribe-green, .btn-ascribe-green-inv:active, .btn-ascribe-green-inv:hover {
background-color: #FFF;
border: 1px solid rgba(2, 182, 163, 0.5);
color: rgba(2, 182, 163, 0.5);
}
.btn-ascribe-green:active, .btn-ascribe-green:hover, .btn-ascribe-green-inv {
border: 1px solid rgba(2, 182, 163, 0.5);
color: white;
background-color: rgba(2, 182, 163, 0.5);
}
.ascribe-detail-header {
margin-top: 2em;
}
.ascribe-detail-title {
font-size: 2em;
margin-bottom: -0.2em;
}
.ascribe-detail-property {
padding-bottom: 0.4em;
}
.ascribe-detail-property > .row-same-height > .col-xs-2 {
text-transform: uppercase;
}
.input-text-ascribe {
border-bottom: 1px solid black;
border-top: 0;
border-left: 0;
border-right: 0;
background: transparent;
border-radius: 0 !important;
box-shadow: none;
}
.textarea-ascribe-message {
height: 13em !important;
}
/* columns of same height styles */
/* http://www.minimit.com/articles/solutions-tutorials/bootstrap-3-responsive-columns-of-same-height */
.row-full-height {
height: 100%;
}
.col-full-height {
height: 100%;
vertical-align: middle;
}
.row-same-height {
display: table;
width: 100%;
/* fix overflow */
table-layout: fixed;
}
.col-xs-height {
display: table-cell;
float: none !important;
}
@media (min-width: 768px) {
.col-sm-height {
display: table-cell;
float: none !important;
}
}
@media (min-width: 992px) {
.col-md-height {
display: table-cell;
float: none !important;
}
}
@media (min-width: 1200px) {
.col-lg-height {
display: table-cell;
float: none !important;
}
}
/* vertical alignment styles */
.col-top {
vertical-align: top;
}
.col-middle {
vertical-align: middle;
}
.col-bottom {
vertical-align: bottom;
} }

67
gulpfile.js Normal file
View File

@ -0,0 +1,67 @@
var gulp = require('gulp');
var gulpif = require('gulp-if');
var sourcemaps = require('gulp-sourcemaps');
var util = require('gulp-util');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var watchify = require('watchify');
var browserify = require('browserify');
var browserSync = require('browser-sync');
var babelify = require('babelify');
var notify = require('gulp-notify');
var _ = require('lodash');
gulp.task('build', function() {
bundle(false);
});
gulp.task('serve', ['browser-sync'], function() {
bundle(true);
});
gulp.task('browser-sync', function() {
browserSync({
server: {
baseDir: "."
},
port: process.env.PORT || 3000
});
});
function bundle(watch) {
var bro;
if (watch) {
bro = watchify(browserify('./js/app.js',
// Assigning debug to have sourcemaps
_.assign(watchify.args, {
debug: true
})));
bro.on('update', function() {
rebundle(bro, true);
});
} else {
bro = browserify('./js/app.js', {
debug: true
});
}
bro.transform(babelify.configure({
compact: false
}));
function rebundle(bundler, watch) {
return bundler.bundle()
.on('error', notify.onError('Error: <%= error.message %>'))
.pipe(source('app.js'))
.pipe(buffer())
.pipe(sourcemaps.init({
loadMaps: true
})) // loads map from browserify file
.pipe(sourcemaps.write()) // writes .map file
.pipe(gulp.dest('./build'))
.pipe(browserSync.stream());
}
return rebundle(bro);
}

View File

@ -5,13 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ascribe</title> <title>ascribe</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"></link> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<link rel="stylesheet" href="css/main.css"></link> <link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/ascribe-fonts/ascribe-fonts.css"></link> <link rel="stylesheet" href="css/ascribe-fonts/ascribe-fonts.css">
<link rel="stylesheet" href="css/ascribe-fonts/style.css">
<link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900"> <link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900">
</head> </head>
<body> <body>
<div id="main" class="container"></div> <div id="main" class="container"></div>
<div id="modal" class="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>

View File

@ -0,0 +1,23 @@
import alt from '../alt';
import EditionFetcher from '../fetchers/edition_fetcher';
class EditionActions {
constructor() {
this.generateActions(
'updateEdition'
);
}
fetchOne(editionId) {
EditionFetcher.fetchOne(editionId)
.then((res) => {
this.actions.updateEdition(res.edition);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(EditionActions);

View File

@ -5,7 +5,8 @@ import EditionListFetcher from '../fetchers/edition_list_fetcher.js';
class EditionListActions { class EditionListActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateEditionList' 'updateEditionList',
'selectEdition'
); );
} }
@ -14,7 +15,7 @@ class EditionListActions {
.fetch(pieceId) .fetch(pieceId)
.then((res) => { .then((res) => {
this.actions.updateEditionList({ this.actions.updateEditionList({
'editionList': res.editions, 'editionListOfPiece': res.editions,
pieceId pieceId
}); });
}) })
@ -22,6 +23,6 @@ class EditionListActions {
console.log(err); console.log(err);
}); });
} }
}; }
export default alt.createActions(EditionListActions); export default alt.createActions(EditionListActions);

View File

@ -0,0 +1,23 @@
import alt from '../alt';
import PieceFetcher from '../fetchers/piece_fetcher';
class PieceActions {
constructor() {
this.generateActions(
'updatePiece'
);
}
fetchOne(pieceId) {
PieceFetcher.fetchOne(pieceId)
.then((res) => {
this.actions.updatePiece(res.piece);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(PieceActions);

View File

@ -0,0 +1,32 @@
import React from 'react';
import AppConstants from '../constants/application_constants';
let AclButton = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.array.isRequired
},
render() {
let shouldDisplay = this.props.availableAcls.indexOf(this.props.action) > -1;
let styles = {};
if(shouldDisplay) {
styles.display = 'inline-block';
} else {
styles.display = 'none';
}
return (
<button
style={styles}
type="button"
className="btn btn-default btn-sm">
{this.props.action.toUpperCase()}
</button>
);
}
});
export default AclButton;

View File

@ -0,0 +1,31 @@
import React from 'react';
import Alert from 'react-bootstrap/lib/Alert';
let AlertDismissable = React.createClass({
getInitialState() {
return {
alertVisible: true
};
},
show() {
this.setState({alertVisible: true});
},
hide() {
this.setState({alertVisible: false});
},
render() {
if (this.state.alertVisible) {
let key = this.props.error;
return (
<Alert bsStyle='danger' onDismiss={this.hide}>
{this.props.error}
</Alert>
);
}
return (
<span />
);
}
});
export default AlertDismissable;

View File

@ -0,0 +1,21 @@
import React from 'react';
let ButtonSubmitOrClose = React.createClass({
render() {
if (this.props.submitted){
return (
<div className="modal-footer">
Loading
</div>
)
}
return (
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv">{this.props.text}</button>
<button className="btn btn-ascribe-inv" onClick={this.props.onClose}>CLOSE</button>
</div>
)
}
});
export default ButtonSubmitOrClose;

View File

@ -0,0 +1,53 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from './button_submit_close';
let ShareForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_shares_mail
},
getFormData() {
return {
bitcoin_id: this.props.edition.bitcoin_id,
share_emails: this.refs.share_emails.state.value,
share_message: this.refs.share_message.state.value
}
},
renderForm() {
let message = "Hi,\n" +
"\n" +
"I am sharing \"" + this.props.edition.title + "\" with you.\n" +
"\n" +
"Truly yours,\n" +
this.props.currentUser.username;
return (
<form id="share_modal_content" role="form" key="share_modal_content" onSubmit={this.submit}>
<InputText
ref="share_emails"
placeHolder="Comma separated emails"
required="required"
type="text"
submitted={this.state.submitted}/>
<InputTextArea
ref="share_message"
defaultValue={message}
required=""
/>
<ButtonSubmitOrClose
text="SHARE"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default ShareForm;

View File

@ -0,0 +1,68 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from './button_submit_close';
let TransferForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_transfers
},
getFormData() {
return {
bitcoin_id: this.props.edition.bitcoin_id,
transferee: this.refs.transferee.state.value,
transfer_message: this.refs.transfer_message.state.value,
password: this.refs.password.state.value
}
},
renderForm() {
let message = "Hi,\n" +
"\n" +
"I transfer ownership of \"" + this.props.edition.title + "\" to you.\n" +
"\n" +
"Truly yours,\n" +
this.props.currentUser.username;
return (
<form id="transfer_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_transferee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputText
ref="transferee"
placeHolder="Transferee email"
required="required"
type="email"
submitted={this.state.submitted}/>
<InputTextArea
ref="transfer_message"
defaultValue={message}
required=""
/>
<InputText
ref="password"
placeHolder="Password"
required="required"
type="password"
submitted={this.state.submitted}/>
<div>
Make sure that display instructions and technology details are correct.
They cannot be edited after the transfer.
</div>
<ButtonSubmitOrClose
text="TRANSFER"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default TransferForm;

View File

@ -0,0 +1,35 @@
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'
let InputText = React.createClass({
mixins : [AlertMixin],
getInitialState() {
return {value: null,
alerts: null, // needed in AlertMixin
retry: 0 // needed in AlertMixin for generating unique alerts
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let className = "form-control input-text-ascribe";
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<input className={className}
placeholder={this.props.placeHolder}
required={this.props.required}
type={this.props.type}
onChange={this.handleChange}/>
</div>
);
}
});
export default InputText;

View File

@ -0,0 +1,35 @@
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'
let InputTextArea = React.createClass({
mixins : [AlertMixin],
getInitialState() {
return {value: this.props.defaultValue,
alerts: null, // needed in AlertMixin
retry: 0 // needed in AlertMixin for generating unique alerts
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let className = "form-control input-text-ascribe textarea-ascribe-message";
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<textarea className={className}
defaultValue={this.props.defaultValue}
required={this.props.required}
onChange={this.handleChange}></textarea>
</div>
);
}
});
export default InputTextArea;

View File

@ -0,0 +1,25 @@
import React from 'react';
/**
* This is the component that implements display-specific functionality
*/
let ImageViewer = React.createClass({
propTypes: {
thumbnail: React.PropTypes.string.isRequired
},
render() {
let thumbnail = <img className="img-responsive" src={this.props.thumbnail}/>;
let aligner = <span className="vcenter"></span>;
return (
<div>
<div>
{aligner}
{thumbnail}
</div>
</div>
);
}
});
export default ImageViewer;

View File

@ -0,0 +1,46 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import ShareForm from '../ascribe_forms/form_share_email'
let ShareModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left" overlay={<Tooltip>Share the artwork</Tooltip>}>
<ModalTrigger modal={<ShareModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv btn-glyph-ascribe">
<span className="glyph-ascribe-share2"></span>
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let ShareModal = React.createClass({
onRequestHide(e){
if (e)
e.preventDefault();
this.props.onRequestHide();
},
render() {
return (
<Modal {...this.props} title="Share artwork">
<div className="modal-body">
<ShareForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default ShareModalButton;

View File

@ -0,0 +1,45 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import TransferForm from '../ascribe_forms/form_transfer'
let TransferModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>Transfer the ownership of the artwork</Tooltip>}>
<ModalTrigger modal={<TransferModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv">
TRANSFER
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let TransferModal = React.createClass({
onRequestHide(e){
e.preventDefault();
this.props.onRequestHide();
},
render() {
return (
<Modal {...this.props} title="Transfer artwork">
<div className="modal-body">
<TransferForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default TransferModalButton;

View File

@ -0,0 +1,82 @@
import React from 'react';
import EditionListStore from '../../stores/edition_list_store';
import AclButton from '../acl_button';
let PieceListToolbar = React.createClass({
getInitialState() {
return EditionListStore.getState();
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
EditionListStore.listen(this.onChange)
},
componentDidUnmount() {
EditionListStore.unlisten(this.onChange)
},
filterForSelected(edition) {
return edition.selected;
},
fetchSelectedEditionList() {
let selectedEditionList = [];
Object
.keys(this.state.editionList)
.forEach((key) => {
let filteredEditionsForPiece = this.state.editionList[key].filter(this.filterForSelected);
selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece);
});
return selectedEditionList;
},
intersectAcls(a, b) {
return a.filter((val) => b.indexOf(val) > -1);
},
getAvailableAcls() {
let availableAcls = [];
let selectedEditionList = this.fetchSelectedEditionList();
// If no edition has been selected, availableActions is empty
// If only one edition has been selected, their actions are available
// If more than one editions have been selected, their acl properties are intersected
if(selectedEditionList.length >= 1) {
availableAcls = selectedEditionList[0].acl;
}
if(selectedEditionList.length >= 2) {
for(let i = 1; i < selectedEditionList.length; i++) {
availableAcls = this.intersectAcls(availableAcls, selectedEditionList[i].acl);
}
}
return availableAcls;
},
render() {
let availableAcls = this.getAvailableAcls();
return (
<div className="row no-margin">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 piece-list-toolbar">
<div className="pull-right">
<AclButton availableAcls={availableAcls} action="transfer" />
<AclButton availableAcls={availableAcls} action="consign" />
<AclButton availableAcls={availableAcls} action="share" />
<AclButton availableAcls={availableAcls} action="loan" />
</div>
</div>
</div>
);
}
});
export default PieceListToolbar;

View File

@ -27,12 +27,13 @@ let Table = React.createClass({
if(this.props.itemList && this.props.itemList.length > 0) { if(this.props.itemList && this.props.itemList.length > 0) {
return ( return (
<div className="ascribe-table"> <div className="ascribe-table">
<TableHeader columnList={this.props.columnList} <TableHeader
itemList={this.props.itemList} columnList={this.props.columnList}
fetchList={this.props.fetchList} itemList={this.props.itemList}
changeOrder={this.props.changeOrder} fetchList={this.props.fetchList}
orderAsc={this.props.orderAsc} changeOrder={this.props.changeOrder}
orderBy={this.props.orderBy} /> orderAsc={this.props.orderAsc}
orderBy={this.props.orderBy} />
{this.renderChildren()} {this.renderChildren()}
</div> </div>
); );

View File

@ -12,9 +12,9 @@ let TableHeader = React.createClass({
propTypes: { propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)), columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
itemList: React.PropTypes.array.isRequired, itemList: React.PropTypes.array.isRequired,
changeOrder: React.PropTypes.func.isRequired, changeOrder: React.PropTypes.func,
orderAsc: React.PropTypes.bool.isRequired, orderAsc: React.PropTypes.bool,
orderBy: React.PropTypes.string.isRequired orderBy: React.PropTypes.string
}, },
render() { render() {
@ -23,7 +23,7 @@ let TableHeader = React.createClass({
<div className="row"> <div className="row">
{this.props.columnList.map((val, i) => { {this.props.columnList.map((val, i) => {
let columnClasses = this.calcColumnClasses(this.props.columnList, i); let columnClasses = this.calcColumnClasses(this.props.columnList, i, 12);
let columnName = this.props.columnList[i].columnName; let columnName = this.props.columnList[i].columnName;
let canBeOrdered = this.props.columnList[i].canBeOrdered; let canBeOrdered = this.props.columnList[i].canBeOrdered;

View File

@ -8,10 +8,10 @@ let TableHeaderItem = React.createClass({
columnClasses: React.PropTypes.string.isRequired, columnClasses: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string.isRequired, displayName: React.PropTypes.string.isRequired,
columnName: React.PropTypes.string.isRequired, columnName: React.PropTypes.string.isRequired,
canBeOrdered: React.PropTypes.bool.isRequired, canBeOrdered: React.PropTypes.bool,
changeOrder: React.PropTypes.func.isRequired, changeOrder: React.PropTypes.func,
orderAsc: React.PropTypes.bool.isRequired, orderAsc: React.PropTypes.bool,
orderBy: React.PropTypes.string.isRequired orderBy: React.PropTypes.string
}, },
changeOrder() { changeOrder() {
@ -19,7 +19,7 @@ let TableHeaderItem = React.createClass({
}, },
render() { render() {
if(this.props.canBeOrdered) { if(this.props.canBeOrdered && this.props.changeOrder && this.props.orderAsc != null && this.props.orderBy) {
if(this.props.columnName === this.props.orderBy) { if(this.props.columnName === this.props.orderBy) {
return ( return (
<div <div

View File

@ -1,38 +1,30 @@
import React from 'react'; import React from 'react';
import TableColumnMixin from '../../mixins/table_column_mixin';
import TableColumnContentModel from '../../models/table_column_content_model'; import TableColumnContentModel from '../../models/table_column_content_model';
import TableItemWrapper from './table_item_wrapper';
let TableItem = React.createClass({ let TableItem = React.createClass({
mixins: [TableColumnMixin],
propTypes: { propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)), columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object columnContent: React.PropTypes.object,
onClick: React.PropTypes.func, // See: https://facebook.github.io/react/tips/expose-component-functions.html
classNames: React.PropTypes.string
}, },
render() { render() {
let calcColumnElementContent = () => {
return this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
});
};
return ( return (
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item"> <div
className={this.props.classNames + ' col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item'}
onClick={this.props.onClick}>
<div className="row"> <div className="row">
{calcColumnElementContent()} <TableItemWrapper
columnList={this.props.columnList}
columnContent={this.props.columnContent}
columnWidth={12}>
</TableItemWrapper>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,18 @@
import React from 'react';
let TableItemAcl = React.createClass({
propTypes: {
content: React.PropTypes.array.isRequired
},
render() {
return (
<span>
{this.props.content.join('/')}
</span>
);
}
});
export default TableItemAcl;

View File

@ -0,0 +1,38 @@
import React from 'react';
import classNames from 'classnames';
import TableColumnContentModel from '../../models/table_column_content_model';
import TableItem from './table_item';
// This component is implemented as recommended here: http://stackoverflow.com/a/25723635/1263876
let TableItemSelectable = React.createClass({
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object,
parentId: React.PropTypes.number,
className: React.PropTypes.string
},
selectItem() {
this.props.selectItem(this.props.parentId, this.props.columnContent.edition_number);
},
render() {
let tableItemClasses = classNames({
'ascribe-table-item-selected': this.props.columnContent.selected
});
return (
<TableItem
classNames={tableItemClasses + ' ' + this.props.className}
columnList={this.props.columnList}
columnContent={this.props.columnContent}
onClick={this.selectItem}>
</TableItem>
);
}
});
export default TableItemSelectable;

View File

@ -5,12 +5,12 @@ import TableColumnContentModel from '../../models/table_column_content_model';
import EditionListStore from '../../stores/edition_list_store'; import EditionListStore from '../../stores/edition_list_store';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
// ToDo: Create Table-specific Utils to not lock it to projects utilities
import GeneralUtils from '../../utils/general_utils';
import Table from './table'; import Table from './table';
import TableItem from './table_item'; import TableItemWrapper from './table_item_wrapper';
import TableItemText from './table_item_text'; import TableItemText from './table_item_text';
import TableItemAcl from './table_item_acl';
import TableItemSelectable from './table_item_selectable';
import TableItemSubtableButton from './table_item_subtable_button'; import TableItemSubtableButton from './table_item_subtable_button';
@ -40,6 +40,7 @@ let TableItemSubtable = React.createClass({
'open': false 'open': false
}); });
} else { } else {
EditionListActions.fetchEditionList(this.props.columnContent.id); EditionListActions.fetchEditionList(this.props.columnContent.id);
this.setState({ this.setState({
'open': true, 'open': true,
@ -48,43 +49,21 @@ let TableItemSubtable = React.createClass({
} }
}, },
calcColumnClasses(list, i) { selectItem(parentId, itemId) {
let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-']; EditionListActions.selectEdition({
'pieceId': parentId,
let listOfRowValues = list.map((column) => column.rowWidth ); 'editionId': itemId
let numOfColumns = GeneralUtils.sumNumList(listOfRowValues); });
if(numOfColumns > 10) {
throw new Error('Bootstrap has only 12 columns to assign. You defined ' + numOfColumns + '. Change this in the columnMap you\'re passing to the table.')
} else {
return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i];
}
}, },
render() { render() {
let calcColumnElementContent = () => {
return this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
});
};
let renderEditionListTable = () => { let renderEditionListTable = () => {
let columnList = [ let columnList = [
new TableColumnContentModel('edition_number', 'Edition Number', TableItemText, 2, false), new TableColumnContentModel('edition_number', 'Edition Number', TableItemText, 2, false),
new TableColumnContentModel('user_registered', 'User', TableItemText, 4, true), new TableColumnContentModel('user_registered', 'User', TableItemText, 4, true),
new TableColumnContentModel('bitcoin_id', 'Bitcoin Address', TableItemText, 4, true) new TableColumnContentModel('acl', 'Actions', TableItemAcl, 4, true)
]; ];
if(this.state.open && this.state.editionList[this.props.columnContent.id] && this.state.editionList[this.props.columnContent.id].length) { if(this.state.open && this.state.editionList[this.props.columnContent.id] && this.state.editionList[this.props.columnContent.id].length) {
@ -94,9 +73,12 @@ let TableItemSubtable = React.createClass({
<Table itemList={this.state.editionList[this.props.columnContent.id]} columnList={columnList}> <Table itemList={this.state.editionList[this.props.columnContent.id]} columnList={columnList}>
{this.state.editionList[this.props.columnContent.id].map((edition, i) => { {this.state.editionList[this.props.columnContent.id].map((edition, i) => {
return ( return (
<TableItem <TableItemSelectable
className="ascribe-table-item-selectable"
selectItem={this.selectItem}
parentId={this.props.columnContent.id}
key={i}> key={i}>
</TableItem> </TableItemSelectable>
); );
})} })}
</Table> </Table>
@ -109,10 +91,14 @@ let TableItemSubtable = React.createClass({
return ( return (
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item"> <div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item">
<div className="row"> <div className="row">
{calcColumnElementContent()} <TableItemWrapper
<div className="col-xs-2 col-sm-2 col-md-2 col-lg-2 ascribe-table-item-column"> columnList={this.props.columnList}
<TableItemSubtableButton content={this.props.columnContent['num_editions'] + ' Editions'} columnContent={this.props.columnContent}
onClick={this.loadEditionList} /> columnWidth={12}>
</TableItemWrapper>
<div className="col-xs-1 col-sm-1 col-md-1 col-lg-1 ascribe-table-item-column">
<TableItemSubtableButton content="+" onClick={this.loadEditionList}>
</TableItemSubtableButton>
</div> </div>
</div> </div>
{renderEditionListTable()} {renderEditionListTable()}

View File

@ -10,7 +10,7 @@ let TableItemSubtableButton = React.createClass({
render() { render() {
return ( return (
<span> <span>
<button type="button" className="btn btn-ascribe btn-primary btn-sm" onClick={this.props.onClick}> <button type="button" className="btn btn-default btn-sm ascribe-table-expand-button" onClick={this.props.onClick}>
{this.props.content} {this.props.content}
</button> </button>
</span> </span>

View File

@ -0,0 +1,34 @@
import React from 'react';
import TableColumnContentModel from '../../models/table_column_content_model';
import TableColumnMixin from '../../mixins/table_column_mixin';
let TableItemWrapper = React.createClass({
mixins: [TableColumnMixin],
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
columnContent: React.PropTypes.object,
columnWidth: React.PropTypes.number.isRequired
},
render() {
return (
<div>
{this.props.columnList.map((column, i) => {
let TypeElement = column.displayType;
let columnClass = this.calcColumnClasses(this.props.columnList, i, this.props.columnWidth);
return (
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
</div>
);
})}
</div>
);
}
});
export default TableItemWrapper;

78
js/components/edition.js Normal file
View File

@ -0,0 +1,78 @@
import React from 'react';
import ImageViewer from './ascribe_media/image_viewer';
import TransferModalButton from './ascribe_modal/modal_transfer';
import ShareModalButton from './ascribe_modal/modal_share';
/**
* This is the component that implements display-specific functionality
*/
let Edition = React.createClass({
render() {
return (
<div>
<div className="col-md-7">
<ImageViewer thumbnail={this.props.edition.thumbnail}/>
</div>
<div className="col-md-5">
<EditionHeader edition={this.props.edition}/>
<EditionDetails edition={this.props.edition} currentUser={ this.props.currentUser }/>
</div>
</div>
);
}
});
let EditionHeader = React.createClass({
render() {
var title_html = <div className="ascribe-detail-title">{this.props.edition.title}</div>;
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="title" value={title_html} />
<EditionDetailProperty label="by" value={this.props.edition.artist_name} />
<EditionDetailProperty label="date" value={ this.props.edition.date_created.slice(0,4) } />
<hr/>
</div>
);
}
});
let EditionDetails = React.createClass({
render() {
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="edition"
value={this.props.edition.edition_number + " of " + this.props.edition.num_editions} />
<EditionDetailProperty label="id" value={ this.props.edition.bitcoin_id } />
<EditionDetailProperty label="owner" value={ this.props.edition.owner } />
<br/>
<TransferModalButton edition={ this.props.edition } currentUser={ this.props.currentUser }/>
<ShareModalButton edition={ this.props.edition } currentUser={ this.props.currentUser }/>
<hr/>
</div>
);
}
});
let EditionDetailProperty = React.createClass({
render() {
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className="col-xs-2 col-xs-height col-bottom">
<div>{ this.props.label }:</div>
</div>
<div className="col-xs-10 col-xs-height col-bottom">
<div>{ this.props.value }</div>
</div>
</div>
</div>
);
}
});
export default Edition;

View File

@ -0,0 +1,52 @@
import React from 'react';
import EditionActions from '../actions/edition_actions';
import EditionStore from '../stores/edition_store';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import Edition from './edition';
/**
* This is the component that implements resource/data specific functionality
*/
let EditionContainer = React.createClass({
getInitialState() {
return {'user': UserStore.getState(),
'edition': EditionStore.getState()}
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
EditionActions.fetchOne(this.props.params.editionId);
EditionStore.listen(this.onChange);
UserActions.fetchCurrentUser();
UserStore.listen(this.onChange);
},
componentDidUnmount() {
EditionStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
render() {
if('title' in this.state.edition) {
return (
<Edition edition={this.state.edition } currentUser={this.state.currentUser}></Edition>
);
} else {
return (
<p>Loading</p>
);
}
}
});
export default EditionContainer;

View File

@ -1,12 +0,0 @@
import React from 'react';
let Piece = React.createClass({
render() {
return (
<p>this.props.piece.title</p>
);
}
});
export default Piece;

View File

@ -13,7 +13,9 @@ import TableItemSubtableButton from './ascribe_table/table_item_subtable_button'
import TableColumnContentModel from '../models/table_column_content_model'; import TableColumnContentModel from '../models/table_column_content_model';
import Pagination from './ascribe_pagination/pagination' import Pagination from './ascribe_pagination/pagination';
import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar';
let PieceList = React.createClass({ let PieceList = React.createClass({
@ -60,6 +62,7 @@ let PieceList = React.createClass({
if(this.state.pieceList && this.state.pieceList.length > 0) { if(this.state.pieceList && this.state.pieceList.length > 0) {
return ( return (
<div> <div>
<PieceListToolbar />
<Table <Table
columnList={columnList} columnList={columnList}
changeOrder={this.tableChangeOrder} changeOrder={this.tableChangeOrder}

8
js/constants/api_urls.js Normal file
View File

@ -0,0 +1,8 @@
import AppConstants from './application_constants';
let apiUrls = {
'ownership_shares_mail' : AppConstants.baseUrl + 'ownership/shares/mail/',
'ownership_transfers' : AppConstants.baseUrl + 'ownership/transfers/'
};
export default apiUrls;

View File

@ -1,6 +1,7 @@
let constants = { let constants = {
'baseUrl': 'http://staging.ascribe.io/api/', 'baseUrl': 'http://localhost:8000/api/',
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw' // dimi@mailinator:0000000000 'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw', // dimi@mailinator:0000000000
'aclList': ['edit', 'consign', 'transfer', 'loan', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection']
}; };
export default constants; export default constants;

View File

@ -0,0 +1,24 @@
import fetch from 'isomorphic-fetch';
import AppConstants from '../constants/application_constants';
import FetchApiUtils from '../utils/fetch_api_utils';
let EditionFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*
*/
fetchOne(editionId) {
return fetch(AppConstants.baseUrl + 'editions/' + editionId + '/', {
headers: {
'Authorization': 'Basic ' + AppConstants.debugCredentialBase64
}
}).then(
(res) => res.json()
);
}
};
export default EditionFetcher;

View File

@ -0,0 +1,22 @@
import fetch from 'isomorphic-fetch';
import AppConstants from '../constants/application_constants';
import FetchApiUtils from '../utils/fetch_api_utils';
let PieceFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*
*/
fetchOne(pieceId) {
return fetch(AppConstants.baseUrl + 'pieces/' + pieceId + '/', {
headers: {
'Authorization': 'Basic ' + AppConstants.debugCredentialBase64
}
}).then((res) => res.json());
}
};
export default PieceFetcher;

16
js/mixins/alert_mixin.js Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import AlertDismissable from '../components/ascribe_forms/alert';
let AlertMixin = {
setAlerts(errors){
let alerts = errors.map(
function(error) {
let key = error + this.state.retry;
return <AlertDismissable error={error} key={key}/>;
}.bind(this)
);
this.setState({alerts: alerts, retry: this.state.retry + 1});
}
};
export default AlertMixin;

67
js/mixins/form_mixin.js Normal file
View File

@ -0,0 +1,67 @@
import React from 'react';
import AppConstants from '../constants/application_constants'
import AlertDismissable from '../components/ascribe_forms/alert'
export const FormMixin = {
getInitialState() {
return {
submitted: false
, status: null
}
},
submit(e) {
e.preventDefault();
this.setState({submitted: true});
fetch(this.url(), {
method: 'post',
headers: {
'Authorization': 'Basic ' + AppConstants.debugCredentialBase64,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(this.getFormData())
})
.then(
(response) => this.handleResponse(response)
);
},
handleResponse(response){
if (response.status >= 200 && response.status < 300){
this.props.onRequestHide();
}
else if (response.status >= 400 && response.status < 500) {
this.handleError(response);
}
else {
this.setState({submitted: false, status: response.status});
}
},
handleError(response){
response.json().then((response) => this.dispatchErrors(response.errors));
},
dispatchErrors(errors){
for (var input in errors){
if (this.refs && this.refs[input] && this.refs[input].state){
this.refs[input].setAlerts(errors[input]);
}
}
this.setState({submitted: false});
},
render(){
let alert = null;
if (this.state.status >= 500){
alert = <AlertDismissable error="Something went wrong, please try again later"/>;
}
return (
<div>
{alert}
{this.renderForm()}
</div>
)
}
};
export default FormMixin;

View File

@ -7,14 +7,14 @@ let TableColumnMixin = {
* Generates the bootstrap grid column declarations automatically using * Generates the bootstrap grid column declarations automatically using
* the columnMap. * the columnMap.
*/ */
calcColumnClasses(list, i) { calcColumnClasses(list, i, numOfColumns) {
let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-']; let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-'];
let listOfRowValues = list.map((column) => column.rowWidth ); let listOfRowValues = list.map((column) => column.rowWidth );
let numOfColumns = GeneralUtils.sumNumList(listOfRowValues); let numOfUsedColumns = GeneralUtils.sumNumList(listOfRowValues);
if(numOfColumns > 12) { if(numOfUsedColumns > numOfColumns) {
throw new Error('Bootstrap has only 12 columns to assign. You defined ' + numOfColumns + '. Change this in the columnMap you\'re passing to the table.') throw new Error('This table has only ' + numOfColumns + ' columns to assign. You defined ' + numOfUsedColumns + '. Change this in the columnMap you\'re passing to the table.')
} else { } else {
return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i]; return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i];
} }

View File

@ -3,7 +3,7 @@ import Router from 'react-router';
import AscribeApp from './components/ascribe_app'; import AscribeApp from './components/ascribe_app';
import PieceList from './components/piece_list'; import PieceList from './components/piece_list';
import Piece from './components/piece'; import EditionContainer from './components/edition_container';
let Route = Router.Route; let Route = Router.Route;
@ -11,7 +11,9 @@ let Route = Router.Route;
let routes = ( let routes = (
<Route name="app" path="/" handler={AscribeApp}> <Route name="app" path="/" handler={AscribeApp}>
<Route name="pieces" path="/pieces" handler={PieceList}> <Route name="pieces" path="/pieces" handler={PieceList}>
<Route name="piece" path="/pieces/:bitcoin_ID_noPrefix" handler={Piece} />
</Route>
<Route name="edition" path="/editions/:editionId" handler={EditionContainer}>
</Route> </Route>
</Route> </Route>
); );

View File

@ -1,3 +1,5 @@
import React from 'react';
import alt from '../alt'; import alt from '../alt';
import EditionsListActions from '../actions/edition_list_actions'; import EditionsListActions from '../actions/edition_list_actions';
@ -7,8 +9,29 @@ class EditionListStore {
this.bindActions(EditionsListActions); this.bindActions(EditionsListActions);
} }
onUpdateEditionList({pieceId, editionList}) { onUpdateEditionList({pieceId, editionListOfPiece}) {
this.editionList[pieceId] = editionList; if(this.editionList[pieceId]) {
this.editionList[pieceId].forEach((edition, i) => {
// This uses the index of the new editionList for determining the edition.
// If the list of editions can be sorted in the future, this needs to be changed!
editionListOfPiece[i] = React.addons.update(edition, {$merge: editionListOfPiece[i]});
})
}
this.editionList[pieceId] = editionListOfPiece;
}
onSelectEdition({pieceId, editionId}) {
this.editionList[pieceId].forEach((edition) => {
if(edition.edition_number === editionId) {
if(edition.selected) {
edition.selected = false;
} else {
edition.selected = true;
}
}
});
} }
}; };

View File

@ -0,0 +1,16 @@
import alt from '../alt';
import EditionAction from '../actions/edition_actions';
class EditionStore {
constructor() {
this.edition = {};
this.bindActions(EditionAction);
}
onUpdateEdition(edition) {
this.edition = edition;
}
}
export default alt.createStore(EditionStore);

16
js/stores/piece_store.js Normal file
View File

@ -0,0 +1,16 @@
import alt from '../alt';
import PieceAction from '../actions/piece_actions';
class PieceStore {
constructor() {
this.piece = {};
this.bindActions(PieceAction);
}
onUpdatePiece(piece) {
this.piece = piece;
}
}
export default alt.createStore(PieceStore);

View File

@ -54,6 +54,13 @@ let FetchApiUtils = {
} }
return interpolation + orderBy; return interpolation + orderBy;
},
status(response) {
if (response.status >= 200 && response.status < 300) {
return response
}
throw new Error(response.json())
} }
}; };

View File

@ -35,6 +35,7 @@ let GeneralUtils = {
l.forEach((num) => sum += parseFloat(num) || 0); l.forEach((num) => sum += parseFloat(num) || 0);
return sum; return sum;
} }
}; };
export default GeneralUtils; export default GeneralUtils;

View File

@ -3,26 +3,24 @@
"version": "0.0.1", "version": "0.0.1",
"description": "Das neue web client for Ascribe", "description": "Das neue web client for Ascribe",
"main": "js/app.js", "main": "js/app.js",
"scripts": {
"watch": "watchify -o build/app.js -v -d js/app.js",
"build": "browserify . -t [envify --NODE_ENV production] | uglifyjs -cm > build/app.js",
"test": "jest"
},
"author": "Ascribe", "author": "Ascribe",
"license": "Copyright", "license": "Copyright",
"browserify": {
"transform": [
"babelify",
"envify"
]
},
"devDependencies": { "devDependencies": {
"babel-jest": "^4.0.0", "babel-jest": "^4.0.0",
"babelify": "^6.0.2", "babelify": "^6.1.2",
"browser-sync": "^2.7.5",
"browserify": "^9.0.8", "browserify": "^9.0.8",
"envify": "^3.4.0", "envify": "^3.4.0",
"gulp": "^3.8.11",
"gulp-if": "^1.2.5",
"gulp-notify": "^2.2.0",
"gulp-sourcemaps": "^1.5.2",
"gulp-util": "^3.0.4",
"jest-cli": "^0.4.0", "jest-cli": "^0.4.0",
"lodash": "^3.9.3",
"reactify": "^1.1.0", "reactify": "^1.1.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.1.2" "watchify": "^3.1.2"
}, },
"dependencies": { "dependencies": {
@ -32,7 +30,8 @@
"object-assign": "^2.0.0", "object-assign": "^2.0.0",
"react": "^0.13.2", "react": "^0.13.2",
"react-router": "^0.13.3", "react-router": "^0.13.3",
"uglifyjs": "^2.4.10" "uglifyjs": "^2.4.10",
"react-bootstrap": "~0.22.6"
}, },
"jest": { "jest": {
"scriptPreprocessor": "node_modules/babel-jest", "scriptPreprocessor": "node_modules/babel-jest",