summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2021-03-17 18:11:26 +0100
committerJules Laplace <julescarbon@gmail.com>2021-03-17 18:11:26 +0100
commitd165a0727e42349d935ab3ee287242f1e5029742 (patch)
treeb4fa68209127efdd4eb46c82eaef280535692611 /frontend
parent92566ba17f5e921d5bff1f3fb4e4b0d92ca4fd39 (diff)
frontend. export/view button. interactivity sanity check
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/common/app.css14
-rw-r--r--frontend/app/utils/index.js3
-rw-r--r--frontend/app/views/audio/components/audio.select.js11
-rw-r--r--frontend/app/views/graph/components/page.form.js10
-rw-r--r--frontend/app/views/graph/graph.actions.js11
-rw-r--r--frontend/app/views/graph/graph.reducer.js14
-rw-r--r--frontend/app/views/page/components/page.header.js18
-rw-r--r--frontend/app/views/page/components/tile.form.js41
-rw-r--r--frontend/app/views/page/components/tile.handle.js1
-rw-r--r--frontend/site/audio/audio.player.js43
-rw-r--r--frontend/site/index.js2
-rw-r--r--frontend/site/site.css20
-rw-r--r--frontend/site/site/site.actions.js4
-rw-r--r--frontend/site/site/site.reducer.js7
-rw-r--r--frontend/site/store.js3
-rw-r--r--frontend/site/types.js4
-rw-r--r--frontend/site/viewer/viewer.container.js60
17 files changed, 239 insertions, 27 deletions
diff --git a/frontend/app/common/app.css b/frontend/app/common/app.css
index 2e9dc4e..486e5fa 100644
--- a/frontend/app/common/app.css
+++ b/frontend/app/common/app.css
@@ -116,6 +116,20 @@ header > div > button:hover {
border-color: #fff;
color: #fff;
}
+
+header .building {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ margin-left: 1rem;
+ color: #888;
+}
+header .building .loader {
+ transform: scale(0.75);
+ margin-right: 0.5rem;
+}
+
header .vcat-btn {
font-size: 0.875rem;
padding-left: 0.5rem;
diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js
index 1114d65..6e5a909 100644
--- a/frontend/app/utils/index.js
+++ b/frontend/app/utils/index.js
@@ -50,7 +50,8 @@ export const pad = (n, m) => {
}
export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's')
-
+export const capitalize = s => s.split(' ').map(capitalizeWord).join(' ')
+export const capitalizeWord = s => s.substr(0, 1).toUpperCase() + s.substr(1)
export const padSeconds = n => n < 10 ? '0' + n : n
export const timestamp = (n = 0, fps = 25) => {
diff --git a/frontend/app/views/audio/components/audio.select.js b/frontend/app/views/audio/components/audio.select.js
index 73142f0..cf1dfb2 100644
--- a/frontend/app/views/audio/components/audio.select.js
+++ b/frontend/app/views/audio/components/audio.select.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Select } from 'app/common'
+import { unslugify } from 'app/utils'
const NO_AUDIO = 0
const AUDIO_TOP_OPTIONS = [
@@ -16,14 +17,14 @@ class AudioSelect extends Component {
constructor(props) {
super(props)
- this.handleSelect = this.handleSelect.bind(this)
+ this.handleChange = this.handleChange.bind(this)
}
componentDidMount(){
const { uploads } = this.props.graph.show.res
const audioUploads = uploads
.filter(upload => upload.tag === 'audio')
- .map(page => ({ name: upload.id, label: page.path }))
+ .map(upload => ({ name: upload.id, label: unslugify(upload.fn) }))
let audioList = [
...AUDIO_TOP_OPTIONS,
...audioUploads,
@@ -33,6 +34,10 @@ class AudioSelect extends Component {
})
}
+ handleChange(name, value) {
+ this.props.onChange(name, parseInt(value))
+ }
+
render() {
return (
<Select
@@ -40,7 +45,7 @@ class AudioSelect extends Component {
name={this.props.name}
selected={this.props.selected || NO_AUDIO}
options={this.state.audioList}
- onChange={this.props.onChange}
+ onChange={this.handleChange}
/>
)
}
diff --git a/frontend/app/views/graph/components/page.form.js b/frontend/app/views/graph/components/page.form.js
index 91a40a6..8148864 100644
--- a/frontend/app/views/graph/components/page.form.js
+++ b/frontend/app/views/graph/components/page.form.js
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'
import { session } from 'app/session'
import { TextInput, ColorInput, Checkbox, LabelDescription, TextArea, SubmitButton, Loader } from 'app/common'
-import { AudioSelect } from 'app/views/audio/components/audio.select'
+import AudioSelect from 'app/views/audio/components/audio.select'
const newPage = (data) => ({
path: '',
@@ -15,8 +15,8 @@ const newPage = (data) => ({
x: 0.05,
y: 0.05,
background_color: '#000000',
- audio: "",
- restartAudio: false,
+ background_audio_id: 0,
+ restart_audio: false,
},
...data,
})
@@ -186,8 +186,8 @@ export default class PageForm extends Component {
<Checkbox
label="Restart audio on load"
- name="restartAudio"
- checked={data.settings.restartAudio}
+ name="restart_audio"
+ checked={data.settings.restart_audio}
onChange={this.handleSettingsSelect}
autoComplete="off"
/>
diff --git a/frontend/app/views/graph/graph.actions.js b/frontend/app/views/graph/graph.actions.js
index eba3f92..4185386 100644
--- a/frontend/app/views/graph/graph.actions.js
+++ b/frontend/app/views/graph/graph.actions.js
@@ -1,4 +1,5 @@
import * as types from 'app/types'
+import { api } from 'app/utils'
import actions from 'app/actions'
export const showAddPageForm = () => dispatch => {
@@ -38,4 +39,12 @@ export const setHomePageId = (graph, page) => dispatch => {
delete updated_graph.pages
updated_graph.home_page_id = page.id
actions.graph.update(updated_graph)
-} \ No newline at end of file
+}
+
+export const viewPage = (graph, page) => dispatch => {
+ api(dispatch, types.api, 'export', `/api/v1/graph/export/${graph.path}`)
+ .then(result => {
+ console.log(result)
+ window.open(`${process.env.EXPORT_HOST}/${graph.path}/${page.path}`)
+ })
+}
diff --git a/frontend/app/views/graph/graph.reducer.js b/frontend/app/views/graph/graph.reducer.js
index 30049b5..725c256 100644
--- a/frontend/app/views/graph/graph.reducer.js
+++ b/frontend/app/views/graph/graph.reducer.js
@@ -8,6 +8,7 @@ const initialState = crudState('graph', {
addingPage: false,
editingPage: false,
showingAudio: false,
+ building: false,
},
options: {
}
@@ -125,6 +126,19 @@ export default function graphReducer(state = initialState, action) {
}
}
+ case types.api.loading:
+ if (action.tag !== 'view' && action.tag !== 'export') {
+ return state
+ }
+ return { ...state, editor: { ...state.editor, building: action.tag } }
+
+ case types.api.loaded:
+ case types.api.error:
+ if (action.tag !== 'view' && action.tag !== 'export') {
+ return state
+ }
+ return { ...state, editor: { ...state.editor, building: null } }
+
default:
return state
}
diff --git a/frontend/app/views/page/components/page.header.js b/frontend/app/views/page/components/page.header.js
index 998572a..dbdf1b6 100644
--- a/frontend/app/views/page/components/page.header.js
+++ b/frontend/app/views/page/components/page.header.js
@@ -3,6 +3,9 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
+import { Loader } from 'app/common'
+import { capitalize } from 'app/utils'
+
import * as graphActions from '../../graph/graph.actions'
import * as pageActions from '../page.actions'
@@ -10,13 +13,22 @@ function PageHeader(props) {
return (
<header>
<div>
- <Link to={props.graph.show.res ? "/" + props.graph.show.res.path : "/"} className="logo arrow">{"◁"}</Link>
+ <Link to={props.graph ? "/" + props.graph.path : "/"} className="logo arrow">{"◁"}</Link>
<b>{props.site.siteTitle}</b>
+ {props.building && (
+ <div className='building'>
+ <div className='loader'>
+ <Loader />
+ </div>
+ {capitalize(props.building)}ing...
+ </div>
+ )}
</div>
<div>
<button onClick={() => props.pageActions.toggleAddTileForm()}>+ Add tile</button>
<button onClick={() => props.pageActions.toggleTileList()}>Sort tiles</button>
<button onClick={() => props.graphActions.toggleEditPageForm()}>Edit page</button>
+ <button onClick={() => props.graphActions.viewPage(props.graph, props.page)}>View page</button>
</div>
</header>
)
@@ -25,7 +37,9 @@ function PageHeader(props) {
const mapStateToProps = (state) => ({
// auth: state.auth,
site: state.site,
- graph: state.graph,
+ graph: state.graph.show.res,
+ page: state.page.show.res,
+ building: state.graph.editor.building,
// isAuthenticated: state.auth.isAuthenticated,
})
diff --git a/frontend/app/views/page/components/tile.form.js b/frontend/app/views/page/components/tile.form.js
index da72e27..d6272bc 100644
--- a/frontend/app/views/page/components/tile.form.js
+++ b/frontend/app/views/page/components/tile.form.js
@@ -10,7 +10,7 @@ import {
TextInput, NumberInput, ColorInput, Slider,
Select, LabelDescription, TextArea, Checkbox,
SubmitButton, Loader } from 'app/common'
-import { AudioSelect } from 'app/views/audio/components/audio.select'
+import AudioSelect from 'app/views/audio/components/audio.select'
import { preloadImage, preloadVideo } from 'app/utils'
import * as tileActions from '../../tile/tile.actions'
@@ -150,6 +150,10 @@ const newPosition = (data) => ({
opacity: 1,
units: false,
align: "center_center",
+ has_audio: false,
+ audio_on_click_id: 0,
+ audio_on_hover_id: 0,
+ wait_for_audio_on_click: false,
...data,
})
@@ -198,6 +202,7 @@ class TileForm extends Component {
...PAGE_LIST_TOP_OPTIONS,
...linkPages.map(page => ({ name: page.id, label: page.path }))
]
+ this.setState({ pageList })
if (isNew) {
const newTile = newImage({
id: "new",
@@ -430,8 +435,8 @@ class TileForm extends Component {
: ""}
{this.renderHyperlinkForm()}
- {this.renderAudioForm()}
{this.renderMiscForm()}
+ {this.renderAudioForm()}
<div className='row buttons'>
<SubmitButton
@@ -753,8 +758,40 @@ class TileForm extends Component {
}
renderAudioForm() {
+ const { temporaryTile } = this.props
return (
<div>
+ <Checkbox
+ label="Play audio"
+ name="has_audio"
+ checked={temporaryTile.settings.has_audio}
+ onChange={this.handleSettingsSelect}
+ />
+ {temporaryTile.settings.has_audio && (
+ <div >
+ <AudioSelect
+ title="On click"
+ name="audio_on_click_id"
+ selected={temporaryTile.settings.audio_on_click_id}
+ onChange={this.handleSettingsSelect}
+ />
+
+ <Checkbox
+ label="Navigate when audio finishes"
+ name="wait_for_audio_on_click"
+ checked={temporaryTile.settings.wait_for_audio_on_click}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+
+ <AudioSelect
+ title="On hover"
+ name="audio_on_hover_id"
+ selected={temporaryTile.settings.audio_on_hover_id}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ )}
</div>
)
}
diff --git a/frontend/app/views/page/components/tile.handle.js b/frontend/app/views/page/components/tile.handle.js
index f47c3cd..090069d 100644
--- a/frontend/app/views/page/components/tile.handle.js
+++ b/frontend/app/views/page/components/tile.handle.js
@@ -165,7 +165,6 @@ const generateTransform = (tile, box) => {
}
const generateVideoStyle = (tile, bounds) => {
- // console.log(bounds)
const style = {
pointerEvents: "none",
}
diff --git a/frontend/site/audio/audio.player.js b/frontend/site/audio/audio.player.js
new file mode 100644
index 0000000..9a2d783
--- /dev/null
+++ b/frontend/site/audio/audio.player.js
@@ -0,0 +1,43 @@
+export default class AudioPlayer {
+ players = {}
+
+ play({ item, restart, loop }) {
+ return new Promise((resolve, reject) => {
+ const { id, url } = item
+ if (id in players) {
+ if (restart) {
+ players[id].currentTime = 0
+ players[id].play()
+ return resolve()
+ }
+ return reject()
+ }
+ const player = document.createElement('audio')
+ const handleEnded = () => {
+ unbind()
+ resolve()
+ }
+ const handleError = (error) => {
+ console.error(error)
+ unbind()
+ reject(error)
+ }
+ const bind = () => {
+ player.addEventListener('ended', handleEnded)
+ player.addEventListener('error', handleError)
+ this.players[id] = player
+ }
+ const unbind = () => {
+ player.removeEventListener('ended', handleEnded)
+ player.removeEventListener('error', handleError)
+ delete this.players[id]
+ }
+ const start = () => {
+ player.src = url
+ player.play()
+ }
+ bind()
+ start()
+ })
+ }
+}
diff --git a/frontend/site/index.js b/frontend/site/index.js
index 36eeae8..337d362 100644
--- a/frontend/site/index.js
+++ b/frontend/site/index.js
@@ -2,6 +2,8 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
+import './site.css'
+
import App from 'site/app'
import { store, history } from 'site/store'
diff --git a/frontend/site/site.css b/frontend/site/site.css
new file mode 100644
index 0000000..0597514
--- /dev/null
+++ b/frontend/site/site.css
@@ -0,0 +1,20 @@
+.roadblock {
+ position: fixed;
+ top: 0; left: 0;
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+.roadblock div {
+ display: inline-block;
+ text-align: center;
+}
+.roadblock h2 {
+ font-style: italic;
+}
+.roadblock button {
+ padding: 0.5rem;
+}
diff --git a/frontend/site/site/site.actions.js b/frontend/site/site/site.actions.js
index 3547ec0..07814d6 100644
--- a/frontend/site/site/site.actions.js
+++ b/frontend/site/site/site.actions.js
@@ -9,3 +9,7 @@ export const setSiteTitle = title => dispatch => {
export const loadSite = (graph_name, path_name) => dispatch => (
api(dispatch, types.site, 'site', '/' + graph_name + '/index.json')
)
+
+export const interact = () => dispatch => {
+ dispatch({ type: types.site.interact })
+} \ No newline at end of file
diff --git a/frontend/site/site/site.reducer.js b/frontend/site/site/site.reducer.js
index 53fa555..e0b53fb 100644
--- a/frontend/site/site/site.reducer.js
+++ b/frontend/site/site/site.reducer.js
@@ -2,6 +2,7 @@ import * as types from 'site/types'
const initialState = {
siteTitle: 'swimmer',
+ interactive: false,
graph: {
loading: true,
}
@@ -22,6 +23,12 @@ export default function siteReducer(state = initialState, action) {
graph: action.data.graph,
}
+ case types.site.interact:
+ return {
+ ...state,
+ interactive: true,
+ }
+
case '@@router/LOCATION_CHANGE':
return {
...state,
diff --git a/frontend/site/store.js b/frontend/site/store.js
index 6511613..5cb1a1b 100644
--- a/frontend/site/store.js
+++ b/frontend/site/store.js
@@ -4,6 +4,7 @@ import { createBrowserHistory } from 'history'
import thunk from 'redux-thunk'
import siteReducer from 'site/site/site.reducer'
+import AudioPlayer from 'site/audio/audio.player'
const createRootReducer = history => (
combineReducers({
@@ -13,7 +14,7 @@ const createRootReducer = history => (
})
)
-const configureStore = (initialState = {}, history) => {
+const configureStore = (initialState = { audio: audioPlayer }, history) => {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
diff --git a/frontend/site/types.js b/frontend/site/types.js
index 23bed98..4ab897f 100644
--- a/frontend/site/types.js
+++ b/frontend/site/types.js
@@ -1,7 +1,7 @@
-import { with_type, crud_type } from 'app/api/crud.types'
+import { with_type } from 'app/api/crud.types'
export const site = with_type('site', [
- 'set_site_title', 'loading', 'loaded', 'error',
+ 'set_site_title', 'loading', 'loaded', 'error', 'interact'
])
export const system = with_type('system', [
diff --git a/frontend/site/viewer/viewer.container.js b/frontend/site/viewer/viewer.container.js
index 1b3d564..3f1c9c5 100644
--- a/frontend/site/viewer/viewer.container.js
+++ b/frontend/site/viewer/viewer.container.js
@@ -12,12 +12,17 @@ import 'app/views/page/page.css'
class ViewerContainer extends Component {
state = {
page: {},
+ bounds: { width: window.innerWidth, height: window.innerHeight },
+ roadblock: false,
}
constructor(props) {
super(props)
this.pageRef = React.createRef()
this.handleMouseDown = this.handleMouseDown.bind(this)
+ this.handleResize = this.handleResize.bind(this)
+ this.removeRoadblock = this.removeRoadblock.bind(this)
+ window.addEventListener('resize', this.handleResize)
}
componentDidUpdate(prevProps) {
@@ -27,20 +32,30 @@ class ViewerContainer extends Component {
}
}
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize)
+ actions.site.interact()
+ }
+
+ handleResize() {
+ this.setState({
+ bounds: {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ }
+ })
+ }
+
load() {
const { graph_name, page_name } = this.props.match.params
const page_path = ["", graph_name, page_name].join('/')
const { pages, home_page } = this.props.graph
- const page = pages[page_path]
- if (!page) {
- // console.log('-> home page')
- console.log(page_path)
- const { home_page } = this.props.graph
- this.setState({ page: pages[home_page] })
+ const page = pages[page_path] || pages[home_page]
+ if (!this.props.interactive && hasAutoplayVideo(page)) {
+ this.setState({ page, roadblock: true })
} else {
- // console.log(page)
- console.log(page_path)
- this.setState({ page })
+ this.setState({ page, roadblock: false })
+ actions.site.interact()
}
}
@@ -50,6 +65,9 @@ class ViewerContainer extends Component {
render() {
const { page } = this.state
+ if (this.state.roadblock) {
+ return this.renderRoadblock()
+ }
if (this.props.graph.loading || !page.id) {
return (
<div>
@@ -83,11 +101,35 @@ class ViewerContainer extends Component {
</div>
)
}
+
+ removeRoadblock() {
+ actions.site.interact()
+ this.setState({ roadblock: false })
+ }
+
+ renderRoadblock() {
+ const { title } = this.props.graph
+ return (
+ <div className='roadblock' onClick={this.removeRoadblock}>
+ <div>
+ <h2>{title}</h2>
+ <button>Enter</button>
+ </div>
+ </div>
+ )
+ }
+}
+
+const hasAutoplayVideo = page => {
+ return page.tiles.some(tile => {
+ return tile.type === 'video' && !tile.settings.muted
+ })
}
const mapStateToProps = state => ({
site: state.site,
graph: state.site.graph,
+ interactive: state.site.interactive,
})
const mapDispatchToProps = dispatch => ({