Merge branch 'AD-416-account-settings-page-navbar' of bitbucket.org:ascribe/onion into AD-416-account-settings-page-navbar

Conflicts:
	sass/main.scss
This commit is contained in:
Tim Daubenschütz 2015-06-24 15:50:45 +02:00
commit ea88e87355
36 changed files with 15684 additions and 390 deletions

View File

@ -4,4 +4,5 @@ node_modules
js/**/__tests__
server.js
server.js
js/components/ascribe_uploader/vendor

1
.gitignore vendored
View File

@ -12,7 +12,6 @@ logs
results
node_modules/*
!node_modules/react-s3-fineuploader
build

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[submodule "node_modules/react-s3-fineuploader"]
path = node_modules/react-s3-fineuploader
url = https://bitbucket.org/ascribe/react-s3-fineuploader.git

View File

@ -69,7 +69,7 @@ gulp.task('js:build', function() {
bundle(false);
});
gulp.task('serve', ['browser-sync', 'run-server', 'sass:watch', 'copy'], function() {
gulp.task('serve', ['browser-sync', 'run-server', 'lint:watch', 'sass:build', 'sass:watch', 'copy'], function() {
bundle(true);
});

View File

@ -1,4 +1,5 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">

View File

@ -0,0 +1,34 @@
'use strict';
import alt from '../alt';
import ApplicationFetcher from '../fetchers/application_fetcher';
class ApplicationActions {
constructor() {
this.generateActions(
'updateApplications'
);
}
fetchApplication() {
ApplicationFetcher.fetch()
.then((res) => {
this.actions.updateApplications(res.applications);
})
.catch((err) => {
console.log(err);
});
}
refreshApplicationToken(applicationName) {
ApplicationFetcher.refreshToken(applicationName)
.then((res) => {
this.actions.updateApplications(res.applications);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(ApplicationActions);

View File

@ -18,6 +18,12 @@ const CollapsibleParagraph = React.createClass({
iconName: React.PropTypes.string
},
getDefaultProps() {
return {
show: true
};
},
mixins: [CollapsibleMixin],
getCollapsibleDOMNode(){
@ -35,19 +41,23 @@ const CollapsibleParagraph = React.createClass({
render() {
let styles = this.getCollapsibleClassSet();
let text = this.isExpanded() ? '-' : '+';
return (
<div className="ascribe-detail-header">
<div className="ascribe-edition-collapsible-wrapper">
<div onClick={this.handleToggle}>
<span>{text} {this.props.title} </span>
</div>
<div ref='panel' className={classNames(styles) + ' ascribe-edition-collapible-content'}>
{this.props.children}
let text = this.isExpanded() ? '[hide]' : '[show]';
if(this.props.show) {
return (
<div className="ascribe-detail-header">
<div className="ascribe-edition-collapsible-wrapper">
<div onClick={this.handleToggle}>
<span>{this.props.title}</span><span className="pull-right">{text}</span>
</div>
<div ref='panel' className={classNames(styles) + ' ascribe-edition-collapible-content'}>
{this.props.children}
</div>
</div>
</div>
</div>
);
);
} else {
return null;
}
}
});

View File

@ -10,6 +10,9 @@ import AlertDismissable from './alert';
let Form = React.createClass({
propTypes: {
url: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
getFormData: React.PropTypes.func,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
@ -47,6 +50,9 @@ let Form = React.createClass({
},
getFormData(){
if ('getFormData' in this.props){
return this.props.getFormData();
}
let data = {};
for (let ref in this.refs){
data[this.refs[ref].props.name] = this.refs[ref].state.value;
@ -66,7 +72,7 @@ let Form = React.createClass({
this.refs[ref].handleSuccess();
}
}
this.setState({edited: false});
this.setState({edited: false, submitted: false});
},
handleError(err){
if (err.json) {
@ -102,9 +108,11 @@ let Form = React.createClass({
if (this.state.edited){
buttons = (
<div className="pull-right">
<Button className="ascribe-btn" type="submit">Save</Button>
<Button className="ascribe-btn" onClick={this.reset}>Cancel</Button>
<div className="row" style={{margin: 0}}>
<p className="pull-right">
<Button className="ascribe-btn" type="submit">Save</Button>
<Button className="ascribe-btn" onClick={this.reset}>Cancel</Button>
</p>
</div>
);

View File

@ -5,47 +5,58 @@ import React from 'react';
import requests from '../../utils/requests';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
let PieceExtraDataForm = React.createClass({
mixins: [FormMixin],
url() {
return requests.prepareUrl(apiUrls.piece_extradata, {piece_id: this.props.editions[0].bitcoin_id});
propTypes: {
edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
name: React.PropTypes.string,
title: React.PropTypes.string,
editable: React.PropTypes.bool
},
getFormData() {
getFormData(){
let extradata = {};
extradata[this.props.name] = this.refs[this.props.name].state.value;
extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value;
return {
bitcoin_id: this.getBitcoinIds().join(),
bitcoin_id: this.props.edition.bitcoin_id,
extradata: extradata
};
},
renderForm() {
let defaultValue = this.props.editions[0].extra_data[this.props.name] || '';
render() {
let defaultValue = this.props.edition.extra_data[this.props.name] || '';
if (defaultValue.length === 0 && !this.props.editable){
return null;
}
let url = requests.prepareUrl(apiUrls.piece_extradata, {piece_id: this.props.edition.bitcoin_id});
return (
<form role="form" key={this.props.name}>
<h5>{this.props.title}</h5>
<InputTextAreaToggable
ref={this.props.name}
className="form-control"
defaultValue={defaultValue}
rows={3}
editable={this.props.editable}
required=""
onSubmit={this.submit}
/>
</form>
<Form
ref='form'
url={url}
handleSuccess={this.props.handleSuccess}
getFormData={this.getFormData}>
<Property
name={this.props.name}
label={this.props.title}
editable={this.props.editable}>
<InputTextAreaToggable
rows={3}
editable={this.props.editable}
defaultValue={defaultValue}
placeholder={'Fill in ' + this.props.title}
required/>
</Property>
<Property hidden={true} name='bitcoin_id'>
<input defaultValue={this.props.edition.bitcoin_id}/>
</Property>
<hr />
</Form>
);
}
});
export default PieceExtraDataForm;

View File

@ -12,7 +12,7 @@ let ShareForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_shares_mail;
return ApiUrls.ownership_shares;
},
getFormData() {

View File

@ -2,56 +2,29 @@
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin';
import TextareaAutosize from 'react-textarea-autosize';
import Button from 'react-bootstrap/lib/Button';
let InputTextAreaToggable = React.createClass({
propTypes: {
editable: React.PropTypes.bool.isRequired,
submitted: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
onSubmit: React.PropTypes.func.isRequired,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
},
mixins: [AlertMixin],
getInitialState() {
return {
value: this.props.defaultValue,
edited: false,
alerts: null // needed in AlertMixin
value: this.props.defaultValue
};
},
handleChange(event) {
this.setState({
value: event.target.value,
edited: true
});
},
reset(){
this.setState(this.getInitialState());
},
submit(){
this.props.onSubmit();
this.setState({edited: false});
this.setState({value: event.target.value});
this.props.onChange(event);
},
render() {
let className = 'form-control ascribe-textarea';
let buttons = null;
let textarea = null;
if (this.props.editable && this.state.edited){
buttons = (
<div className="pull-right">
<Button className="ascribe-btn" onClick={this.submit}>Save</Button>
<Button className="ascribe-btn" onClick={this.reset}>Cancel</Button>
</div>
);
}
if (this.props.editable){
className = className + ' ascribe-textarea-editable';
textarea = (
@ -61,21 +34,16 @@ let InputTextAreaToggable = React.createClass({
rows={this.props.rows}
required={this.props.required}
onChange={this.handleChange}
placeholder='Write something...' />
onBlur={this.props.onBlur}
placeholder={this.props.placeholder} />
);
}
else{
textarea = <pre className="ascribe-pre">{this.state.value}</pre>;
}
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
{textarea}
{buttons}
</div>
);
return textarea;
}
});
export default InputTextAreaToggable;

View File

@ -8,7 +8,9 @@ import Tooltip from 'react-bootstrap/lib/Tooltip';
let Property = React.createClass({
propTypes: {
hidden: React.PropTypes.bool,
editable: React.PropTypes.bool,
tooltip: React.PropTypes.element,
label: React.PropTypes.string,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
@ -19,7 +21,8 @@ let Property = React.createClass({
getDefaultProps() {
return {
editable: true
editable: true,
hidden: false
};
},
@ -38,8 +41,16 @@ let Property = React.createClass({
});
},
reset(){
// maybe do reset by reload instead of frontend state?
this.setState({value: this.state.initialValue});
this.refs.input.getDOMNode().value = this.state.initialValue;
if (this.refs.input.state){
// This is probably not the right way but easy fix
this.refs.input.state.value = this.state.initialValue;
}
else{
this.refs.input.getDOMNode().value = this.state.initialValue;
}
},
handleChange(event) {
@ -74,6 +85,9 @@ let Property = React.createClass({
this.setState({errors: null});
},
getClassName() {
if(this.props.hidden){
return 'is-hidden';
}
if(!this.props.editable){
return 'is-fixed';
}

View File

@ -0,0 +1,122 @@
import React from 'react';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
var FileDragAndDrop = React.createClass({
propTypes: {
onDragStart: React.PropTypes.func,
onDrop: React.PropTypes.func.isRequired,
onDragEnter: React.PropTypes.func,
onLeave: React.PropTypes.func,
onDragOver: React.PropTypes.func,
onDragEnd: React.PropTypes.func,
filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func
},
handleDragStart(event) {
if (typeof this.props.onDragStart === 'function') {
this.props.onDragStart(event);
}
},
handleDrag(event) {
if (typeof this.props.onDrag === 'function') {
this.props.onDrag(event);
}
},
handleDragEnd(event) {
if (typeof this.props.onDragEnd === 'function') {
this.props.onDragEnd(event);
}
},
handleDragEnter(event) {
if (typeof this.props.onDragEnter === 'function') {
this.props.onDragEnter(event);
}
},
handleDragLeave(event) {
if (typeof this.props.onDragLeave === 'function') {
this.props.onDragLeave(event);
}
},
handleDragOver(event) {
event.preventDefault();
if (typeof this.props.onDragOver === 'function') {
this.props.onDragOver(event);
}
},
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
let files;
// handle Drag and Drop
if(event.dataTransfer && event.dataTransfer.files.length > 0) {
files = event.dataTransfer.files;
} else if(event.target.files) { // handle input type file
files = event.target.files;
}
if(typeof this.props.onDrop === 'function' && files) {
this.props.onDrop(files);
}
},
handleDeleteFile(fileId) {
// input's value is not change the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.props.handleDeleteFile(fileId);
},
handleOnClick() {
// Simulate click on hidden file input
var event = document.createEvent('HTMLEvents');
event.initEvent('click', false, true);
this.refs.fileinput.getDOMNode().dispatchEvent(event);
},
render: function () {
let hasFiles = this.props.filesToUpload.length > 0;
return (
<div
className={hasFiles ? 'file-drag-and-drop has-files' : 'file-drag-and-drop' }
onClick={this.handleOnClick}
onDragStart={this.handleDragStart}
onDrag={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.handleDragEnd}>
{hasFiles ? null : <span>Click or drag to add files</span>}
<FileDragAndDropPreviewIterator
files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile}/>
<input
multiple
ref="fileinput"
type="file"
style={{
display: 'none',
height: 0,
width: 0
}}
onChange={this.handleDrop} />
</div>
);
}
});
module.exports = FileDragAndDrop;

View File

@ -0,0 +1,52 @@
import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
let FileDragAndDropPreview = React.createClass({
propsTypes: {
file: React.PropTypes.shape({
url: React.PropTypes.string,
type: React.PropTypes.string
}).isRequired,
handleDeleteFile: React.PropTypes.func
},
handleDeleteFile(event) {
event.preventDefault();
event.stopPropagation();
// handleDeleteFile is optional, so if its not submitted,
// don't run it
if(this.props.handleDeleteFile) {
this.props.handleDeleteFile(this.props.file.id);
}
},
render() {
let previewElement;
// Decide wether an image or a placeholder picture should be displayed
if(this.props.file.type.split('/')[0] === 'image') {
previewElement = (<FileDragAndDropPreviewImage
progress={this.props.file.progress}
url={this.props.file.url}/>);
} else {
previewElement = (<FileDragAndDropPreviewOther
progress={this.props.file.progress}
type={this.props.file.type.split('/')[1]}/>);
}
return (
<div
className="file-drag-and-drop-position"
onClick={this.handleDeleteFile}>
{previewElement}
</div>
);
}
});
export default FileDragAndDropPreview;

View File

@ -0,0 +1,26 @@
import React from 'react';
import ProgressBar from 'react-progressbar';
let FileDragAndDropPreviewImage = React.createClass({
propTypes: {
progress: React.PropTypes.number,
url: React.PropTypes.string
},
render() {
let imageStyle = {
backgroundImage: 'url("' + this.props.url + '")',
backgroundSize: 'cover'
};
return (
<div
className="file-drag-and-drop-preview-image"
style={imageStyle}>
<ProgressBar completed={this.props.progress} color="black"/>
</div>
);
}
});
export default FileDragAndDropPreviewImage;

View File

@ -0,0 +1,31 @@
import React from 'react';
import FileDragAndDropPreview from './file_drag_and_drop_preview';
let FileDragAndDropPreviewIterator = React.createClass({
propTypes: {
files: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func
},
render() {
if(this.props.files) {
return (
<div>
{this.props.files.map((file, i) => {
return (
<FileDragAndDropPreview
key={i}
file={file}
handleDeleteFile={this.props.handleDeleteFile}/>
);
})}
</div>
);
} else {
return null;
}
}
});
export default FileDragAndDropPreviewIterator;

View File

@ -0,0 +1,25 @@
import React from 'react';
import ProgressBar from 'react-progressbar';
let FileDragAndDropPreviewOther = React.createClass({
propTypes: {
type: React.PropTypes.string,
progress: React.PropTypes.number
},
render() {
return(
<div
className="file-drag-and-drop-preview">
<ProgressBar completed={this.props.progress} color="black"/>
<div className="file-drag-and-drop-preview-table-wrapper">
<div className="file-drag-and-drop-preview-other">
<span>{'.' + this.props.type}</span>
</div>
</div>
</div>
);
}
});
export default FileDragAndDropPreviewOther;

View File

@ -0,0 +1,306 @@
'use strict';
import React from 'react/addons';
import promise from 'es6-promise';
promise.polyfill();
import fetch from 'isomorphic-fetch';
import fineUploader from 'fineUploader';
import FileDragAndDrop from './file_drag_and_drop';
var ReactS3FineUploader = React.createClass({
propTypes: {
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
fileClass: React.PropTypes.string
}),
createBlobRoutine: React.PropTypes.shape({
url: React.PropTypes.string
}),
handleChange: React.PropTypes.func,
autoUpload: React.PropTypes.bool,
debug: React.PropTypes.bool,
objectProperties: React.PropTypes.shape({
acl: React.PropTypes.string
}),
request: React.PropTypes.shape({
endpoint: React.PropTypes.string,
accessKey: React.PropTypes.string,
params: React.PropTypes.shape({
csrfmiddlewaretoken: React.PropTypes.string
})
}),
signature: React.PropTypes.shape({
endpoint: React.PropTypes.string
}),
uploadSuccess: React.PropTypes.shape({
method: React.PropTypes.string,
endpoint: React.PropTypes.string,
params: React.PropTypes.shape({
isBrowserPreviewCapable: React.PropTypes.any, // maybe fix this later
bitcoin_ID_noPrefix: React.PropTypes.string
})
}),
cors: React.PropTypes.shape({
expected: React.PropTypes.bool
}),
chunking: React.PropTypes.shape({
enabled: React.PropTypes.bool
}),
resume: React.PropTypes.shape({
enabled: React.PropTypes.bool
}),
deleteFile: React.PropTypes.shape({
enabled: React.PropTypes.bool,
method: React.PropTypes.string,
endpoint: React.PropTypes.string
}),
session: React.PropTypes.shape({
endpoint: React.PropTypes.bool
}),
validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string
}),
messages: React.PropTypes.shape({
unsupportedBrowser: React.PropTypes.string
}),
formatFileName: React.PropTypes.func,
multiple: React.PropTypes.bool,
retry: React.PropTypes.shape({
enableAuto: React.PropTypes.bool
})
},
getInitialState() {
return {
filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig())
};
},
propsToConfig() {
let objectProperties = this.props.objectProperties;
objectProperties.key = this.requestKey;
return {
autoUpload: this.props.autoUpload,
debug: this.props.debug,
objectProperties: objectProperties, // do a special key handling here
request: this.props.request,
signature: this.props.signature,
uploadSuccess: this.props.uploadSuccess,
cors: this.props.cors,
chunking: this.props.chunking,
resume: this.props.resume,
deleteFile: this.props.deleteFile,
session: this.props.session,
validation: this.props.validation,
messages: this.props.messages,
formatFileName: this.props.formatFileName,
multiple: this.props.multiple,
retry: this.props.retry,
callbacks: {
onSubmit: this.onSubmit,
onComplete: this.onComplete,
onDelete: this.onDelete,
onSessionRequestComplete: this.onSessionRequestComplete,
onProgress: this.onProgress,
onRetry: this.onRetry,
onAutoRetry: this.onAutoRetry,
onManualRetry: this.onManualRetry,
onDeleteComplete: this.onDeleteComplete
}
};
},
getCookie(name) {
console.log(document.cookie);
let value = '; ' + document.cookie;
let parts = value.split('; ' + name + '=');
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
},
requestKey(fileId) {
let filename = this.state.uploader.getName(fileId);
let defer = new fineUploader.Promise();
fetch(this.props.keyRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': this.getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
'filename': filename,
'file_class': 'digitalwork'
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
defer.success(res.key);
})
.catch((err) => {
console.error(err);
});
return defer;
},
/* FineUploader specific callback function handlers */
onSubmit() {
console.log('submit');
},
onComplete(id) {
let files = this.state.filesToUpload;
files[id].status = 'upload successful';
files[id].key = this.state.uploader.getKey(id);
let newState = React.addons.update(this.state, {
filesToUpload: { $set: files }
});
this.setState(newState);
this.createBlob(files[id]);
this.props.handleChange();
console.log('completed ' + files[id].name);
},
createBlob(file) {
let defer = new fineUploader.Promise();
fetch(this.props.createBlobRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': this.getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
defer.success(res.key);
})
.catch((err) => {
console.error(err);
});
return defer;
},
onRetry() {
console.log('retry');
},
onAutoRetry() {
console.log('autoretry');
},
onManualRetry() {
console.log('manualretry');
},
onDelete() {
console.log('delete');
},
onCancel() {
console.log('cancel');
},
onSessionRequestComplete() {
console.log('sessionrequestcomplete');
},
onDeleteComplete(id, xhr, isError) {
if(isError) {
// also, sync files from state with the ones from fineuploader
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can
filesToUpload.splice(id, 1);
// set state
this.setState({
filesToUpload: React.addons.update(this.state.filesToUpload, {$set: filesToUpload})
});
} else {
console.log(id);
// TODO: add global notification
}
},
onProgress(id, name, uploadedBytes, totalBytes) {
var newState = React.addons.update(this.state, {
filesToUpload: { [id]: {
progress: { $set: (uploadedBytes / totalBytes) * 100} }
}
});
this.setState(newState);
},
handleDeleteFile(fileId) {
// delete file from server
this.state.uploader.deleteFile(fileId);
// this is being continues in onDeleteFile, as
// fineuploaders deleteFile does not return a correct callback or
// promise
},
handleUploadFile(files) {
this.state.uploader.addFiles(files);
let oldFiles = this.state.filesToUpload;
let oldAndNewFiles = this.state.uploader.getUploads();
// Add fineuploader specific information to new files
for(let i = 0; i < oldAndNewFiles.length; i++) {
for(let j = 0; j < files.length; j++) {
if(oldAndNewFiles[i].originalName === files[j].name) {
oldAndNewFiles[i].progress = 0;
oldAndNewFiles[i].type = files[j].type;
oldAndNewFiles[i].url = URL.createObjectURL(files[j]);
}
}
}
// and re-add fineuploader specific information for old files as well
for(let i = 0; i < oldAndNewFiles.length; i++) {
for(let j = 0; j < oldFiles.length; j++) {
if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
oldAndNewFiles[i].progress = oldFiles[j].progress;
oldAndNewFiles[i].type = oldFiles[j].type;
oldAndNewFiles[i].url = oldFiles[j].url;
}
}
}
let newState = React.addons.update(this.state, {
filesToUpload: { $set: oldAndNewFiles }
});
this.setState(newState);
},
render() {
return (
<FileDragAndDrop
onDrop={this.handleUploadFile}
filesToUpload={this.state.filesToUpload}
handleDeleteFile={this.handleDeleteFile}/>
);
}
});
export default ReactS3FineUploader;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -2,19 +2,21 @@
import React from 'react';
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 UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import MediaPlayer from './ascribe_media/media_player';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin';
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 CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph';
import PersonalNoteForm from './ascribe_forms/form_note_personal';
import EditionNoteForm from './ascribe_forms/form_note_edition';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import PieceExtraDataForm from './ascribe_forms/form_piece_extradata';
import RequestActionForm from './ascribe_forms/form_request_action';
@ -25,7 +27,8 @@ import AclButtonList from './ascribe_buttons/acl_button_list';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import classNames from 'classnames';
import requests from '../utils/requests';
import apiUrls from '../constants/api_urls';
/**
* This is the component that implements display-specific functionality
@ -92,64 +95,72 @@ let Edition = React.createClass({
<EditionSummary
edition={this.props.edition} />
<CollapsibleEditionDetails
title="Personal Note (private)"
show={this.state.currentUser.username && true || false}>
<CollapsibleParagraph
title="Notes"
show={(this.state.currentUser.username && true || false) ||
(this.props.edition.acl.indexOf('edit') > -1 || this.props.edition.public_note)}>
<EditionPersonalNote
currentUser={this.state.currentUser}
handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleEditionDetails>
<CollapsibleEditionDetails
title="Edition Note (public)"
show={this.props.edition.acl.indexOf('edit') > -1 || this.props.edition.public_note}>
<EditionPublicEditionNote
handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleEditionDetails>
</CollapsibleParagraph>
<CollapsibleEditionDetails
<CollapsibleParagraph
title="Further Details (all editions)"
show={this.props.edition.acl.indexOf('edit') > -1 || Object.keys(this.props.edition.extra_data).length > 0}>
<EditionFurtherDetails
handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleEditionDetails>
</CollapsibleParagraph>
<CollapsibleEditionDetails
<CollapsibleParagraph
title="Provenance/Ownership History"
show={this.props.edition.ownership_history && this.props.edition.ownership_history.length > 0}>
<EditionDetailHistoryIterator
history={this.props.edition.ownership_history} />
</CollapsibleEditionDetails>
</CollapsibleParagraph>
<CollapsibleEditionDetails
<CollapsibleParagraph
title="Consignment History"
show={this.props.edition.consign_history && this.props.edition.consign_history.length > 0}>
<EditionDetailHistoryIterator
history={this.props.edition.consign_history} />
</CollapsibleEditionDetails>
</CollapsibleParagraph>
<CollapsibleEditionDetails
<CollapsibleParagraph
title="Loan History"
show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}>
<EditionDetailHistoryIterator
history={this.props.edition.loan_history} />
</CollapsibleEditionDetails>
</CollapsibleParagraph>
<CollapsibleEditionDetails
<CollapsibleParagraph
title="SPOOL Details">
<EditionDetailProperty
label="Artwork ID"
value={bitcoinIdValue} />
<EditionDetailProperty
label="Hash of Artwork, title, etc"
value={hashOfArtwork} />
<EditionDetailProperty
label="Owned by SPOOL address"
value={ownerAddress} />
</CollapsibleEditionDetails>
<Form >
<Property
name='artwork_id'
label="Artwork ID"
editable={false}>
<pre className="ascribe-pre">{bitcoinIdValue}</pre>
</Property>
<Property
name='hash_of_artwork'
label="Hash of Artwork, title, etc"
editable={false}>
<pre className="ascribe-pre">{hashOfArtwork}</pre>
</Property>
<Property
name='owner_address'
label="Owned by SPOOL address"
editable={false}>
<pre className="ascribe-pre">{ownerAddress}</pre>
</Property>
<hr />
</Form>
</CollapsibleParagraph>
</Col>
</Row>
);
@ -229,85 +240,6 @@ let EditionSummary = React.createClass({
}
});
let CollapsibleEditionDetails = React.createClass({
propTypes: {
title: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
show: React.PropTypes.bool,
iconName: React.PropTypes.string
},
getDefaultProps() {
return {
show: true
};
},
render() {
if(this.props.show) {
return (
<div className="ascribe-detail-header">
<CollapsibleParagraph
title={this.props.title}
iconName={this.props.iconName}>
{this.props.children}
</CollapsibleParagraph>
</div>
);
} else {
return null;
}
}
});
const CollapsibleParagraph = React.createClass({
propTypes: {
title: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
iconName: React.PropTypes.string
},
mixins: [CollapsibleMixin],
getCollapsibleDOMNode(){
return React.findDOMNode(this.refs.panel);
},
getCollapsibleDimensionValue(){
return React.findDOMNode(this.refs.panel).scrollHeight;
},
onHandleToggle(e){
e.preventDefault();
this.setState({expanded: !this.state.expanded});
},
render() {
let styles = this.getCollapsibleClassSet();
let text = this.isExpanded() ? '-' : '+';
let icon = this.props.iconName ? (<Glyphicon style={{fontSize: '.75em'}} glyph={this.props.iconName} />) : null;
return (
<div className="ascribe-edition-collapsible-wrapper">
<div onClick={this.onHandleToggle}>
<span>{text} {icon} {this.props.title} </span>
</div>
<div ref='panel' className={classNames(styles) + ' ascribe-edition-collapible-content'}>
{this.props.children}
</div>
</div>
);
}
});
let EditionDetailProperty = React.createClass({
propTypes: {
@ -372,6 +304,7 @@ let EditionDetailHistoryIterator = React.createClass({
let EditionPersonalNote = React.createClass({
propTypes: {
edition: React.PropTypes.object,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
showNotification(){
@ -379,21 +312,35 @@ let EditionPersonalNote = React.createClass({
let notification = new GlobalNotificationModel('Private note saved', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
<PersonalNoteForm
editable={true}
handleSuccess={this.showNotification}
editions={[this.props.edition]} />
</Col>
</Row>
);
if (this.props.currentUser.username && true || false) {
return (
<Form
url={apiUrls.note_notes}
handleSuccess={this.showNotification}>
<Property
name='note'
label='Personal note (private)'
editable={true}>
<InputTextAreaToggable
rows={3}
editable={true}
defaultValue={this.props.edition.note_from_user}
placeholder='Enter a personal note...'
required/>
</Property>
<Property hidden={true} name='bitcoin_id'>
<input defaultValue={this.props.edition.bitcoin_id}/>
</Property>
<hr />
</Form>
);
}
return null;
}
});
let EditionPublicEditionNote = React.createClass({
propTypes: {
edition: React.PropTypes.object,
@ -405,16 +352,31 @@ let EditionPublicEditionNote = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
<EditionNoteForm
editable={this.props.edition.acl.indexOf('edit') > -1}
handleSuccess={this.showNotification}
editions={[this.props.edition]} />
</Col>
</Row>
);
let isEditable = this.props.edition.acl.indexOf('edit') > -1;
if (this.props.edition.acl.indexOf('edit') > -1 || this.props.edition.public_note){
return (
<Form
url={apiUrls.note_edition}
handleSuccess={this.showNotification}>
<Property
name='note'
label='Edition note (public)'
editable={isEditable}>
<InputTextAreaToggable
rows={3}
editable={isEditable}
defaultValue={this.props.edition.public_note}
placeholder='Enter a public note for this edition...'
required/>
</Property>
<Property hidden={true} name='bitcoin_id'>
<input defaultValue={this.props.edition.bitcoin_id}/>
</Property>
<hr />
</Form>
);
}
return null;
}
});
@ -440,19 +402,19 @@ let EditionFurtherDetails = React.createClass({
title='Artist Contact Info'
handleSuccess={this.showNotification}
editable={editable}
editions={[this.props.edition]} />
edition={this.props.edition} />
<PieceExtraDataForm
name='display_instructions'
title='Display Instructions'
handleSuccess={this.showNotification}
editable={editable}
editions={[this.props.edition]} />
edition={this.props.edition} />
<PieceExtraDataForm
name='technology_details'
title='Technology Details'
handleSuccess={this.showNotification}
editable={editable}
editions={[this.props.edition]} />
edition={this.props.edition} />
</Col>
</Row>
);

View File

@ -39,6 +39,7 @@ let Header = React.createClass({
handleLogout(){
UserActions.logoutCurrentUser();
Alt.flush();
this.transitionTo('login');
},
onChange(state) {
this.setState(state);
@ -71,7 +72,6 @@ let Header = React.createClass({
<Navbar brand={brand} toggleNavKey={0}>
<CollapsibleNav eventKey={0}>
<Nav navbar />
<Nav navbar right>
{account}
{signup}

View File

@ -15,18 +15,99 @@ import Property from './ascribe_forms/property';
import apiUrls from '../constants/api_urls';
import ReactS3FineUploader from 'ReactS3FineUploader';
import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import DatePicker from 'react-datepicker/dist/react-datepicker';
let RegisterPiece = React.createClass( {
render() {
mixins: [Router.Navigation],
getInitialState(){
return {digital_work_key: null};
},
handleSuccess(){
let notification = new GlobalNotificationModel('Login successsful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
getFormData(){
let data = {};
for (let ref in this.refs.form.refs){
data[this.refs.form.refs[ref].props.name] = this.refs.form.refs[ref].state.value;
}
data.digital_work_key = this.state.digital_work_key;
return data;
},
handleChange(){
this.setState({digital_work_key: this.refs.uploader.refs.fineuploader.state.filesToUpload[0].key})
},
render() {
let buttons = null;
if (this.refs.uploader && this.refs.uploader.refs.fineuploader.state.filesToUpload[0].status === 'upload successful'){
buttons = (
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Register your artwork
</button>);
}
return (
<div className="row ascribe-row">
<div className="col-md-6">
<FileUploader />
<div className="col-md-5">
<FileUploader
ref='uploader'
handleChange={this.handleChange}/>
<br />
</div>
<div className="col-md-6">
<LoginForm />
<div className="col-md-7">
<h3 style={{'marginTop': 0}}>Lock down title</h3>
<Form
ref='form'
url={apiUrls.pieces_list}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={buttons}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='artist_name'
label="Artist Name">
<input
type="text"
placeholder="The name of the creator"
required/>
</Property>
<Property
name='title'
label="Artwork title">
<input
type="text"
placeholder="The title of the artwork"
required/>
</Property>
<Property
name='date_created'
label="Year Created">
<input
type="number"
placeholder="Year Created (e.g. 2015)"
min={0}
required/>
</Property>
<Property
name='num_editions'
label="Number of editions">
<input
type="number"
placeholder="Specify the number of unique editions for this artwork"
min={1}
required/>
</Property>
<hr />
</Form>
</div>
</div>
);
@ -36,13 +117,17 @@ let RegisterPiece = React.createClass( {
let FileUploader = React.createClass( {
render() {
return (
<ReactS3FineUploader
ref='fineuploader'
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: apiUrls.blob_digitalworks
}}
handleChange={this.props.handleChange}
autoUpload={true}
debug={false}
objectProperties={{
@ -94,60 +179,46 @@ let FileUploader = React.createClass( {
}
return name;
}}
multiple={true}/>
multiple={false}/>
);
}
});
let LoginForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(){
let notification = new GlobalNotificationModel('Login successsful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
let InputDate = React.createClass({
propTypes: {
placeholderText: React.PropTypes.string
},
render() {
getInitialState() {
return {
value: null,
value_formatted: null
};
},
handleChange(date) {
this.setState({
value: date,
value_formatted: date.format('YYYY')});
let event = document.createEvent('HTMLEvents');
event.initEvent('click', false, true);
document.dispatchEvent(event);
event.target.value = date;
this.props.onChange(event);
},
render: function () {
return (
<Form
url={apiUrls.users_login}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Log in to ascribe
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='email'
label="Email">
<input
type="email"
placeholder="Enter your email"
autoComplete="on"
required/>
</Property>
<Property
name='password'
label="Password">
<input
type="password"
placeholder="Enter your password"
autoComplete="on"
required/>
</Property>
<hr />
<div className="ascribe-login-text">
Not an ascribe user&#63; Sign up...<br/>
Forgot my password&#63; Rescue me...
</div>
</Form>
<DatePicker
key="example2"
dateFormat="YYYY"
selected={this.state.value}
onChange={this.handleChange}
onBlur={this.props.onBlur}
placeholderText={this.props.placeholderText}/>
);
}
});
export default RegisterPiece;

View File

@ -9,6 +9,9 @@ import UserStore from '../stores/user_store';
import WalletSettingsActions from '../actions/wallet_settings_actions';
import WalletSettingsStore from '../stores/wallet_settings_store';
import ApplicationActions from '../actions/application_actions';
import ApplicationStore from '../stores/application_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
@ -28,6 +31,7 @@ let SettingsContainer = React.createClass({
<div>
<AccountSettings />
<BitcoinWalletSettings />
<APISettings />
</div>
);
}
@ -54,47 +58,65 @@ let AccountSettings = React.createClass({
handleSuccess(){
UserActions.fetchCurrentUser();
let notification = new GlobalNotificationModel('username succesfully updated', 'success', 10000);
let notification = new GlobalNotificationModel('username succesfully updated', 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.currentUser.username) {
return (
<CollapsibleParagraph
title="Account"
show={true}
defaultExpanded={true}>
<Form
url={apiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='username'
label="Username">
<input
type="text"
defaultValue={this.state.currentUser.username}
placeholder="Enter your username"
required/>
</Property>
<Property
name='email'
label="Email"
editable={false}>
<input
type="text"
defaultValue={this.state.currentUser.email}
placeholder="Enter your username"
required/>
</Property>
</Form>
</CollapsibleParagraph>
);
}
else {
return (
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
content = (
<Form
url={apiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='username'
label="Username">
<input
type="text"
defaultValue={this.state.currentUser.username}
placeholder="Enter your username"
required/>
</Property>
<Property
name='email'
label="Email"
editable={false}>
<input
type="text"
defaultValue={this.state.currentUser.email}
placeholder="Enter your username"
required/>
</Property>
<hr />
</Form>
);
}
return (
<CollapsibleParagraph
title="Account"
show={true}
defaultExpanded={true}>
{content}
<Form
url={AppConstants.serverUrl + 'api/users/set_language/'}>
<Property
name='language'
label="Choose your Language"
editable={true}>
<select id="select-lang" name="language">
<option value="fr">
Fran&ccedil;ais
</option>
<option value="en"
selected="selected">
English
</option>
</select>
</Property>
<hr />
</Form>
</CollapsibleParagraph>
);
}
});
@ -120,42 +142,33 @@ let BitcoinWalletSettings = React.createClass({
},
render() {
if (this.state.walletSettings.btc_public_key) {
return (
<CollapsibleParagraph
title="Crypto Wallet"
show={true}
defaultExpanded={true}>
<Form
url={apiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='btc_public_key'
label="Bitcoin public key"
editable={false}>
<input
type="text"
defaultValue={this.state.walletSettings.btc_public_key}
placeholder="Enter your username"
required/>
</Property>
<Property
name='btc_root_address'
label="Root Address"
editable={false}>
<input
type="text"
defaultValue={this.state.walletSettings.btc_root_address}/>
</Property>
</Form>
</CollapsibleParagraph>
);
}
else {
return (
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
);
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.walletSettings.btc_public_key) {
content = (
<Form >
<Property
name='btc_public_key'
label="Bitcoin public key"
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_public_key}</pre>
</Property>
<Property
name='btc_root_address'
label="Root Address"
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_root_address}</pre>
</Property>
<hr />
</Form>);
}
return (
<CollapsibleParagraph
title="Crypto Wallet"
show={true}
defaultExpanded={true}>
{content}
</CollapsibleParagraph>
);
}
});
@ -177,18 +190,88 @@ let ContractSettings = React.createClass({
});
let APISettings = React.createClass({
propTypes: {
currentUser: React.PropTypes.object
getInitialState() {
return ApplicationStore.getState();
},
render() {
componentDidMount() {
ApplicationStore.listen(this.onChange);
ApplicationActions.fetchApplication();
},
componentWillUnmount() {
ApplicationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleCreateSuccess: function(){
ApplicationActions.fetchApplication();
let notification = new GlobalNotificationModel('Application successfully created', 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
handleTokenRefresh: function(event){
let applicationName = event.target.getAttribute('data-id');
ApplicationActions.refreshApplicationToken(applicationName);
let notification = new GlobalNotificationModel('Token refreshed', 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app) {
return (
<Property
name={app.name}
label={app.name}>
<div className="row-same-height">
<div className="col-xs-6 col-xs-height col-middle">
{'Bearer ' + app.bearer_token.token}
</div>
<div className="col-xs-6 col-xs-height">
<button
className="pull-right btn btn-default btn-sm"
onClick={this.handleTokenRefresh}
data-id={app.name}>
REFRESH
</button>
</div>
</div>
</Property>);
}, this);
content = (
<div>
<Form>
{content}
<hr />
</Form>
</div>);
}
return (
<div>
<div>Username: {this.props.currentUser.username}</div>
<div>Email: {this.props.currentUser.email}</div>
</div>
<CollapsibleParagraph
title="API Integration"
show={true}
defaultExpanded={true}>
<Form
url={apiUrls.applications}
handleSuccess={this.handleCreateSuccess}>
<Property
name='name'
label='Application Name'>
<input
type="text"
placeholder="Enter the name of your app"
required/>
</Property>
<hr />
</Form>
<pre>
Usage: curl &lt;url&gt; -H 'Authorization: Bearer &lt;token&gt;'
</pre>
{content}
</CollapsibleParagraph>
);
}
});

View File

@ -3,6 +3,9 @@
import AppConstants from './application_constants';
let apiUrls = {
'applications': AppConstants.apiEndpoint + 'applications/',
'application_token_refresh': AppConstants.apiEndpoint + 'applications/refresh_token/',
'blob_digitalworks': AppConstants.apiEndpoint + 'blob/digitalworks/',
'edition': AppConstants.apiEndpoint + 'editions/${bitcoin_id}/',
'edition_delete': AppConstants.apiEndpoint + 'editions/${edition_id}/',
'edition_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/${edition_id}/',
@ -15,7 +18,7 @@ let apiUrls = {
'ownership_loans': AppConstants.apiEndpoint + 'ownership/loans/',
'ownership_loans_confirm': AppConstants.apiEndpoint + 'ownership/loans/confirm/',
'ownership_loans_deny': AppConstants.apiEndpoint + 'ownership/loans/deny/',
'ownership_shares_mail': AppConstants.apiEndpoint + 'ownership/shares/mail/',
'ownership_shares': AppConstants.apiEndpoint + 'ownership/shares/',
'ownership_transfers': AppConstants.apiEndpoint + 'ownership/transfers/',
'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/',
'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/',

View File

@ -0,0 +1,17 @@
'use strict';
import requests from '../utils/requests';
let ApplicationFetcher = {
/**
* Fetch the registered applications of a user from the API.
*/
fetch() {
return requests.get('applications');
},
refreshToken(applicationName) {
return requests.post('application_token_refresh', { body: {'name': applicationName}});
}
};
export default ApplicationFetcher;

