mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 09:23:13 +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:
commit
d4e55f649d
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@ node_modules
|
||||
npm-debug.log
|
||||
|
||||
build/app.js
|
||||
|
||||
.DS_Store
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
173
css/main.css
173
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;
|
||||
}
|
67
gulpfile.js
Normal file
67
gulpfile.js
Normal 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);
|
||||
}
|
@ -5,13 +5,15 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>ascribe</title>
|
||||
<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="css/main.css"></link>
|
||||
<link rel="stylesheet" href="css/ascribe-fonts/ascribe-fonts.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 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">
|
||||
</head>
|
||||
<body>
|
||||
<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://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
||||
|
23
js/actions/edition_actions.js
Normal file
23
js/actions/edition_actions.js
Normal 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);
|
@ -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);
|
23
js/actions/piece_actions.js
Normal file
23
js/actions/piece_actions.js
Normal 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);
|
32
js/components/acl_button.js
Normal file
32
js/components/acl_button.js
Normal 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;
|
31
js/components/ascribe_forms/alert.js
Normal file
31
js/components/ascribe_forms/alert.js
Normal 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;
|
21
js/components/ascribe_forms/button_submit_close.js
Normal file
21
js/components/ascribe_forms/button_submit_close.js
Normal 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;
|
53
js/components/ascribe_forms/form_share_email.js
Normal file
53
js/components/ascribe_forms/form_share_email.js
Normal 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;
|
68
js/components/ascribe_forms/form_transfer.js
Normal file
68
js/components/ascribe_forms/form_transfer.js
Normal 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;
|
35
js/components/ascribe_forms/input_text.js
Normal file
35
js/components/ascribe_forms/input_text.js
Normal 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;
|
35
js/components/ascribe_forms/input_textarea.js
Normal file
35
js/components/ascribe_forms/input_textarea.js
Normal 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;
|
25
js/components/ascribe_media/image_viewer.js
Normal file
25
js/components/ascribe_media/image_viewer.js
Normal 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;
|
46
js/components/ascribe_modal/modal_share.js
Normal file
46
js/components/ascribe_modal/modal_share.js
Normal 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;
|
45
js/components/ascribe_modal/modal_transfer.js
Normal file
45
js/components/ascribe_modal/modal_transfer.js
Normal 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;
|
@ -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;
|
@ -27,12 +27,13 @@ let Table = React.createClass({
|
||||
if(this.props.itemList && this.props.itemList.length > 0) {
|
||||
return (
|
||||
<div className="ascribe-table">
|
||||
<TableHeader columnList={this.props.columnList}
|
||||
itemList={this.props.itemList}
|
||||
fetchList={this.props.fetchList}
|
||||
changeOrder={this.props.changeOrder}
|
||||
orderAsc={this.props.orderAsc}
|
||||
orderBy={this.props.orderBy} />
|
||||
<TableHeader
|
||||
columnList={this.props.columnList}
|
||||
itemList={this.props.itemList}
|
||||
fetchList={this.props.fetchList}
|
||||
changeOrder={this.props.changeOrder}
|
||||
orderAsc={this.props.orderAsc}
|
||||
orderBy={this.props.orderBy} />
|
||||
{this.renderChildren()}
|
||||
</div>
|
||||
);
|
||||
|
@ -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({
|
||||
<div className="row">
|
||||
{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;
|
||||
|
||||
|
@ -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 (
|
||||
<div
|
||||
|
@ -1,38 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import TableColumnMixin from '../../mixins/table_column_mixin';
|
||||
|
||||
import TableColumnContentModel from '../../models/table_column_content_model';
|
||||
|
||||
import TableItemWrapper from './table_item_wrapper';
|
||||
|
||||
|
||||
let TableItem = React.createClass({
|
||||
mixins: [TableColumnMixin],
|
||||
|
||||
propTypes: {
|
||||
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() {
|
||||
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 (
|
||||
<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">
|
||||
{calcColumnElementContent()}
|
||||
<TableItemWrapper
|
||||
columnList={this.props.columnList}
|
||||
columnContent={this.props.columnContent}
|
||||
columnWidth={12}>
|
||||
</TableItemWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
18
js/components/ascribe_table/table_item_acl.js
Normal file
18
js/components/ascribe_table/table_item_acl.js
Normal 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;
|
38
js/components/ascribe_table/table_item_selectable.js
Normal file
38
js/components/ascribe_table/table_item_selectable.js
Normal 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;
|
@ -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 (
|
||||
<div className={columnClass + ' ascribe-table-item-column'} key={i}>
|
||||
<TypeElement content={this.props.columnContent[column.columnName]} width="50" />
|
||||
</div>
|
||||
);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
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({
|
||||
<Table itemList={this.state.editionList[this.props.columnContent.id]} columnList={columnList}>
|
||||
{this.state.editionList[this.props.columnContent.id].map((edition, i) => {
|
||||
return (
|
||||
<TableItem
|
||||
<TableItemSelectable
|
||||
className="ascribe-table-item-selectable"
|
||||
selectItem={this.selectItem}
|
||||
parentId={this.props.columnContent.id}
|
||||
key={i}>
|
||||
</TableItem>
|
||||
</TableItemSelectable>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
@ -109,10 +91,14 @@ let TableItemSubtable = React.createClass({
|
||||
return (
|
||||
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12 ascribe-table-item">
|
||||
<div className="row">
|
||||
{calcColumnElementContent()}
|
||||
<div className="col-xs-2 col-sm-2 col-md-2 col-lg-2 ascribe-table-item-column">
|
||||
<TableItemSubtableButton content={this.props.columnContent['num_editions'] + ' Editions'}
|
||||
onClick={this.loadEditionList} />
|
||||
<TableItemWrapper
|
||||
columnList={this.props.columnList}
|
||||
columnContent={this.props.columnContent}
|
||||
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>
|
||||
{renderEditionListTable()}
|
||||
|
@ -10,7 +10,7 @@ let TableItemSubtableButton = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<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}
|
||||
</button>
|
||||
</span>
|
||||
|
34
js/components/ascribe_table/table_item_wrapper.js
Normal file
34
js/components/ascribe_table/table_item_wrapper.js
Normal 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
78
js/components/edition.js
Normal 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;
|
||||
|
52
js/components/edition_container.js
Normal file
52
js/components/edition_container.js
Normal 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;
|
@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
|
||||
let Piece = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<p>this.props.piece.title</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Piece;
|
@ -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 (
|
||||
<div>
|
||||
<PieceListToolbar />
|
||||
<Table
|
||||
columnList={columnList}
|
||||
changeOrder={this.tableChangeOrder}
|
||||
|
8
js/constants/api_urls.js
Normal file
8
js/constants/api_urls.js
Normal 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;
|
@ -1,6 +1,7 @@
|
||||
let constants = {
|
||||
'baseUrl': 'http://staging.ascribe.io/api/',
|
||||
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw' // dimi@mailinator:0000000000
|
||||
'baseUrl': 'http://localhost:8000/api/',
|
||||
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw', // dimi@mailinator:0000000000
|
||||
'aclList': ['edit', 'consign', 'transfer', 'loan', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection']
|
||||
};
|
||||
|
||||
export default constants;
|
24
js/fetchers/edition_fetcher.js
Normal file
24
js/fetchers/edition_fetcher.js
Normal 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;
|
22
js/fetchers/piece_fetcher.js
Normal file
22
js/fetchers/piece_fetcher.js
Normal 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
16
js/mixins/alert_mixin.js
Normal 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
67
js/mixins/form_mixin.js
Normal 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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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 = (
|
||||
<Route name="app" path="/" handler={AscribeApp}>
|
||||
<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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
16
js/stores/edition_store.js
Normal file
16
js/stores/edition_store.js
Normal 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
16
js/stores/piece_store.js
Normal 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);
|
@ -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())
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -35,6 +35,7 @@ let GeneralUtils = {
|
||||
l.forEach((num) => sum += parseFloat(num) || 0);
|
||||
return sum;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default GeneralUtils;
|
||||
|
25
package.json
25
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",
|
||||
|
Loading…
Reference in New Issue
Block a user