diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2021-03-17 18:11:26 +0100 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2021-03-17 18:11:26 +0100 |
| commit | d165a0727e42349d935ab3ee287242f1e5029742 (patch) | |
| tree | b4fa68209127efdd4eb46c82eaef280535692611 /frontend | |
| parent | 92566ba17f5e921d5bff1f3fb4e4b0d92ca4fd39 (diff) | |
frontend. export/view button. interactivity sanity check
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/app/common/app.css | 14 | ||||
| -rw-r--r-- | frontend/app/utils/index.js | 3 | ||||
| -rw-r--r-- | frontend/app/views/audio/components/audio.select.js | 11 | ||||
| -rw-r--r-- | frontend/app/views/graph/components/page.form.js | 10 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.actions.js | 11 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.reducer.js | 14 | ||||
| -rw-r--r-- | frontend/app/views/page/components/page.header.js | 18 | ||||
| -rw-r--r-- | frontend/app/views/page/components/tile.form.js | 41 | ||||
| -rw-r--r-- | frontend/app/views/page/components/tile.handle.js | 1 | ||||
| -rw-r--r-- | frontend/site/audio/audio.player.js | 43 | ||||
| -rw-r--r-- | frontend/site/index.js | 2 | ||||
| -rw-r--r-- | frontend/site/site.css | 20 | ||||
| -rw-r--r-- | frontend/site/site/site.actions.js | 4 | ||||
| -rw-r--r-- | frontend/site/site/site.reducer.js | 7 | ||||
| -rw-r--r-- | frontend/site/store.js | 3 | ||||
| -rw-r--r-- | frontend/site/types.js | 4 | ||||
| -rw-r--r-- | frontend/site/viewer/viewer.container.js | 60 |
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 => ({ |
