diff --git a/.gitignore b/.gitignore
index 580f3863..31b4a8c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ node_modules
npm-debug.log
build/app.js
+
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
index 21c85c0c..c4391d52 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,7 @@ Install some nice extensions for Chrom(e|ium):
git clone git@bitbucket.org:ascribe/onion.git
cd onion
npm install
-npm run watch
-
-python -mSimpleHTTPServer
+gulp serve
```
diff --git a/css/ascribe-fonts/style.css b/css/ascribe-fonts/style.css
index c4ecbdc8..eaab4bca 100644
--- a/css/ascribe-fonts/style.css
+++ b/css/ascribe-fonts/style.css
@@ -1,10 +1,10 @@
@font-face {
font-family: 'ascribe';
- src:url('fonts/ascribe.eot?-6bb2dq');
- src:url('fonts/ascribe.eot?#iefix-6bb2dq') format('embedded-opentype'),
- url('fonts/ascribe.woff?-6bb2dq') format('woff'),
- url('fonts/ascribe.ttf?-6bb2dq') format('truetype'),
- url('fonts/ascribe.svg?-6bb2dq#ascribe') format('svg');
+ src:url('ascribe.eot?-6bb2dq');
+ src:url('ascribe.eot?#iefix-6bb2dq') format('embedded-opentype'),
+ url('ascribe.woff?-6bb2dq') format('woff'),
+ url('ascribe.ttf?-6bb2dq') format('truetype'),
+ url('ascribe.svg?-6bb2dq#ascribe') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -187,3 +187,7 @@
content: "\eae9";
}
+.btn-glyph-ascribe{
+ font-size: 18px;
+ padding: 4px 12px 0 10px
+}
\ No newline at end of file
diff --git a/css/main.css b/css/main.css
index cac29beb..cbd7f7ec 100644
--- a/css/main.css
+++ b/css/main.css
@@ -12,15 +12,17 @@
.ascribe-table-header-row {
border-bottom: 2px solid rgba(2, 182, 163, 0.5);
border-top: 2px solid rgba(2, 182, 163, 0.5);
+ padding: 0;
}
-
+
.ascribe-table-header-column {
display: table;
- height:4em;
+ height:3em;
+ padding: 0;
}
.ascribe-table-header-column > span {
- display:table-cell;
+ display: table-cell;
vertical-align: middle;
font-family: 'Source Sans Pro';
font-weight: 600;
@@ -31,10 +33,10 @@
.ascribe-table-header-column > span > .glyphicon {
font-size: .5em;
}
-
+/*
.ascribe-table-item:nth-child(even) {
background-color: #F5F5F5;
-}
+}*/
/*.ascribe-table-item:hover {
background-color: #EEEEEE;
@@ -44,15 +46,168 @@
display: table;
font-family: 'Source Sans Pro';
font-size: 1.2em;
- height:4em;
+ height:3em;
}
.ascribe-table-item-column > * {
- display:table-cell;
+ display: table-cell;
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);
- 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;
}
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 00000000..3b42e4de
--- /dev/null
+++ b/gulpfile.js
@@ -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);
+}
\ No newline at end of file
diff --git a/index.html b/index.html
index 824eeaee..21dac715 100644
--- a/index.html
+++ b/index.html
@@ -5,13 +5,15 @@
ascribe
-
-
-
+
+
+
+
+
diff --git a/js/actions/edition_actions.js b/js/actions/edition_actions.js
new file mode 100644
index 00000000..a39c330a
--- /dev/null
+++ b/js/actions/edition_actions.js
@@ -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);
diff --git a/js/actions/edition_list_actions.js b/js/actions/edition_list_actions.js
index 055ef2a5..48e02d2d 100644
--- a/js/actions/edition_list_actions.js
+++ b/js/actions/edition_list_actions.js
@@ -5,7 +5,8 @@ import EditionListFetcher from '../fetchers/edition_list_fetcher.js';
class EditionListActions {
constructor() {
this.generateActions(
- 'updateEditionList'
+ 'updateEditionList',
+ 'selectEdition'
);
}
@@ -14,7 +15,7 @@ class EditionListActions {
.fetch(pieceId)
.then((res) => {
this.actions.updateEditionList({
- 'editionList': res.editions,
+ 'editionListOfPiece': res.editions,
pieceId
});
})
@@ -22,6 +23,6 @@ class EditionListActions {
console.log(err);
});
}
-};
+}
export default alt.createActions(EditionListActions);
\ No newline at end of file
diff --git a/js/actions/piece_actions.js b/js/actions/piece_actions.js
new file mode 100644
index 00000000..d657539b
--- /dev/null
+++ b/js/actions/piece_actions.js
@@ -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);
diff --git a/js/components/acl_button.js b/js/components/acl_button.js
new file mode 100644
index 00000000..9d52a09a
--- /dev/null
+++ b/js/components/acl_button.js
@@ -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 (
+
+ );
+ }
+});
+
+export default AclButton;
\ No newline at end of file
diff --git a/js/components/ascribe_forms/alert.js b/js/components/ascribe_forms/alert.js
new file mode 100644
index 00000000..72ced310
--- /dev/null
+++ b/js/components/ascribe_forms/alert.js
@@ -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 (
+
+ {this.props.error}
+
+ );
+ }
+ return (
+
+ );
+ }
+});
+
+export default AlertDismissable;
\ No newline at end of file
diff --git a/js/components/ascribe_forms/button_submit_close.js b/js/components/ascribe_forms/button_submit_close.js
new file mode 100644
index 00000000..60091f2f
--- /dev/null
+++ b/js/components/ascribe_forms/button_submit_close.js
@@ -0,0 +1,21 @@
+import React from 'react';
+
+let ButtonSubmitOrClose = React.createClass({
+ render() {
+ if (this.props.submitted){
+ return (
+
+ Loading
+
+ )
+ }
+ return (
+
+
+
+
+ )
+ }
+});
+
+export default ButtonSubmitOrClose;
diff --git a/js/components/ascribe_forms/form_share_email.js b/js/components/ascribe_forms/form_share_email.js
new file mode 100644
index 00000000..dd61bc7b
--- /dev/null
+++ b/js/components/ascribe_forms/form_share_email.js
@@ -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 (
+
+ );
+ }
+});
+
+export default ShareForm;
\ No newline at end of file
diff --git a/js/components/ascribe_forms/form_transfer.js b/js/components/ascribe_forms/form_transfer.js
new file mode 100644
index 00000000..28e86db3
--- /dev/null
+++ b/js/components/ascribe_forms/form_transfer.js
@@ -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 (
+
+ );
+ }
+});
+
+export default TransferForm;
\ No newline at end of file
diff --git a/js/components/ascribe_forms/input_text.js b/js/components/ascribe_forms/input_text.js
new file mode 100644
index 00000000..a65bcc8f
--- /dev/null
+++ b/js/components/ascribe_forms/input_text.js
@@ -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 (
+
+ {alerts}
+
+
+ );
+
+ }
+});
+
+export default InputText;
\ No newline at end of file
diff --git a/js/components/ascribe_forms/input_textarea.js b/js/components/ascribe_forms/input_textarea.js
new file mode 100644
index 00000000..b5f64b66
--- /dev/null
+++ b/js/components/ascribe_forms/input_textarea.js
@@ -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 (
+
+ {alerts}
+
+
+ );
+
+ }
+});
+
+export default InputTextArea;
\ No newline at end of file
diff --git a/js/components/ascribe_media/image_viewer.js b/js/components/ascribe_media/image_viewer.js
new file mode 100644
index 00000000..fdbd0342
--- /dev/null
+++ b/js/components/ascribe_media/image_viewer.js
@@ -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 = ;
+ let aligner = ;
+ return (
+
+
+ {aligner}
+ {thumbnail}
+
+
+ );
+ }
+});
+
+export default ImageViewer;
\ No newline at end of file
diff --git a/js/components/ascribe_modal/modal_share.js b/js/components/ascribe_modal/modal_share.js
new file mode 100644
index 00000000..176541ee
--- /dev/null
+++ b/js/components/ascribe_modal/modal_share.js
@@ -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 (
+ Share the artwork}>
+ }>
+
+
+
+
+
+ )
+ }
+});
+
+let ShareModal = React.createClass({
+ onRequestHide(e){
+ if (e)
+ e.preventDefault();
+ this.props.onRequestHide();
+ },
+ render() {
+ return (
+
+
+
+
+
+ )
+ }
+});
+
+
+export default ShareModalButton;
diff --git a/js/components/ascribe_modal/modal_transfer.js b/js/components/ascribe_modal/modal_transfer.js
new file mode 100644
index 00000000..b57064ff
--- /dev/null
+++ b/js/components/ascribe_modal/modal_transfer.js
@@ -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 (
+ Transfer the ownership of the artwork}>
+ }>
+
+ TRANSFER
+
+
+
+ )
+ }
+});
+
+let TransferModal = React.createClass({
+ onRequestHide(e){
+ e.preventDefault();
+ this.props.onRequestHide();
+ },
+ render() {
+ return (
+
+
+
+
+
+ )
+ }
+});
+
+
+export default TransferModalButton;
diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js
new file mode 100644
index 00000000..1ce4d744
--- /dev/null
+++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js
@@ -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 (
+
+ );
+ }
+});
+
+export default PieceListToolbar;
\ No newline at end of file
diff --git a/js/components/ascribe_table/table.js b/js/components/ascribe_table/table.js
index c51c4994..06af7b89 100644
--- a/js/components/ascribe_table/table.js
+++ b/js/components/ascribe_table/table.js
@@ -27,12 +27,13 @@ let Table = React.createClass({
if(this.props.itemList && this.props.itemList.length > 0) {
return (
-
+
{this.renderChildren()}
);
diff --git a/js/components/ascribe_table/table_header.js b/js/components/ascribe_table/table_header.js
index 564f7725..8176ad31 100644
--- a/js/components/ascribe_table/table_header.js
+++ b/js/components/ascribe_table/table_header.js
@@ -12,9 +12,9 @@ let TableHeader = React.createClass({
propTypes: {
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(TableColumnContentModel)),
itemList: React.PropTypes.array.isRequired,
- changeOrder: React.PropTypes.func.isRequired,
- orderAsc: React.PropTypes.bool.isRequired,
- orderBy: React.PropTypes.string.isRequired
+ changeOrder: React.PropTypes.func,
+ orderAsc: React.PropTypes.bool,
+ orderBy: React.PropTypes.string
},
render() {
@@ -23,7 +23,7 @@ let TableHeader = React.createClass({
{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 canBeOrdered = this.props.columnList[i].canBeOrdered;
diff --git a/js/components/ascribe_table/table_header_item.js b/js/components/ascribe_table/table_header_item.js
index fef82f34..dce2261e 100644
--- a/js/components/ascribe_table/table_header_item.js
+++ b/js/components/ascribe_table/table_header_item.js
@@ -8,10 +8,10 @@ let TableHeaderItem = React.createClass({
columnClasses: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string.isRequired,
columnName: React.PropTypes.string.isRequired,
- canBeOrdered: React.PropTypes.bool.isRequired,
- changeOrder: React.PropTypes.func.isRequired,
- orderAsc: React.PropTypes.bool.isRequired,
- orderBy: React.PropTypes.string.isRequired
+ canBeOrdered: React.PropTypes.bool,
+ changeOrder: React.PropTypes.func,
+ orderAsc: React.PropTypes.bool,
+ orderBy: React.PropTypes.string
},
changeOrder() {
@@ -19,7 +19,7 @@ let TableHeaderItem = React.createClass({
},
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) {
return (
{
- return this.props.columnList.map((column, i) => {
-
- let TypeElement = column.displayType;
- let columnClass = this.calcColumnClasses(this.props.columnList, i);
-
- return (
-
-
-
- );
-
- });
- };
-
return (
-
+
- {calcColumnElementContent()}
+
+
);
diff --git a/js/components/ascribe_table/table_item_acl.js b/js/components/ascribe_table/table_item_acl.js
new file mode 100644
index 00000000..87e14761
--- /dev/null
+++ b/js/components/ascribe_table/table_item_acl.js
@@ -0,0 +1,18 @@
+import React from 'react';
+
+
+let TableItemAcl = React.createClass({
+ propTypes: {
+ content: React.PropTypes.array.isRequired
+ },
+
+ render() {
+ return (
+
+ {this.props.content.join('/')}
+
+ );
+ }
+});
+
+export default TableItemAcl;
diff --git a/js/components/ascribe_table/table_item_selectable.js b/js/components/ascribe_table/table_item_selectable.js
new file mode 100644
index 00000000..d62f8d04
--- /dev/null
+++ b/js/components/ascribe_table/table_item_selectable.js
@@ -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 (
+
+
+ );
+ }
+});
+
+export default TableItemSelectable;
diff --git a/js/components/ascribe_table/table_item_subtable.js b/js/components/ascribe_table/table_item_subtable.js
index e9bd05f2..0e055991 100644
--- a/js/components/ascribe_table/table_item_subtable.js
+++ b/js/components/ascribe_table/table_item_subtable.js
@@ -5,12 +5,12 @@ import TableColumnContentModel from '../../models/table_column_content_model';
import EditionListStore from '../../stores/edition_list_store';
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 TableItem from './table_item';
+import TableItemWrapper from './table_item_wrapper';
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';
@@ -40,6 +40,7 @@ let TableItemSubtable = React.createClass({
'open': false
});
} else {
+
EditionListActions.fetchEditionList(this.props.columnContent.id);
this.setState({
'open': true,
@@ -48,43 +49,21 @@ let TableItemSubtable = React.createClass({
}
},
- calcColumnClasses(list, i) {
- let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-'];
-
- let listOfRowValues = list.map((column) => column.rowWidth );
- 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];
- }
+ selectItem(parentId, itemId) {
+ EditionListActions.selectEdition({
+ 'pieceId': parentId,
+ 'editionId': itemId
+ });
},
render() {
- let calcColumnElementContent = () => {
- return this.props.columnList.map((column, i) => {
-
- let TypeElement = column.displayType;
- let columnClass = this.calcColumnClasses(this.props.columnList, i);
-
- return (
-
-
-
- );
-
- });
- };
-
-
let renderEditionListTable = () => {
let columnList = [
new TableColumnContentModel('edition_number', 'Edition Number', TableItemText, 2, false),
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) {
@@ -94,9 +73,12 @@ let TableItemSubtable = React.createClass({
{this.state.editionList[this.props.columnContent.id].map((edition, i) => {
return (
-
-
+
);
})}
@@ -109,10 +91,14 @@ let TableItemSubtable = React.createClass({
return (
- {calcColumnElementContent()}
-
{renderEditionListTable()}
diff --git a/js/components/ascribe_table/table_item_subtable_button.js b/js/components/ascribe_table/table_item_subtable_button.js
index 7638dc13..8c5431d8 100644
--- a/js/components/ascribe_table/table_item_subtable_button.js
+++ b/js/components/ascribe_table/table_item_subtable_button.js
@@ -10,7 +10,7 @@ let TableItemSubtableButton = React.createClass({
render() {
return (
-
diff --git a/js/components/ascribe_table/table_item_wrapper.js b/js/components/ascribe_table/table_item_wrapper.js
new file mode 100644
index 00000000..2f9f4c7e
--- /dev/null
+++ b/js/components/ascribe_table/table_item_wrapper.js
@@ -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 (
+
+ {this.props.columnList.map((column, i) => {
+
+ let TypeElement = column.displayType;
+ let columnClass = this.calcColumnClasses(this.props.columnList, i, this.props.columnWidth);
+
+ return (
+
+
+
+ );
+
+ })}
+
+ );
+ }
+});
+
+export default TableItemWrapper;
\ No newline at end of file
diff --git a/js/components/edition.js b/js/components/edition.js
new file mode 100644
index 00000000..ffeb9b25
--- /dev/null
+++ b/js/components/edition.js
@@ -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 (
+
+ );
+ }
+});
+
+let EditionHeader = React.createClass({
+ render() {
+ var title_html =
{this.props.edition.title}
;
+ return (
+
+
+
+
+
+
+ );
+ }
+});
+
+let EditionDetails = React.createClass({
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+
+ }
+});
+
+let EditionDetailProperty = React.createClass({
+ render() {
+ return (
+
+
+
+
{ this.props.label }:
+
+
+
+
+ );
+ }
+});
+
+
+export default Edition;
+
diff --git a/js/components/edition_container.js b/js/components/edition_container.js
new file mode 100644
index 00000000..77d28962
--- /dev/null
+++ b/js/components/edition_container.js
@@ -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 (
+
+ );
+ } else {
+ return (
+
Loading
+ );
+ }
+
+
+ }
+});
+
+export default EditionContainer;
diff --git a/js/components/piece.js b/js/components/piece.js
deleted file mode 100644
index bd268115..00000000
--- a/js/components/piece.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-
-let Piece = React.createClass({
- render() {
- return (
-
this.props.piece.title
- );
- }
-});
-
-export default Piece;
diff --git a/js/components/piece_list.js b/js/components/piece_list.js
index 191c0d9d..34a1baf0 100644
--- a/js/components/piece_list.js
+++ b/js/components/piece_list.js
@@ -13,7 +13,9 @@ import TableItemSubtableButton from './ascribe_table/table_item_subtable_button'
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({
@@ -60,6 +62,7 @@ let PieceList = React.createClass({
if(this.state.pieceList && this.state.pieceList.length > 0) {
return (
+
res.json()
+ );
+ }
+};
+
+export default EditionFetcher;
diff --git a/js/fetchers/piece_fetcher.js b/js/fetchers/piece_fetcher.js
new file mode 100644
index 00000000..4aaecda5
--- /dev/null
+++ b/js/fetchers/piece_fetcher.js
@@ -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;
diff --git a/js/mixins/alert_mixin.js b/js/mixins/alert_mixin.js
new file mode 100644
index 00000000..efabed0e
--- /dev/null
+++ b/js/mixins/alert_mixin.js
@@ -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 ;
+ }.bind(this)
+ );
+ this.setState({alerts: alerts, retry: this.state.retry + 1});
+ }
+};
+
+export default AlertMixin;
\ No newline at end of file
diff --git a/js/mixins/form_mixin.js b/js/mixins/form_mixin.js
new file mode 100644
index 00000000..6bbf5efa
--- /dev/null
+++ b/js/mixins/form_mixin.js
@@ -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 = ;
+ }
+ return (
+
+ {alert}
+ {this.renderForm()}
+
+ )
+ }
+};
+
+export default FormMixin;
+
diff --git a/js/mixins/table_column_mixin.js b/js/mixins/table_column_mixin.js
index 5904389d..d74214e1 100644
--- a/js/mixins/table_column_mixin.js
+++ b/js/mixins/table_column_mixin.js
@@ -7,14 +7,14 @@ let TableColumnMixin = {
* Generates the bootstrap grid column declarations automatically using
* the columnMap.
*/
- calcColumnClasses(list, i) {
+ calcColumnClasses(list, i, numOfColumns) {
let bootstrapClasses = ['col-xs-', 'col-sm-', 'col-md-', 'col-lg-'];
let listOfRowValues = list.map((column) => column.rowWidth );
- let numOfColumns = GeneralUtils.sumNumList(listOfRowValues);
+ let numOfUsedColumns = GeneralUtils.sumNumList(listOfRowValues);
- if(numOfColumns > 12) {
- throw new Error('Bootstrap has only 12 columns to assign. You defined ' + numOfColumns + '. Change this in the columnMap you\'re passing to the table.')
+ if(numOfUsedColumns > numOfColumns) {
+ 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 {
return bootstrapClasses.join( listOfRowValues[i] + ' ') + listOfRowValues[i];
}
diff --git a/js/routes.js b/js/routes.js
index b9dd01c8..0fccc60a 100644
--- a/js/routes.js
+++ b/js/routes.js
@@ -3,7 +3,7 @@ import Router from 'react-router';
import AscribeApp from './components/ascribe_app';
import PieceList from './components/piece_list';
-import Piece from './components/piece';
+import EditionContainer from './components/edition_container';
let Route = Router.Route;
@@ -11,7 +11,9 @@ let Route = Router.Route;
let routes = (
-
+
+
+
);
diff --git a/js/stores/edition_list_store.js b/js/stores/edition_list_store.js
index 4158e74c..feb0ba2a 100644
--- a/js/stores/edition_list_store.js
+++ b/js/stores/edition_list_store.js
@@ -1,3 +1,5 @@
+import React from 'react';
+
import alt from '../alt';
import EditionsListActions from '../actions/edition_list_actions';
@@ -7,8 +9,29 @@ class EditionListStore {
this.bindActions(EditionsListActions);
}
- onUpdateEditionList({pieceId, editionList}) {
- this.editionList[pieceId] = editionList;
+ onUpdateEditionList({pieceId, editionListOfPiece}) {
+ 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;
+ }
+ }
+ });
+
}
};
diff --git a/js/stores/edition_store.js b/js/stores/edition_store.js
new file mode 100644
index 00000000..9859ec7c
--- /dev/null
+++ b/js/stores/edition_store.js
@@ -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);
diff --git a/js/stores/piece_store.js b/js/stores/piece_store.js
new file mode 100644
index 00000000..35e8c229
--- /dev/null
+++ b/js/stores/piece_store.js
@@ -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);
diff --git a/js/utils/fetch_api_utils.js b/js/utils/fetch_api_utils.js
index e8395490..47b52e66 100644
--- a/js/utils/fetch_api_utils.js
+++ b/js/utils/fetch_api_utils.js
@@ -54,6 +54,13 @@ let FetchApiUtils = {
}
return interpolation + orderBy;
+ },
+
+ status(response) {
+ if (response.status >= 200 && response.status < 300) {
+ return response
+ }
+ throw new Error(response.json())
}
};
diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js
index fe92611b..7ab180fa 100644
--- a/js/utils/general_utils.js
+++ b/js/utils/general_utils.js
@@ -35,6 +35,7 @@ let GeneralUtils = {
l.forEach((num) => sum += parseFloat(num) || 0);
return sum;
}
+
};
export default GeneralUtils;
diff --git a/package.json b/package.json
index a7b45c6c..40277a40 100644
--- a/package.json
+++ b/package.json
@@ -3,26 +3,24 @@
"version": "0.0.1",
"description": "Das neue web client for Ascribe",
"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",
"license": "Copyright",
- "browserify": {
- "transform": [
- "babelify",
- "envify"
- ]
- },
"devDependencies": {
"babel-jest": "^4.0.0",
- "babelify": "^6.0.2",
+ "babelify": "^6.1.2",
+ "browser-sync": "^2.7.5",
"browserify": "^9.0.8",
"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",
+ "lodash": "^3.9.3",
"reactify": "^1.1.0",
+ "vinyl-buffer": "^1.0.0",
+ "vinyl-source-stream": "^1.1.0",
"watchify": "^3.1.2"
},
"dependencies": {
@@ -32,7 +30,8 @@
"object-assign": "^2.0.0",
"react": "^0.13.2",
"react-router": "^0.13.3",
- "uglifyjs": "^2.4.10"
+ "uglifyjs": "^2.4.10",
+ "react-bootstrap": "~0.22.6"
},
"jest": {
"scriptPreprocessor": "node_modules/babel-jest",