1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-28 00:28:00 +02:00

piece view

This commit is contained in:
diminator 2015-07-08 22:54:07 +02:00
parent d5ce1ecde2
commit 8ac26275f5
12 changed files with 471 additions and 167 deletions

View File

@ -0,0 +1,55 @@
'use strict';
import React from 'react';
let DetailProperty = React.createClass({
propTypes: {
label: React.PropTypes.string,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
]),
separator: React.PropTypes.string,
labelClassName: React.PropTypes.string,
valueClassName: React.PropTypes.string
},
getDefaultProps() {
return {
separator: ':',
labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3',
valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9'
};
},
render() {
let value = this.props.value;
if (this.props.children){
value = (
<div className="row-same-height">
<div className="col-xs-6 col-xs-height col-bottom no-padding">
{ this.props.value }
</div>
<div className="col-xs-6 col-xs-height">
{ this.props.children }
</div>
</div>);
}
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom'}>
<div>{ this.props.label + this.props.separator}</div>
</div>
<div className={this.props.valueClassName + ' col-xs-height col-bottom'}>
{value}
</div>
</div>
</div>
);
}
});
export default DetailProperty;

View File

@ -7,21 +7,23 @@ import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Button from 'react-bootstrap/lib/Button';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store';
import MediaPlayer from './../ascribe_media/media_player';
import MediaContainer from './media_container';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable';
import EditionHeader from './header';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import RequestActionForm from './../ascribe_forms/form_request_action';
@ -35,7 +37,6 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import apiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import classNames from 'classnames';
import { getCookie } from '../../utils/fetch_api_utils';
@ -72,10 +73,10 @@ let Edition = React.createClass({
<Row>
<Col md={6}>
<MediaContainer
edition={this.props.edition}/>
content={this.props.edition}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<EditionHeader edition={this.props.edition}/>
<EditionHeader content={this.props.edition}/>
<EditionSummary
currentUser={this.state.currentUser}
edition={this.props.edition} />
@ -142,106 +143,6 @@ let Edition = React.createClass({
}
});
let MediaContainer = React.createClass({
propTypes: {
edition: React.PropTypes.object
},
render() {
let thumbnail = this.props.edition.thumbnail;
let mimetype = this.props.edition.digital_work.mime;
let embed = null;
let extraData = null;
if (this.props.edition.digital_work.encoding_urls) {
extraData = this.props.edition.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
}
if (['video', 'audio'].indexOf(mimetype) > -1){
embed = (
<CollapsibleButton
button={
<Button bsSize="xsmall" className="ascribe-margin-1px">
Embed
</Button>
}
panel={
<pre className="">
{'<iframe width="560" height="315" src="http://embed.ascribe.io/edition/'
+ this.props.edition.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'
}
</pre>
}/>
);
}
return (
<div>
<MediaPlayer mimetype={mimetype}
preview={thumbnail}
url={this.props.edition.digital_work.url}
extraData={extraData} />
<p className="text-center">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.edition.digital_work.url} target="_blank">
Download <Glyphicon glyph="cloud-download"/>
</Button>
{embed}
</p>
</div>
);
}
});
let CollapsibleButton = React.createClass({
propTypes: {
button: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
])
},
getInitialState() {
return {expanded: false};
},
handleToggle(e){
e.preventDefault();
this.setState({expanded: !this.state.expanded});
},
render() {
let isVisible = (this.state.expanded) ? '' : 'invisible';
return (
<span>
<span onClick={this.handleToggle}>
{this.props.button}
</span>
<div ref='panel' className={isVisible}>
{this.props.panel}
</div>
</span>
);
}
});
let EditionHeader = React.createClass({
propTypes: {
edition: React.PropTypes.object
},
render() {
var titleHtml = <div className="ascribe-detail-title">{this.props.edition.title}</div>;
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="TITLE" value={titleHtml} />
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.edition.date_created.slice(0, 4) } />
<hr/>
</div>
);
}
});
let EditionSummary = React.createClass({
propTypes: {
@ -259,7 +160,7 @@ let EditionSummary = React.createClass({
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
getStatus(){
let status = null;
if (this.props.edition.status.length > 0){
let statusStr = this.props.edition.status.join().replace(/_/, ' ');
@ -281,6 +182,9 @@ let EditionSummary = React.createClass({
);
}
}
return status;
},
getActions(){
let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){
actions = (
@ -300,16 +204,18 @@ let EditionSummary = React.createClass({
</Col>
</Row>);
}
return actions;
},
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 } />
{status}
{this.getStatus()}
<br/>
{actions}
{this.getActions()}
<hr/>
</div>
);
@ -318,54 +224,6 @@ let EditionSummary = React.createClass({
});
let EditionDetailProperty = React.createClass({
propTypes: {
label: React.PropTypes.string,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
]),
separator: React.PropTypes.string,
labelClassName: React.PropTypes.string,
valueClassName: React.PropTypes.string
},
getDefaultProps() {
return {
separator: ':',
labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3',
valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9'
};
},
render() {
let value = this.props.value;
if (this.props.children){
value = (
<div className="row-same-height">
<div className="col-xs-6 col-xs-height col-bottom no-padding">
{ this.props.value }
</div>
<div className="col-xs-6 col-xs-height">
{ this.props.children }
</div>
</div>);
}
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom'}>
<div>{ this.props.label + this.props.separator}</div>
</div>
<div className={this.props.valueClassName + ' col-xs-height col-bottom'}>
{value}
</div>
</div>
</div>
);
}
});
let EditionDetailHistoryIterator = React.createClass({
propTypes: {
history: React.PropTypes.array

View File

@ -2,10 +2,10 @@
import React from 'react';
import EditionActions from '../actions/edition_actions';
import EditionStore from '../stores/edition_store';
import EditionActions from '../../actions/edition_actions';
import EditionStore from '../../stores/edition_store';
import Edition from './ascribe_detail/edition';
import Edition from './edition';
/**
* This is the component that implements resource/data specific functionality

View File

@ -0,0 +1,174 @@
'use strict';
import React from 'react';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import apiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils';
let FurtherDetails = React.createClass({
propTypes: {
content: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
getInitialState() {
return {
loading: false
};
},
showNotification(){
this.props.handleSuccess();
let notification = new GlobalNotificationModel('Details updated', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
submitKey(key){
this.setState({
otherDataKey: key
});
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
isReadyForFormSubmission(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
if(files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
},
render() {
let editable = this.props.content.acl.indexOf('edit') > -1;
return (<span />);
//return (
// <Row>
// <Col md={12} className="ascribe-edition-personal-note">
// <PieceExtraDataForm
// name='artist_contact_info'
// title='Artist Contact Info'
// handleSuccess={this.showNotification}
// editable={editable}
// content={this.props.content} />
// <PieceExtraDataForm
// name='display_instructions'
// title='Display Instructions'
// handleSuccess={this.showNotification}
// editable={editable}
// content={this.props.content} />
// <PieceExtraDataForm
// name='technology_details'
// title='Technology Details'
// handleSuccess={this.showNotification}
// editable={editable}
// content={this.props.content} />
// <FileUploader
// submitKey={this.submitKey}
// setIsUploadReady={this.setIsUploadReady}
// isReadyForFormSubmission={this.isReadyForFormSubmission}
// editable={editable}
// content={this.props.content}/>
// </Col>
// </Row>
//);
}
});
let FileUploader = React.createClass({
propTypes: {
content: React.PropTypes.object,
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool
},
render() {
// Essentially there a three cases important to the fileuploader
//
// 1. there is no other_data => do not show the fileuploader at all
// 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons
// 3. both other_data and editable are defined or true => show fileuploade with all action buttons
if (!this.props.editable && !this.props.content.other_data){
return null;
}
return (
<Form>
<Property
label="Additional files">
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'otherdata',
bitcoinId: this.props.content.bitcoin_id
}}
createBlobRoutine={{
url: apiUrls.blob_otherdatas,
bitcoinId: this.props.content.bitcoin_id
}}
validation={{
itemLimit: 100000,
sizeLimit: '10000000'
}}
submitKey={this.props.submitKey}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
},
params: {
'pk': this.props.content.other_data ? this.props.content.other_data.id : null
},
cors: {
expected: true,
sendCredentials: true
}
}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}/>
</Property>
<hr />
</Form>
);
}
});
export default FurtherDetails;

View File

@ -0,0 +1,26 @@
'use strict';
import React from 'react';
import EditionDetailProperty from './detail_property';
let Header = React.createClass({
propTypes: {
content: React.PropTypes.object
},
render() {
var titleHtml = <div className="ascribe-detail-title">{this.props.content.title}</div>;
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="TITLE" value={titleHtml} />
<EditionDetailProperty label="BY" value={this.props.content.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.content.date_created.slice(0, 4) } />
<hr/>
</div>
);
}
});
export default Header;

View File

@ -0,0 +1,62 @@
'use strict';
import React from 'react';
import Button from 'react-bootstrap/lib/Button';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MediaPlayer from './../ascribe_media/media_player';
import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
let MediaContainer = React.createClass({
propTypes: {
content: React.PropTypes.object
},
render() {
let thumbnail = this.props.content.thumbnail;
let mimetype = this.props.content.digital_work.mime;
let embed = null;
let extraData = null;
if (this.props.content.digital_work.encoding_urls) {
extraData = this.props.content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
}
if (['video', 'audio'].indexOf(mimetype) > -1){
embed = (
<CollapsibleButton
button={
<Button bsSize="xsmall" className="ascribe-margin-1px">
Embed
</Button>
}
panel={
<pre className="">
{'<iframe width="560" height="315" src="http://embed.ascribe.io/content/'
+ this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'
}
</pre>
}/>
);
}
return (
<div>
<MediaPlayer mimetype={mimetype}
preview={thumbnail}
url={this.props.content.digital_work.url}
extraData={extraData} />
<p className="text-center">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
Download <Glyphicon glyph="cloud-download"/>
</Button>
{embed}
</p>
</div>
);
}
});
export default MediaContainer;

View File

@ -0,0 +1,71 @@
'use strict';
import React from 'react';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import FurtherDetails from './further_details';
//import UserActions from '../../actions/user_actions';
//import UserStore from '../../stores/user_store';
import MediaContainer from './media_container';
import Header from './header';
/**
* This is the component that implements display-specific functionality
*/
let Piece = React.createClass({
propTypes: {
piece: React.PropTypes.object,
loadPiece: React.PropTypes.func
},
//getInitialState() {
// return UserStore.getState();
//},
//
//componentDidMount() {
// UserStore.listen(this.onChange);
// UserActions.fetchCurrentUser();
//},
//
//componentWillUnmount() {
// UserStore.unlisten(this.onChange);
//},
//
//onChange(state) {
// this.setState(state);
//},
render() {
return (
<Row>
<Col md={6}>
<MediaContainer
content={this.props.piece}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<Header
content={this.props.piece}/>
<CollapsibleParagraph
title="Further Details"
show={this.props.piece.acl.indexOf('edit') > -1
|| Object.keys(this.props.piece.extra_data).length > 0
|| this.props.piece.other_data !== null}>
<FurtherDetails
handleSuccess={this.props.loadPiece}
content={this.props.piece}/>
</CollapsibleParagraph>
</Col>
</Row>
);
}
});
export default Piece;

View File

@ -0,0 +1,57 @@
'use strict';
import React from 'react';
import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store';
import Piece from './piece';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
getInitialState() {
return PieceStore.getState();
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
},
componentWillUnmount() {
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
PieceStore.unlisten(this.onChange);
},
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
},
render() {
if('title' in this.state.piece) {
return (
<Piece
piece={this.state.piece}
loadPiece={this.loadPiece}/>
);
} else {
return (
<p>Loading</p>
);
}
}
});
export default PieceContainer;

View File

@ -91,7 +91,7 @@ let RegisterPiece = React.createClass( {
this.state.orderBy,
this.state.orderAsc);
this.transitionTo('edition', {editionId: response.piece.bitcoin_id});
this.transitionTo('piece', {editionId: response.piece.id});
},
getFormData(){

View File

@ -30,7 +30,7 @@ let apiUrls = {
'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/',
'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/',
'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/',
'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}',
'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}/',
'piece_extradata': AppConstants.apiEndpoint + 'pieces/${piece_id}/extradata/',
'piece_first_edition_id': AppConstants.apiEndpoint + 'pieces/${piece_id}/edition_index/',
'pieces_list': AppConstants.apiEndpoint + 'pieces/',

View File

@ -5,11 +5,10 @@ import requests from '../utils/requests';
let PieceFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
* Fetch a piece from the API.
*/
fetchOne() {
return requests.get('piece');
fetchOne(id) {
return requests.get('piece', {'piece_id': id});
}
};

View File

@ -5,7 +5,8 @@ import Router from 'react-router';
import AscribeApp from './components/ascribe_app';
import PieceList from './components/piece_list';
import EditionContainer from './components/edition_container';
import PieceContainer from './components/ascribe_detail/piece_container';
import EditionContainer from './components/ascribe_detail/edition_container';
import LoginContainer from './components/login_container';
import SignupContainer from './components/signup_container';
@ -26,6 +27,7 @@ let routes = (
<Route name="signup" path="signup" handler={SignupContainer} />
<Route name="login" path="login" handler={LoginContainer} />
<Route name="pieces" path="collection" handler={PieceList} />
<Route name="piece" path="pieces/:pieceId" handler={PieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="register_piece" path="register_piece" handler={RegisterPiece} />