diff options
Diffstat (limited to 'animism-align/frontend/app/views')
4 files changed, 438 insertions, 0 deletions
diff --git a/animism-align/frontend/app/views/editor/captions/captions.container.js b/animism-align/frontend/app/views/editor/captions/captions.container.js new file mode 100644 index 0000000..41cb2aa --- /dev/null +++ b/animism-align/frontend/app/views/editor/captions/captions.container.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import './captions.css' + +import { MEDIA_ANNOTATION_TYPES } from 'app/constants' +import CaptionForm from './components/caption.form' + +class CaptionsContainer extends Component { + render() { + const { annotation, media, episode } = this.props + const { order, lookup: annotationLookup } = annotation + const { lookup: mediaLookup } = media + + const mediaItems = order.map(id => annotationLookup[id]) + .filter(annotation => ( + MEDIA_ANNOTATION_TYPES.has(annotation.type) + && !annotation.settings.hideCitation + && annotation.settings.media_id in mediaLookup + )) + .map(annotation => annotation.settings.media_id) + .reduce((dedupe, media_id) => { + if (dedupe.indexOf(media_id) === -1) { + dedupe.push(media_id) + } + return dedupe + }, []) + .map(media_id => mediaLookup[media_id]) + + // console.log(mediaItems) + + return ( + <div className='overview'> + <div className='project-top'> + <div className='project-heading'> + <h2>Captions</h2> + </div> + {mediaItems.map((item, index) => ( + <CaptionForm + key={item.id} + episode={episode} + media={item} + index={index+1} + /> + ))} + </div> + </div> + ) + } +} + +const mapStateToProps = state => ({ + project: state.site.project, + episode: state.site.episode, + annotation: state.annotation.index, + media: state.media.index, +}) + +export default connect(mapStateToProps)(CaptionsContainer) diff --git a/animism-align/frontend/app/views/editor/captions/captions.css b/animism-align/frontend/app/views/editor/captions/captions.css new file mode 100644 index 0000000..23db204 --- /dev/null +++ b/animism-align/frontend/app/views/editor/captions/captions.css @@ -0,0 +1,75 @@ +/* caption entries */ + +.caption-entry { + display: flex; + justify-content: flex-start; + align-items: flex-start; + background: #111; + padding: 0.5rem; + margin-bottom: 0.5rem; + max-width: 800px; + cursor: pointer; + transition: background 0.1s; +} +.caption-entry:hover { + background: #333; +} +.caption-index { + width: 120px; + text-align: right; + margin: 0; + padding: 0 1rem 0 0; +} +.caption-text { + flex: 1; +} +.caption-entry.generated .caption-index, +.caption-entry.generated .caption-text { + color: #888; + font-style: italic; +} + +/* caption form */ + +.caption-form .textarea span { + text-align: right; + padding-right: 1.25rem; +} +.caption-form .textarea textarea { + width: 670px; + height: 70px; +} +.caption-form .buttons { + display: flex; + align-items: center; + margin-bottom: 0.5rem; +} +.caption-form .buttons span { + display: block; + text-align: right; + padding-right: 1.25rem; + width: 128px; +} +.caption-form .buttons button { + margin-right: 0.5rem; +} + +/* gallery captions */ + +.gallery-captions { + margin-bottom: 1rem; +} +.gallery-captions.expanded .caption-entry { + background: #222; +} +.gallery-captions .button-spacer { + display: block; + width: 103px; +} +.gallery-captions.expanded button.expander { + margin-bottom: 1rem; +} +.gallery-captions button.expander { + font-size: 0.75rem; + padding: 0.5rem; +}
\ No newline at end of file diff --git a/animism-align/frontend/app/views/editor/captions/components/caption.form.js b/animism-align/frontend/app/views/editor/captions/components/caption.form.js new file mode 100644 index 0000000..3e38ea8 --- /dev/null +++ b/animism-align/frontend/app/views/editor/captions/components/caption.form.js @@ -0,0 +1,189 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' + +import { TextArea, Button } from 'app/common' +import GalleryCaptionForm from './galleryCaption.form' +import actions from 'app/actions' + +const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("") + +export default class CaptionForm extends Component { + state = { + editing: false, + expanded: false, + media: { settings: {} }, + } + + constructor(props) { + super(props) + this.edit = this.edit.bind(this) + this.expand = this.expand.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.handleCancel = this.handleCancel.bind(this) + this.handleUpdateGalleryCaption = this.handleUpdateGalleryCaption.bind(this) + } + + componentDidMount() { + this.setState({ + media: { + ...this.props.media, + settings: { + ...this.props.media.settings, + } + } + }) + } + + edit() { + this.setState({ editing: true }) + } + expand() { + this.setState({ expanded: !this.state.expanded }) + } + + handleChange(e){ + e.preventDefault() + this.setState({ + media: { + ...this.state.media, + settings: { + ...this.state.media, + bibliography: e.target.value, + } + } + }) + } + + handleSubmit(e) { + e.preventDefault() + actions.media.update(this.state.media) + this.setState({ editing: false }) + } + + handleUpdateGalleryCaption(caption_id, item) { + const media = { + ...this.state.media, + settings: { + ...this.state.media.settings, + caption_lookup: { + ...this.state.media.settings.caption_lookup, + [caption_id]: { ...item } + } + } + } + // console.log(media) + actions.media.update(media) + this.setState({ media }) + } + + handleCancel(e) { + e.preventDefault() + this.setState({ + editing: false, + media: { ...this.props.media } + }) + } + + render() { + return this.state.editing + ? this.renderForm() + : this.renderEntry() + } + + renderForm() { + const { episode, index } = this.props + const { media } = this.state + return ( + <form className='caption-form' onSubmit={this.handleSubmit}> + <TextArea + title={`Edit caption ${media.id}`} + name="bibliography" + placeholder={( + media.settings.bibliography + ? "Enter caption" + : "This caption was automatically generated and is not explicitly set" + )} + data={media.settings} + onChange={this.handleChange} + /> + <div className='buttons'> + <span> + <Link to={`/editor/${episode.id}/media/${media.id}/edit/`}>Edit media</Link> + </span> + <div> + <button onClick={this.handleSubmit}>Save caption</button> + <button onClick={this.handleCancel}>Cancel</button> + </div> + </div> + </form> + ) + } + + renderEntry() { + const { index } = this.props + const { media } = this.state + const { bibliography } = media.settings + return ( + <div> + <div className={media.settings.bibliography ? 'caption-entry' : 'caption-entry generated'}onClick={this.edit}> + <div className='caption-index'> + {index}{'. '} + </div> + {media.settings.bibliography + ? <div className='caption-text' dangerouslySetInnerHTML={{ __html: media.settings.bibliography }} /> + : this.renderAutomaticCaption() + } + </div> + {media.type === 'gallery' && this.renderGalleryEntries()} + </div> + ) + } + + renderAutomaticCaption() { + const { media } = this.state + return ( + <div className='caption-text'> + {media.author} + {', '} + {media.pre_title && media.pre_title} + {media.title} + {media.post_title && media.post_title} + {'. '} + {media.date && ( + ' ' + media.date + '.' + )} + {media.medium && ( + ' ' + media.medium + '.' + )} + {media.source && ( + ' ' + media.source.trim() + )} + {' (Generated)'} + </div> + ) + } + + renderGalleryEntries() { + const { index } = this.props + const { media, expanded } = this.state + const { image_order, caption_lookup } = media.settings + return ( + <div className={expanded ? "gallery-captions expanded" : "gallery-captions"}> + <div className="row"> + <div className='button-spacer'></div> + <button className="expander" onClick={this.expand}>{expanded ? "▼ Hide gallery entries" : "► Show gallery entries"}</button> + </div> + {expanded && image_order.map((caption_id, caption_index) => ( + <GalleryCaptionForm + key={caption_index} + caption_id={caption_id} + item={caption_lookup[caption_id]} + index={index + ALPHABET[caption_index]} + onUpdate={this.handleUpdateGalleryCaption} + /> + ))} + </div> + ) + } +} diff --git a/animism-align/frontend/app/views/editor/captions/components/galleryCaption.form.js b/animism-align/frontend/app/views/editor/captions/components/galleryCaption.form.js new file mode 100644 index 0000000..ff07052 --- /dev/null +++ b/animism-align/frontend/app/views/editor/captions/components/galleryCaption.form.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' + +import { TextArea, Button } from 'app/common' +import actions from 'app/actions' + +export default class GalleryCaptionForm extends Component { + state = { + editing: false, + item: {}, + } + + constructor(props) { + super(props) + this.edit = this.edit.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.handleCancel = this.handleCancel.bind(this) + } + + componentDidMount() { + this.setState({ + item: { + ...this.props.item, + } + }) + } + + edit() { + this.setState({ editing: true }) + } + + handleChange(e){ + e.preventDefault() + this.setState({ + item: { + ...this.state.item, + caption: e.target.value, + } + }) + } + + handleSubmit(e) { + e.preventDefault() + this.props.onUpdate(this.props.caption_id, this.state.item) + this.setState({ editing: false }) + } + + handleCancel(e) { + e.preventDefault() + this.setState({ + editing: false, + item: { ...this.props.item } + }) + } + + render() { + return this.state.editing + ? this.renderForm() + : this.renderEntry() + } + + renderForm() { + const { episode, index } = this.props + const { item } = this.state + return ( + <form className='caption-form' onSubmit={this.handleSubmit}> + <TextArea + title={`Edit caption`} + name="caption" + placeholder="Enter caption" + data={item} + onChange={this.handleChange} + /> + <div className='buttons'> + <span /> + <div> + <button onClick={this.handleSubmit}>Save caption</button> + <button onClick={this.handleCancel}>Cancel</button> + </div> + </div> + </form> + ) + } + + renderEntry() { + const { index } = this.props + const { item } = this.state + return ( + <div> + <div className={item.caption ? 'caption-entry' : 'caption-entry generated'} onClick={this.edit}> + <div className='caption-index'> + {index}{'. '} + </div> + {item.caption + ? <div className='caption-text' dangerouslySetInnerHTML={{ __html: item.caption }} /> + : this.renderAutomaticCaption() + } + </div> + </div> + ) + } + + renderAutomaticCaption() { + const { item } = this.state + return ( + <div className='caption-text'> + {item.author ? item.author + '. ' : ''} + {item.title ? item.title + '. ' : ''} + {item.date} + {' (Generated)'} + </div> + ) + } +} |
