summaryrefslogtreecommitdiff
path: root/animism-align/frontend/app/views/paragraph/components
diff options
context:
space:
mode:
Diffstat (limited to 'animism-align/frontend/app/views/paragraph/components')
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraph.form.js87
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraph.list.js165
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraphTypes/index.js22
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.image.js17
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.text.js35
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.video.js19
6 files changed, 345 insertions, 0 deletions
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraph.form.js b/animism-align/frontend/app/views/paragraph/components/paragraph.form.js
new file mode 100644
index 0000000..751ec7f
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraph.form.js
@@ -0,0 +1,87 @@
+import React, { Component } from 'react'
+// import { Link } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import actions from 'app/actions'
+
+import { clamp, timestamp, capitalize } from 'app/utils'
+import { Select } from 'app/common'
+
+const PARAGRAPH_TYPES = [
+ 'paragraph', 'blockquote', 'hidden',
+].map(name => ({ name, label: capitalize(name.replace('_', ' ')) }))
+
+class ParagraphForm extends Component {
+ constructor(props){
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSelect = this.handleSelect.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+ componentDidMount() {
+ if (this.textareaRef && this.textareaRef.current) {
+ this.textareaRef.current.focus()
+ }
+ }
+ handleChange(e) {
+ const { name, value } = e.target
+ this.handleSelect(name, value)
+ }
+ handleSelect(name, value) {
+ const { onUpdate, paragraph } = this.props
+ onUpdate({
+ ...paragraph,
+ [name]: value,
+ })
+ }
+ handleSubmit() {
+ const { paragraph, onClose } = this.props
+ actions.paragraph.update(paragraph)
+ .then(response => {
+ console.log(response)
+ onClose()
+ })
+ }
+ render() {
+ const { paragraph, y } = this.props
+ return (
+ <div
+ className='paragraphForm'
+ style={{
+ top: y,
+ }}
+ >
+ {this.renderButtons()}
+ </div>
+ )
+ }
+ renderButtons() {
+ const { paragraph } = this.props
+ return (
+ <div className='row buttons'>
+ <div className='row'>
+ <Select
+ name='type'
+ selected={paragraph.type}
+ options={PARAGRAPH_TYPES}
+ defaultOption='text'
+ onChange={this.handleSelect}
+ />
+ <div className='ts'>{timestamp(paragraph.start_ts, 1, true)}</div>
+ </div>
+ <div>
+ <button onClick={this.handleSubmit}>Save</button>
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ParagraphForm)
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraph.list.js b/animism-align/frontend/app/views/paragraph/components/paragraph.list.js
new file mode 100644
index 0000000..1b8a0ac
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraph.list.js
@@ -0,0 +1,165 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { floatLT, floatLTE } from 'app/utils'
+import actions from 'app/actions'
+import ParagraphForm from '../components/paragraph.form'
+
+const MEDIA_TYPES = new Set([
+ 'image', 'gallery', 'vitrine',
+ 'video',
+])
+
+class ParagraphList extends Component {
+ state = {
+ paragraphs: [],
+ currentParagraph: -1,
+ currentAnnotation: -1,
+ }
+
+ componentDidMount() {
+ this.build()
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.paragraph !== prevProps.paragraph) {
+ this.build()
+ }
+ if (this.props.audio.play_ts === prevProps.audio.play_ts) return
+ this.setCurrentParagraph()
+ }
+
+ setCurrentParagraph() {
+ const { play_ts } = this.props.audio
+ const insideParagraph = this.state.paragraphs.some(paragraph => {
+ if (floatLTE(paragraph.start_ts, play_ts) && floatLT(play_ts, paragraph.end_ts)) {
+ this.setCurrentAnnotation(paragraph, play_ts)
+ return true
+ }
+ return false
+ })
+ if (!insideParagraph) {
+ this.setState({
+ currentParagraph: -1,
+ currentAnnotation: -1,
+ })
+ }
+ }
+
+ setCurrentAnnotation(paragraph, play_ts) {
+ const { id: currentParagraph, annotations } = paragraph
+ let currentAnnotation
+ let annotation
+ let i = 0
+ let len = annotations.length
+ for (let i = 0; i < len - 1; i++) {
+ if (floatLT(play_ts, annotations[i+1].start_ts)) {
+ currentAnnotation = annotations[i].id
+ break
+ }
+ }
+ if (!currentAnnotation) {
+ currentAnnotation = annotations[len-1].id
+ }
+ this.setState({ currentParagraph, currentAnnotation })
+ }
+
+ build() {
+ const { order: annotationOrder, lookup: annotationLookup } = this.props.annotation
+ const { lookup: paragraphLookup } = this.props.paragraph
+ let currentParagraph = {}
+ const paragraphs = []
+ // loop over the annotations in time order
+ annotationOrder.forEach((annotation_id, i) => {
+ const annotation = annotationLookup[annotation_id]
+ const paragraph = paragraphLookup[annotation.paragraph_id]
+ // if this annotation is media, insert it after the current paragraph
+ if (MEDIA_TYPES.has(annotation.type)) {
+ paragraphs.push({
+ id: ('index_' + i),
+ type: annotation.type,
+ start_ts: annotation.start_ts,
+ end_ts: 0,
+ annotations: [annotation],
+ })
+ return
+ }
+ // if this annotation is from a different paragraph, make a new paragraph
+ if (annotation.paragraph_id !== currentParagraph.id) {
+ const paragraph_type = getParagraphType(annotation, paragraph)
+ currentParagraph = {
+ id: annotation.paragraph_id || ('index_' + i),
+ type: paragraph_type,
+ start_ts: annotation.start_ts,
+ end_ts: 0,
+ annotations: [],
+ }
+ paragraphs.push(currentParagraph)
+ }
+ // if this annotation is a paragraph_end, set the end timestamp
+ if (annotation.type === 'paragraph_end') {
+ currentParagraph.end_ts = annotation.start_ts
+ }
+ // otherwise, just append this annotation to the paragraph
+ else {
+ currentParagraph.annotations.push(annotation)
+ }
+ })
+ for (let i = 0; i < (paragraphs.length - 1); i++) {
+ if (!paragraphs[i].end_ts) {
+ paragraphs[i].end_ts = paragraphs[i+1].start_ts - 0.1
+ }
+ }
+ this.setState({ paragraphs })
+ }
+
+ render() {
+ const {
+ media, paragraphElementLookup, selectedParagraph,
+ onAnnotationClick, onParagraphDoubleClick
+ } = this.props
+ const { paragraphs, currentParagraph, currentAnnotation } = this.state
+ return paragraphs.map(paragraph => {
+ if (selectedParagraph && selectedParagraph.id === paragraph.id) {
+ paragraph = selectedParagraph
+ }
+ if (paragraph.type in paragraphElementLookup) {
+ const ParagraphElement = paragraphElementLookup[paragraph.type]
+ return (
+ <ParagraphElement
+ key={paragraph.id}
+ paragraph={paragraph}
+ media={media}
+ currentParagraph={paragraph.id === currentParagraph}
+ currentAnnotation={paragraph.id === currentParagraph && currentAnnotation}
+ onAnnotationClick={onAnnotationClick}
+ onDoubleClick={onParagraphDoubleClick}
+ />
+ )
+ } else {
+ return <div key={paragraph.id}>{'(waiting to implement' + paragraph.type + ')'}</div>
+ }
+ })
+ }
+}
+
+const getParagraphType = (annotation, paragraph) => {
+ if (!paragraph) {
+ return annotation.type
+ }
+ return paragraph.type
+}
+
+const mapStateToProps = state => ({
+ paragraph: state.paragraph.index,
+ annotation: state.annotation.index,
+ audio: state.audio,
+ media: state.media.index,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ParagraphList)
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraphTypes/index.js b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/index.js
new file mode 100644
index 0000000..62b4a49
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/index.js
@@ -0,0 +1,22 @@
+import React from 'react'
+
+import {
+ Paragraph, ParagraphHeader
+} from './paragraphTypes.text'
+
+import {
+ MediaVideo
+} from './paragraphTypes.video'
+
+import {
+ MediaImage
+} from './paragraphTypes.image'
+
+export const paragraphElementLookup = {
+ paragraph: React.memo(Paragraph),
+ hidden: React.memo(Paragraph),
+ blockquote: React.memo(Paragraph),
+ header: React.memo(ParagraphHeader),
+ video: React.memo(MediaVideo),
+ image: React.memo(MediaImage),
+}
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.image.js b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.image.js
new file mode 100644
index 0000000..36c72e9
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.image.js
@@ -0,0 +1,17 @@
+import React, { Component } from 'react'
+
+export const MediaImage = ({ paragraph, media, currentParagraph, currentAnnotation, onAnnotationClick, onDoubleClick }) => {
+ if (!media.lookup) return <div />
+ const className = currentParagraph ? 'media image current' : 'media image'
+ const annotation = paragraph.annotations[0]
+ const item = media.lookup[annotation.settings.media_id]
+ if (!item) return <div>Media not found: {annotation.settings.media_id}</div>
+ return (
+ <div
+ className={className}
+ onDoubleClick={e => onDoubleClick(e, paragraph)}
+ >
+ <img src={item.settings.display.url} />
+ </div>
+ )
+}
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.text.js b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.text.js
new file mode 100644
index 0000000..c2ebcd7
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.text.js
@@ -0,0 +1,35 @@
+import React, { Component } from 'react'
+
+export const Paragraph = ({ paragraph, currentParagraph, currentAnnotation, onAnnotationClick, onDoubleClick }) => {
+ let className = paragraph.type
+ if (className !== 'paragraph') className += ' paragraph'
+ if (currentParagraph) className += ' current'
+ return (
+ <div
+ className={className}
+ onDoubleClick={e => onDoubleClick(e, paragraph)}
+ >
+ {paragraph.annotations.map(annotation => (
+ <span
+ key={annotation.id}
+ className={annotation.id === currentAnnotation ? 'current' : ''}
+ onClick={e => onAnnotationClick(e, paragraph, annotation)}
+ dangerouslySetInnerHTML={{ __html: ' ' + annotation.text + ' ' }}
+ />
+ ))}
+ </div>
+ )
+}
+
+export const ParagraphHeader = ({ paragraph, currentParagraph, currentAnnotation, onAnnotationClick, onDoubleClick }) => {
+ let className = currentParagraph ? 'header current' : 'header'
+ const text = paragraph.annotations.map(annotation => annotation.text).join(' ')
+ return (
+ <div
+ className={className}
+ onDoubleClick={e => onDoubleClick(e, paragraph)}
+ >
+ {text}
+ </div>
+ )
+}
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.video.js b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.video.js
new file mode 100644
index 0000000..423864b
--- /dev/null
+++ b/animism-align/frontend/app/views/paragraph/components/paragraphTypes/paragraphTypes.video.js
@@ -0,0 +1,19 @@
+import React, { Component } from 'react'
+
+import VimeoPlayer from '@u-wave/react-vimeo'
+
+export const MediaVideo = ({ paragraph, media, currentParagraph, currentAnnotation, onAnnotationClick, onDoubleClick }) => {
+ if (!media.lookup) return <div />
+ const className = currentParagraph ? 'media current' : 'media'
+ const annotation = paragraph.annotations[0]
+ const item = media.lookup[annotation.settings.media_id]
+ if (!item) return <div>Media not found: {annotation.settings.media_id}</div>
+ return (
+ <div
+ className={className}
+ onDoubleClick={e => onDoubleClick(e, paragraph)}
+ >
+ <VimeoPlayer video={item.url} muted width="650" />
+ </div>
+ )
+}