View File

@ -19,7 +19,6 @@ let Route = Router.Route;
let Redirect = Router.Redirect;
let baseUrl = AppConstants.baseUrl;
let routes = (
<Route name="app" path={baseUrl} handler={AscribeApp}>
<Route name="signup" path="signup" handler={SignupContainer} />
@ -30,8 +29,8 @@ let routes = (
<Route name="register_piece" path="register_piece" handler={RegisterPiece} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Redirect from={baseUrl} to="pieces" />
<Redirect from={baseUrl + '/'} to="pieces" />
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />
</Route>
);

View File

@ -0,0 +1,18 @@
'use strict';
import alt from '../alt';
import ApplicationActions from '../actions/application_actions';
class ApplicationStore {
constructor() {
this.applications = {};
this.bindActions(ApplicationActions);
}
onUpdateApplications(applications) {
this.applications = applications;
}
}
export default alt.createStore(ApplicationStore, 'ApplicationStore');

1
node_modules/react-s3-fineuploader generated vendored

@ -1 +0,0 @@
Subproject commit e2890cf3d28d010abc04e0de3b218afe8c949596

View File

@ -13,8 +13,7 @@
"start": "node server.js"
},
"browser": {
"fineUploader": "./node_modules/react-s3-fineuploader/vendor/s3.fine-uploader.js",
"ReactS3FineUploader": "./node_modules/react-s3-fineuploader/react_s3_fine_uploader.js"
"fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js"
},
"browserify-shim": {
"fineUploader": "qq"
@ -60,7 +59,7 @@
"gulp-notify": "^2.2.0",
"gulp-sass": "^2.0.1",
"gulp-sourcemaps": "^1.5.2",
"gulp-template": "^3.0.0",
"gulp-template": "~3.0.0",
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.4",
"harmonize": "^1.4.2",
@ -71,6 +70,7 @@
"react": "^0.13.2",
"react-bootstrap": "~0.22.6",
"react-datepicker": "~0.8.0",
"react-progressbar": "^1.1.0",
"react-router": "^0.13.3",
"react-textarea-autosize": "^2.2.3",
"reactify": "^1.1.0",

View File

@ -1,6 +1,3 @@
.ascribe-edition-details hr {
border-top: 1px solid #616161;
}
.ascribe-edition-personal-note > textarea {
margin-bottom: 1em;
@ -12,16 +9,23 @@
}
.ascribe-edition-collapsible-wrapper > div:first-child {
width:100%;
cursor:pointer;
}
width: 100%;
cursor: pointer;
background-color: #EEE;
padding: 10px;
border: 1px solid #CCC;
}
.ascribe-edition-collapsible-wrapper > div > span {
font-size:1.3em;
margin-right: .5em;
font-size:1.3em;
margin-right: .5em;
}
.ascribe-edition-collapsible-wrapper > div > span:nth-child(2) {
font-size: 0.9em;
}
.ascribe-edition-collapible-content {
width:100%;
margin-top: 1em;
width:100%;
margin-top: 1em;
}

