summaryrefslogtreecommitdiff
path: root/animism-align/frontend
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2021-03-12 19:34:30 +0100
committerJules Laplace <julescarbon@gmail.com>2021-03-12 19:34:30 +0100
commitcd84d1fbf26a9272b56fec30fa6e1c30bebe7e06 (patch)
tree0eff1a2153ab5402b1e45e78f685cf93d78f27b5 /animism-align/frontend
parentcb47d6a07b857ffc11e5baec07ec9d49439f14e4 (diff)
nicer automatic captions
Diffstat (limited to 'animism-align/frontend')
-rw-r--r--animism-align/frontend/app/views/editor/captions/captions.container.js59
-rw-r--r--animism-align/frontend/app/views/editor/captions/captions.css75
-rw-r--r--animism-align/frontend/app/views/editor/captions/components/caption.form.js189
-rw-r--r--animism-align/frontend/app/views/editor/captions/components/galleryCaption.form.js115
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>
+ )
+ }
+}