summaryrefslogtreecommitdiff
path: root/animism-align/frontend/app/views/editor/paragraph
diff options
context:
space:
mode:
Diffstat (limited to 'animism-align/frontend/app/views/editor/paragraph')
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraph.form.js107
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraph.list.js103
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/index.js26
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.image.js17
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.text.js55
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.video.js16
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/containers/paragraphEditor.container.js93
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/paragraph.container.js66
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/paragraph.css126
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/paragraph.reducer.js28
-rw-r--r--animism-align/frontend/app/views/editor/paragraph/transcript.actions.js10
11 files changed, 647 insertions, 0 deletions
diff --git a/animism-align/frontend/app/views/editor/paragraph/components/paragraph.form.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraph.form.js
new file mode 100644
index 0000000..55cb74e
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/components/paragraph.form.js
@@ -0,0 +1,107 @@
+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 { timestamp, capitalize } from 'app/utils'
+import { Select, Checkbox } from 'app/common'
+
+const PARAGRAPH_TYPES = [
+ 'paragraph', 'intro_paragraph', 'blockquote', 'pullquote', 'big_text', '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.handleSettingsSelect = this.handleSettingsSelect.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,
+ })
+ }
+ handleSettingsSelect(name, value) {
+ const { onUpdate, paragraph } = this.props
+ onUpdate({
+ ...paragraph,
+ settings: {
+ ...paragraph.settings,
+ [name]: value,
+ }
+ })
+ }
+ handleSubmit() {
+ const { paragraph, onClose } = this.props
+ actions.paragraph.update(paragraph)
+ .then(response => {
+ console.log(response)
+ onClose()
+ })
+ }
+ render() {
+ const { paragraph, y } = this.props
+ console.log(paragraph)
+ return (
+ <div
+ className='paragraphForm'
+ style={{
+ top: y,
+ }}
+ >
+ {this.renderButtons()}
+ <div>
+ <Checkbox
+ label="Hide in transcript"
+ name="hide_in_transcript"
+ checked={paragraph.settings ? paragraph.settings.hide_in_transcript : false}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ </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/editor/paragraph/components/paragraph.list.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraph.list.js
new file mode 100644
index 0000000..f7c9c58
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/components/paragraph.list.js
@@ -0,0 +1,103 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { floatLT, floatInRange, capitalize } from 'app/utils'
+import ParagraphForm from '../components/paragraph.form'
+
+class ParagraphList extends Component {
+ state = {
+ currentParagraph: -1,
+ currentAnnotation: -1,
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.audio.play_ts === prevProps.audio.play_ts) return
+ if (!this.props.paragraphs) return
+ this.setCurrentParagraph()
+ }
+
+ setCurrentParagraph() {
+ const { play_ts } = this.props.audio
+ const insideParagraph = this.props.paragraphs.some(paragraph => {
+ if (floatInRange(paragraph.start_ts, 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
+ const possibleAnnotations = annotations.filter(a => a.type === 'sentence')
+ // console.log(possibleAnnotations)
+ if (!possibleAnnotations.length) return
+ let currentAnnotation
+ let annotation
+ let i = 0, next_i
+ let len = possibleAnnotations.length
+ for (let i = 0; i < len - 1; i++) {
+ next_i = i + 1
+ if (next_i < len && floatLT(play_ts, possibleAnnotations[next_i].start_ts)) {
+ currentAnnotation = possibleAnnotations[i].id
+ break
+ }
+ }
+ if (!currentAnnotation) {
+ currentAnnotation = possibleAnnotations[len-1].id
+ }
+ this.setState({ currentParagraph, currentAnnotation })
+ }
+
+ render() {
+ const {
+ paragraphs, media,
+ paragraphElementLookup, selectedParagraph,
+ onAnnotationClick, onParagraphDoubleClick,
+ currentSection,
+ } = this.props
+ const { currentParagraph, currentAnnotation } = this.state
+ if (!paragraphs) return null
+ return paragraphs.map((paragraph, i) => {
+ if (selectedParagraph && selectedParagraph.id === paragraph.id) {
+ paragraph = selectedParagraph
+ }
+ if (paragraph.type in paragraphElementLookup) {
+ const ParagraphElement = paragraphElementLookup[paragraph.type]
+ return (
+ <ParagraphElement
+ key={paragraph.id + "_" + i}
+ paragraph={paragraph}
+ media={media}
+ currentSection={currentSection}
+ currentParagraph={paragraph.id === currentParagraph}
+ currentAnnotation={paragraph.id === currentParagraph && currentAnnotation}
+ onAnnotationClick={onAnnotationClick}
+ onDoubleClick={onParagraphDoubleClick}
+ />
+ )
+ // } else {
+ // return <div key={paragraph.id}>{'(' + capitalize(paragraph.type) + ')'}</div>
+ }
+ })
+ }
+}
+
+const mapStateToProps = state => ({
+ audio: state.audio,
+ media: state.media.index,
+ currentSection: state.viewer.currentSection,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ParagraphList)
diff --git a/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/index.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/index.js
new file mode 100644
index 0000000..5180d5e
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/index.js
@@ -0,0 +1,26 @@
+import React from 'react'
+
+import {
+ Paragraph, ParagraphHeading
+} from './paragraphTypes.text'
+
+import {
+ MediaVideo
+} from './paragraphTypes.video'
+
+import {
+ MediaImage
+} from './paragraphTypes.image'
+
+export const paragraphElementLookup = {
+ paragraph: React.memo(Paragraph),
+ intro_paragraph: React.memo(Paragraph),
+ hidden: React.memo(Paragraph),
+ blockquote: React.memo(Paragraph),
+ pullquote: React.memo(Paragraph),
+ big_text: React.memo(Paragraph),
+ section_heading: React.memo(ParagraphHeading),
+ // heading_text: React.memo(ParagraphHeading),
+ video: React.memo(MediaVideo),
+ image: React.memo(MediaImage),
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.image.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.image.js
new file mode 100644
index 0000000..4eab2cc
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/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)}
+ >
+ </div>
+ )
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.text.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.text.js
new file mode 100644
index 0000000..f0529c8
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.text.js
@@ -0,0 +1,55 @@
+import React, { Component } from 'react'
+
+import { ROMAN_NUMERALS } from 'app/constants'
+
+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 => {
+ if (annotation.type === 'footnote') {
+ return (
+ <span
+ key={annotation.id}
+ className='footnote'
+ onClick={e => onAnnotationClick(e, paragraph, annotation)}
+ dangerouslySetInnerHTML={{ __html: annotation.footnote_id }}
+ />
+ )
+ }
+ return (
+ <span
+ key={annotation.id}
+ className={
+ annotation.type === 'pullquote_credit'
+ ? 'pullquote_credit'
+ : annotation.id === currentAnnotation
+ ? 'current'
+ : ''
+ }
+ onClick={e => onAnnotationClick(e, paragraph, annotation)}
+ dangerouslySetInnerHTML={{ __html: annotation.text }}
+ />
+ )
+ })}
+ </div>
+ )
+}
+
+export const ParagraphHeading = ({ paragraph, currentParagraph, currentAnnotation, onAnnotationClick, onDoubleClick }) => {
+ let className = currentParagraph ? 'section_heading current' : 'section_heading'
+ const text = paragraph.annotations.map(annotation => annotation.text).join(' ')
+ return (
+ <div
+ className={className}
+ onDoubleClick={e => onDoubleClick(e, paragraph)}
+ >
+ <span>{ROMAN_NUMERALS[paragraph.sectionIndex]}{'. '}{text}</span>
+ </div>
+ )
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.video.js b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.video.js
new file mode 100644
index 0000000..ba7a996
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/components/paragraphTypes/paragraphTypes.video.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+
+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)}
+ >
+ </div>
+ )
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/containers/paragraphEditor.container.js b/animism-align/frontend/app/views/editor/paragraph/containers/paragraphEditor.container.js
new file mode 100644
index 0000000..09ba70c
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/containers/paragraphEditor.container.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import actions from 'app/actions'
+import ParagraphForm from '../components/paragraph.form'
+import ParagraphList from '../components/paragraph.list'
+import { paragraphElementLookup } from '../components/paragraphTypes'
+
+class ParagraphEditor extends Component {
+ state = {
+ selectedParagraph: null,
+ selectedParagraphOffset: 0,
+ }
+
+ constructor(props) {
+ super(props)
+ this.handleAnnotationClick = this.handleAnnotationClick.bind(this)
+ this.handleParagraphDoubleClick = this.handleParagraphDoubleClick.bind(this)
+ this.handleCloseParagraphForm = this.handleCloseParagraphForm.bind(this)
+ this.updateSelectedParagraph = this.updateSelectedParagraph.bind(this)
+ }
+
+ componentDidMount() {
+ actions.transcript.buildAllParagraphs()
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.paragraph !== prevProps.paragraph) {
+ actions.transcript.buildAllParagraphs()
+ }
+ }
+
+ handleAnnotationClick(e, paragraph, annotation){
+ actions.audio.seek(annotation.start_ts)
+ }
+
+ handleParagraphDoubleClick(e, paragraph) {
+ let paragraphNode = e.target
+ if (!paragraphNode.classList.contains('paragraph')) {
+ paragraphNode = paragraphNode.parentNode
+ }
+ this.setState({
+ selectedParagraph: { ...paragraph },
+ selectedParagraphOffset: paragraphNode.offsetTop
+ })
+ }
+
+ updateSelectedParagraph(selectedParagraph) {
+ this.setState({ selectedParagraph })
+ }
+
+ handleCloseParagraphForm() {
+ this.setState({ selectedParagraph: null })
+ }
+
+ render() {
+ const { paragraphs } = this.props
+ const { selectedParagraph, selectedParagraphOffset } = this.state
+ return (
+ <div className='paragraphs'>
+ <div className='content'>
+ <ParagraphList
+ paragraphs={paragraphs}
+ paragraphElementLookup={paragraphElementLookup}
+ selectedParagraph={selectedParagraph}
+ onAnnotationClick={this.handleAnnotationClick}
+ onParagraphDoubleClick={this.handleParagraphDoubleClick}
+ />
+ {selectedParagraph &&
+ <ParagraphForm
+ paragraph={selectedParagraph}
+ onUpdate={this.updateSelectedParagraph}
+ onClose={this.handleCloseParagraphForm}
+ y={selectedParagraphOffset}
+ />
+ }
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ paragraph: state.paragraph.index,
+ paragraphs: state.paragraph.paragraphs,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ParagraphEditor)
diff --git a/animism-align/frontend/app/views/editor/paragraph/paragraph.container.js b/animism-align/frontend/app/views/editor/paragraph/paragraph.container.js
new file mode 100644
index 0000000..d0e9b86
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/paragraph.container.js
@@ -0,0 +1,66 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import './paragraph.css'
+
+import actions from 'app/actions'
+import { Loader } from 'app/common'
+
+import ParagraphEditor from './containers/paragraphEditor.container'
+
+class ParagraphContainer extends Component {
+ constructor(props) {
+ super(props)
+ this.handleKeyDown = this.handleKeyDown.bind(this)
+ }
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeyDown)
+ }
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeyDown)
+ }
+ handleKeyDown(e) {
+ if (document.activeElement !== document.body) {
+ return
+ }
+ // console.log(e.keyCode)
+ switch (e.keyCode) {
+ case 32: // spacebar
+ e.preventDefault()
+ actions.audio.toggle()
+ break
+ case 37: // left
+ case 38: // up
+ e.preventDefault()
+ actions.audio.jump(-5.0)
+ break
+ case 39: // right
+ case 40: // down
+ e.preventDefault()
+ actions.audio.jump(5.0)
+ break
+ }
+ }
+ render() {
+ if (!this.props.loaded) {
+ return <div className='body loading'><Loader /></div>
+ }
+ return (
+ <div className='body'>
+ <ParagraphEditor />
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ loaded: !!state.annotation.index.lookup && !!state.paragraph.index.lookup && !!state.media.index.lookup
+})
+
+const mapDispatchToProps = dispatch => ({
+ // alignActions: bindActionCreators({ ...alignActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ParagraphContainer)
diff --git a/animism-align/frontend/app/views/editor/paragraph/paragraph.css b/animism-align/frontend/app/views/editor/paragraph/paragraph.css
new file mode 100644
index 0000000..5743dbb
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/paragraph.css
@@ -0,0 +1,126 @@
+.paragraphs {
+ width: 100%;
+ height: calc(100% - 3.125rem);
+ overflow: scroll;
+ background: white;
+ color: black;
+ padding: 1rem;
+}
+
+/* general paragraph styles */
+
+.paragraphs .content {
+ font-family: 'Georgia', serif;
+ width: 650px;
+ margin: 0 auto;
+ padding-bottom: 6rem;
+ position: relative;
+}
+
+.paragraphs .content > div {
+ margin-bottom: 16px;
+}
+
+/* paragraph subtypes */
+
+.paragraphs .section_heading {
+ font-size: 32px;
+}
+
+.paragraphs .heading_text {
+ font-size: 32px;
+}
+
+.paragraphs .intro_paragraph {
+ font-size: 18px;
+ line-height: 1.5;
+}
+
+.paragraphs .paragraph {
+ font-size: 16px;
+ line-height: 1.5;
+}
+.paragraphs .footnote {
+ font-size: 13px;
+ position: relative;
+ top: -8px;
+ left: -4px;
+ cursor: pointer;
+}
+
+.paragraphs .blockquote {
+ padding-left: 3rem;
+}
+
+.paragraphs .hidden {
+ opacity: 0.5;
+}
+
+.paragraphs .pullquote {
+ border-left: 2px solid #ddd;
+ padding-left: 2rem;
+ font-size: 20px;
+}
+
+.paragraphs .pullquote_credit {
+ display: block;
+ margin-top: 1rem;
+}
+.paragraphs .pullquote_credit:before {
+ content: '—';
+}
+
+
+/* media image */
+
+.paragraphs .media.image img {
+ width: 100%;
+}
+
+/* current paragraph */
+
+.paragraphs .paragraph.current {
+ background: rgba(0,0,0,0.0);
+}
+
+/* sentences */
+
+.paragraphs span {
+ margin-right: 4px;
+ cursor: pointer;
+}
+
+.paragraphs .paragraph .current {
+ box-shadow: -2px -3px 0 #fff,
+ 2px -3px 0 #fff,
+ -2px 3px 0 #fff,
+ 2px 3px 0 #fff;
+ box-decoration-break: clone;
+ background: black;
+ color: white;
+}
+
+/* paragraph form */
+
+.paragraphForm {
+ position: absolute;
+ right: -305px;
+ width: 300px;
+ padding: 0.5rem;
+ background: #838;
+ color: #ddd;
+ box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
+ font-family: 'Roboto', sans-serif;
+}
+.paragraphForm .select div {
+ color: #ddd;
+ font-family: 'Roboto', sans-serif;
+}
+.paragraphForm .row {
+ justify-content: space-between;
+ align-items: center;
+}
+.paragraphForm .row > div {
+ display: flex;
+ align-items: center;
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/paragraph.reducer.js b/animism-align/frontend/app/views/editor/paragraph/paragraph.reducer.js
new file mode 100644
index 0000000..3b33128
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/paragraph.reducer.js
@@ -0,0 +1,28 @@
+import * as types from 'app/types'
+// import { session, getDefault, getDefaultInt } from 'app/session'
+
+import { crudState, crudReducer } from 'app/api/crud.reducer'
+
+const initialState = crudState('paragraph', {
+ paragraphs: [],
+ footnotes: [],
+ options: {
+ }
+})
+
+const reducer = crudReducer('paragraph')
+
+export default function paragraphReducer(state = initialState, action) {
+ // console.log(action.type, action)
+ state = reducer(state, action)
+ switch (action.type) {
+ case types.paragraph.update_transcript:
+ return {
+ ...state,
+ paragraphs: action.paragraphs,
+ footnotes: action.footnotes,
+ }
+ default:
+ return state
+ }
+}
diff --git a/animism-align/frontend/app/views/editor/paragraph/transcript.actions.js b/animism-align/frontend/app/views/editor/paragraph/transcript.actions.js
new file mode 100644
index 0000000..336a79d
--- /dev/null
+++ b/animism-align/frontend/app/views/editor/paragraph/transcript.actions.js
@@ -0,0 +1,10 @@
+import * as types from 'app/types'
+import { store, history, dispatch } from 'app/store'
+import { buildParagraphs } from 'app/utils/transcript.utils'
+
+export const buildAllParagraphs = () => dispatch => {
+ const state = store.getState()
+ const annotationOrder = state.annotation.index.order
+ const { paragraphs, footnotes } = buildParagraphs(annotationOrder)
+ dispatch({ type: types.paragraph.update_transcript, paragraphs, footnotes })
+}