View File

@ -1,7 +1,13 @@
$break-small: 764px;
$break-medium: 991px;
.ascribe-row {
max-width: 600px;
@media screen and (max-width: $break-medium) {
max-width: 600px;
}
@media screen and (min-width: $break-medium) {
max-width: 1200px;
}
margin: 0 auto
}

View File

@ -1,3 +1,4 @@
.ascribe-settings-wrapper {
width: 100%;
text-align: center;
@ -10,11 +11,18 @@
&div:last-of-type {
border-bottom: 1px solid rgba(0,0,0,.2);
}
&:hover{
border-left: 3px solid rgba(2, 182, 163, 0.4);
}
}
.is-hidden{
display: none;
}
.is-focused {
background-color: rgba(2, 182, 163, 0.05);
border-left: 3px solid rgba(2, 182, 163, 1);
border-left: 3px solid rgba(2, 182, 163, 1)!important;
}
.is-error {
@ -26,13 +34,14 @@
font-size: 0.9em;
margin-right: 1em;
}
span {
}
input {
input, textarea {
color: #666;
}
}
&:hover{
border-left: 3px solid rgba(169, 68, 66, 1);
}
}
.is-fixed {
@ -42,13 +51,15 @@
span {
cursor: default;
}
input {
input, div, pre, textarea {
cursor: default;
color: #666;
}
}
}
.ascribe-settings-property {
display: inline-block;
width: 100%;
@ -60,15 +71,28 @@
cursor:pointer;
span {
input, div, span, pre, textarea, select {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
span {
font-weight: normal;
font-size: 0.9em;
color: rgba(0,0,0,.7);
}
input {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
div {
margin-top: 10px;
div {
padding-left: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: normal;
font-size: 1.1em;
cursor: default;
color: rgba(0, 0, 0, .7);
}
}
input, pre, textarea, select {
font-weight: 400;
font-size: 1.1em;
width:100%;
@ -76,10 +100,12 @@
border: 0;
background-color: rgba(0,0,0,0);
color: #38BAAD;
padding-left: 0;
&:focus {
border:0;
outline:0;
box-shadow: none;
}
&::selection {
@ -89,4 +115,9 @@
}
textarea{
color: #666;
margin-top: 1em;
padding: 0;
}
}

View File

@ -10,12 +10,14 @@
.ascribe-pre{
word-break: break-word;
/* white-space: pre-wrap; */
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
/* word-wrap: break-word; */
font-family: inherit;
text-align: justify;
background-color: white;
border: none;
padding: 0;
margin:0;
}

View File

@ -0,0 +1,64 @@
.file-drag-and-drop {
display: table-cell;
outline: 1px dashed #616161;
cursor: pointer;
vertical-align: middle;
text-align: center;
height:208px;
width: 672px;
background-color: #FAFAFA;
transition: .1s linear background-color;
}
.file-drag-and-drop:hover {
background-color: rgba(72, 218, 203, 0.2);
}
.file-drag-and-drop > span {
font-size: 1.5em;
}
.has-files {
text-align: left;
padding: 3em 0 0 0;
}
.file-drag-and-drop-position {
display: inline-block;
margin: 0 0 3em 3em;
float:left;
}
.file-drag-and-drop-preview-table-wrapper {
display: table;
height:94px;
width:104px;
}
.file-drag-and-drop-preview {
overflow:hidden;
cursor: default;
background-color: #EEEEEE;
border: 1px solid #616161;
}
.file-drag-and-drop-preview-image {
display: table;
height:104px;
width:104px;
overflow:hidden;
border: 1px solid #616161;
}
.file-drag-and-drop-preview-other {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.file-drag-and-drop-preview-other span {
font-size: 1.1em;
display: block;
margin-top: -10px;
}

View File

@ -17,6 +17,7 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'ascribe_edition';
@import 'ascribe_textarea';
@import 'ascribe_media_player';
@import 'ascribe_uploader';
@import 'ascribe-global-notification';
@import 'ascribe_piece_register';
@import 'offset_right';