summaryrefslogtreecommitdiff
path: root/frontend/site
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/site')
-rw-r--r--frontend/site/actions.js19
-rw-r--r--frontend/site/app.js35
-rw-r--r--frontend/site/audio/audio.player.js141
-rw-r--r--frontend/site/audio/audio.reducer.js18
-rw-r--r--frontend/site/index.js19
-rw-r--r--frontend/site/site.css20
-rw-r--r--frontend/site/site/site.actions.js15
-rw-r--r--frontend/site/site/site.reducer.js43
-rw-r--r--frontend/site/store.js39
-rw-r--r--frontend/site/types.js11
-rw-r--r--frontend/site/viewer/viewer.container.js223
11 files changed, 583 insertions, 0 deletions
diff --git a/frontend/site/actions.js b/frontend/site/actions.js
new file mode 100644
index 0000000..dea882c
--- /dev/null
+++ b/frontend/site/actions.js
@@ -0,0 +1,19 @@
+import { bindActionCreators } from 'redux'
+// import { actions as crudActions } from './api'
+
+import * as siteActions from 'site/site/site.actions'
+
+import { store } from 'site/store'
+
+export default
+ // Object.keys(crudActions)
+ // .map(a => [a, crudActions[a]])
+ // .concat(
+ [
+ ['site', siteActions],
+ ] //)
+ .map(p => [p[0], bindActionCreators(p[1], store.dispatch)])
+ .concat([
+ // ['socket', socketActions],
+ ])
+ .reduce((a,b) => (a[b[0]] = b[1])&&a,{}) \ No newline at end of file
diff --git a/frontend/site/app.js b/frontend/site/app.js
new file mode 100644
index 0000000..098bd44
--- /dev/null
+++ b/frontend/site/app.js
@@ -0,0 +1,35 @@
+import React, { Component } from 'react'
+import { ConnectedRouter } from 'connected-react-router'
+import { Route } from 'react-router'
+
+import ViewerContainer from 'site/viewer/viewer.container'
+import actions from 'site/actions'
+
+export default class App extends Component {
+ componentDidMount() {
+ const path_partz = window.location.pathname.split('/')
+ const graph_name = path_partz[1]
+ // let path_name = null
+ // if (path_partz.length > 2) {
+ // path_name = path_partz[2]
+ // }
+ // console.log('loading', graph_name, path_name)
+ actions.site.loadSite(graph_name)
+ }
+
+ render() {
+ return (
+ <ConnectedRouter history={this.props.history}>
+ <div className='app'>
+ <Route path={'/:graph_name/:page_name'} component={ViewerContainer} exact />
+ <Route exact key='root' path='/' render={() => {
+ // setTimeout(() => this.props.history.push('/'), 10)
+ return null
+ }} />
+ </div>
+ </ConnectedRouter>
+ )
+ }
+}
+/*
+*/
diff --git a/frontend/site/audio/audio.player.js b/frontend/site/audio/audio.player.js
new file mode 100644
index 0000000..17edeee
--- /dev/null
+++ b/frontend/site/audio/audio.player.js
@@ -0,0 +1,141 @@
+import { history } from 'site/store'
+
+export default class AudioPlayer {
+ files = {}
+ players = {}
+ current_background_id = 0
+
+ constructor() {
+ this.done = this.done.bind(this)
+ }
+
+ load(graph) {
+ this.files = graph.uploads
+ .filter(upload => upload.tag === 'audio')
+ .reduce((accumulator, item) => {
+ accumulator[item.id] = item
+ return accumulator
+ }, {})
+ }
+
+ has(id) {
+ return (
+ (id > 0) &&
+ (id in this.files)
+ )
+ }
+
+ done(id) {
+ // console.log('remove', id)
+ delete this.players[id]
+ }
+
+ playPage(page) {
+ const { background_audio_id, restart_audio } = page.settings
+ // console.log('playPage', page)
+ if (
+ this.current_background_id
+ && this.current_background_id !== background_audio_id
+ && this.current_background_id in this.players
+ ) {
+ this.players[this.current_background_id].stop()
+ }
+ if (this.has(background_audio_id)) {
+ this.current_background_id = background_audio_id
+ this.playFile({
+ id: background_audio_id,
+ type: 'background',
+ restart: !!restart_audio,
+ })
+ }
+ }
+
+ playTile({ tile, type }) {
+ let id = type === 'click'
+ ? tile.settings.audio_on_click_id
+ : type === 'hover'
+ ? tile.settings.audio_on_hover_id
+ : null
+ if (this.has(id)) {
+ this.playFile({ id, tile, type })
+ }
+ }
+
+ playFile({ id, tile, type, restart, loop }) {
+ const item = this.files[id]
+ if (id in this.players) {
+ if (restart) {
+ this.players[id].restart()
+ }
+ if (tile && !this.players[id].tile) {
+ this.players[id].tile = tile
+ this.players[id].type = type
+ }
+ return this.players[id]
+ } else {
+ this.players[id] = new Player({
+ item,
+ tile,
+ type,
+ done: this.done
+ })
+ this.players[id].play()
+ return this.players[id]
+ }
+ }
+}
+
+class Player {
+ constructor({ item, tile, type, done }) {
+ this.item = item
+ this.tile = tile
+ this.type = type
+ this.done = done
+ this.audio = document.createElement('audio')
+ this.handleEnded = this.handleEnded.bind(this)
+ this.handleError = this.handleError.bind(this)
+ this.release = this.release.bind(this)
+ this.audio.addEventListener('ended', this.handleEnded)
+ this.audio.addEventListener('error', this.handleError)
+ this.audio.src = item.url
+ }
+
+ release() {
+ if (this.type === 'click' && this.tile && this.tile.settings.navigate_when_audio_finishes) {
+ history.push(this.tile.href)
+ }
+ this.audio.removeEventListener('ended', this.handleEnded)
+ this.audio.removeEventListener('error', this.handleError)
+ this.done(this.item.id)
+ this.item = null
+ this.done = null
+ this.audio = null
+ }
+
+ handleError(error) {
+ console.error(error)
+ this.release()
+ }
+
+ handleEnded() {
+ if (this.type === 'background') {
+ this.restart()
+ } else {
+ this.release()
+ }
+ }
+
+ play() {
+ this.audio.play()
+ }
+
+ restart() {
+ this.audio.currentTime = 0
+ this.audio.play()
+ }
+
+ stop() {
+ this.audio.pause()
+ this.release()
+ }
+}
diff --git a/frontend/site/audio/audio.reducer.js b/frontend/site/audio/audio.reducer.js
new file mode 100644
index 0000000..f0bf0e9
--- /dev/null
+++ b/frontend/site/audio/audio.reducer.js
@@ -0,0 +1,18 @@
+import AudioPlayer from 'site/audio/audio.player'
+import * as types from 'site/types'
+
+const initialState = {
+ player: new AudioPlayer(),
+}
+
+export default function audioReducer(state = initialState, action) {
+ // console.log(action.type, action)
+ switch (action.type) {
+ case types.site.loaded:
+ state.player.load(action.data.graph)
+ return state
+
+ default:
+ return state
+ }
+}
diff --git a/frontend/site/index.js b/frontend/site/index.js
new file mode 100644
index 0000000..337d362
--- /dev/null
+++ b/frontend/site/index.js
@@ -0,0 +1,19 @@
+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'
+
+const container = document.createElement('div')
+container.classList.add('container')
+document.body.appendChild(container)
+
+ReactDOM.render(
+ <Provider store={store}>
+ <App history={history} />
+ </Provider>, container
+)
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
new file mode 100644
index 0000000..aab68e8
--- /dev/null
+++ b/frontend/site/site/site.actions.js
@@ -0,0 +1,15 @@
+import * as types from 'site/types'
+import { api } from 'app/utils'
+
+export const setSiteTitle = title => dispatch => {
+ document.querySelector('title').innerText = title
+ dispatch({ type: types.site.set_site_title, payload: title })
+}
+
+export const loadSite = graph_name => dispatch => (
+ api(dispatch, types.site, 'site', '/' + graph_name + '/index.json?t=' + (Date.now() / 3600000))
+)
+
+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
new file mode 100644
index 0000000..9763e48
--- /dev/null
+++ b/frontend/site/site/site.reducer.js
@@ -0,0 +1,43 @@
+import * as types from 'site/types'
+
+const initialState = {
+ siteTitle: 'swimmer',
+ interactive: false,
+ graph: {
+ loading: true,
+ }
+}
+
+export default function siteReducer(state = initialState, action) {
+ // console.log(action.type, action)
+ switch (action.type) {
+ case types.site.set_site_title:
+ return {
+ ...state,
+ siteTitle: action.payload,
+ }
+
+ case types.site.loaded:
+ return {
+ ...state,
+ graph: action.data.graph,
+ }
+
+ case types.site.interact:
+ return {
+ ...state,
+ interactive: true,
+ }
+
+ case '@@router/LOCATION_CHANGE':
+ return {
+ ...state,
+ graph: {
+ ...state.graph,
+ }
+ }
+
+ default:
+ return state
+ }
+}
diff --git a/frontend/site/store.js b/frontend/site/store.js
new file mode 100644
index 0000000..60c3116
--- /dev/null
+++ b/frontend/site/store.js
@@ -0,0 +1,39 @@
+import { applyMiddleware, compose, combineReducers, createStore } from 'redux'
+import { connectRouter, routerMiddleware } from 'connected-react-router'
+import { createBrowserHistory } from 'history'
+import thunk from 'redux-thunk'
+
+import siteReducer from 'site/site/site.reducer'
+import audioReducer from 'site/audio/audio.reducer'
+
+const createRootReducer = history => (
+ combineReducers({
+ auth: (state = {}) => state,
+ router: connectRouter(history),
+ audio: audioReducer,
+ site: siteReducer,
+ })
+)
+
+const configureStore = (initialState = {}, history) => {
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
+
+ const store = createStore(
+ createRootReducer(history),
+ initialState,
+ composeEnhancers(
+ applyMiddleware(
+ thunk,
+ routerMiddleware(history)
+ ),
+ ),
+ )
+
+ return store
+}
+
+const history = createBrowserHistory()
+const store = configureStore({}, history)
+const { dispatch } = store
+
+export { store, history, dispatch }
diff --git a/frontend/site/types.js b/frontend/site/types.js
new file mode 100644
index 0000000..4ab897f
--- /dev/null
+++ b/frontend/site/types.js
@@ -0,0 +1,11 @@
+import { with_type } from 'app/api/crud.types'
+
+export const site = with_type('site', [
+ 'set_site_title', 'loading', 'loaded', 'error', 'interact'
+])
+
+export const system = with_type('system', [
+ 'load_site',
+])
+
+export const init = '@@INIT'
diff --git a/frontend/site/viewer/viewer.container.js b/frontend/site/viewer/viewer.container.js
new file mode 100644
index 0000000..9bf4442
--- /dev/null
+++ b/frontend/site/viewer/viewer.container.js
@@ -0,0 +1,223 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { history } from 'site/store'
+import actions from 'site/actions'
+import { Loader } from 'app/common/loader.component'
+import TileHandle from 'app/views/tile/components/tile.handle'
+
+import 'app/views/page/page.css'
+
+class ViewerContainer extends Component {
+ state = {
+ page: {},
+ bounds: { width: window.innerWidth, height: window.innerHeight },
+ roadblock: false,
+ popups: {},
+ hidden: {},
+ time: 0,
+ maxDeferTime: 0,
+ }
+
+ 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)
+ this.updateTimer = this.updateTimer.bind(this)
+ window.addEventListener('resize', this.handleResize)
+ }
+
+ componentDidUpdate(prevProps) {
+ // console.log('didUpdate', this.props.graph !== prevProps.graph, this.props.location.pathname !== prevProps.location.pathname)
+ if (this.props.graph !== prevProps.graph || this.props.location.pathname !== prevProps.location.pathname) {
+ this.load()
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize)
+ actions.site.interact()
+ }
+
+ 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] || pages[home_page]
+ if (!this.props.interactive && hasAutoplay(page)) {
+ this.setState({ page, popups: {}, hidden: {}, roadblock: true })
+ } else {
+ this.setState({ page, popups: {}, hidden: {}, roadblock: false })
+ actions.site.interact()
+ this.props.audio.player.playPage(page)
+ this.resetTimer(page)
+ }
+ }
+
+ resetTimer(page) {
+ clearTimeout(this.timeout)
+ const maxDeferTime = page.tiles.reduce((max_time, tile) => Math.max(tile.settings.appear_after || 0, max_time), 0)
+ if (maxDeferTime) {
+ this.setState({ time: 0, maxDeferTime })
+ this.timeout = setTimeout(this.updateTimer, 500)
+ }
+ }
+
+ updateTimer() {
+ clearTimeout(this.timeout)
+ this.setState({ time: this.state.time + 0.500 })
+ if (this.state.time < this.state.maxDeferTime) {
+ this.timeout = setTimeout(this.updateTimer, 500)
+ }
+ }
+
+ handleResize() {
+ this.setState({
+ bounds: {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ }
+ })
+ }
+
+ handleMouseDown(e, tile) {
+ if (tile.href) {
+ if (tile.href.indexOf('http') === 0) {
+ window.location.href = tile.href
+ return
+ }
+ else if (tile.href === '__open_popup') {
+ this.setState({
+ popups: {
+ ...this.state.popups,
+ [tile.settings.target_popup]: true,
+ },
+ })
+ }
+ else if (tile.href === '__close_popup') {
+ this.setState({
+ popups: {
+ ...this.state.popups,
+ [tile.settings.target_popup]: false,
+ },
+ })
+ }
+ else if (!tile.settings.navigate_when_audio_finishes) {
+ history.push(tile.href)
+ }
+ }
+ if (tile.settings.audio_on_click_id > 0) {
+ this.props.audio.player.playTile({
+ type: "click",
+ tile,
+ })
+ }
+ if (tile.settings.hide_on_click) {
+ this.setState({
+ hidden: {
+ ...this.state.hidden,
+ [tile.id]: true,
+ }
+ })
+ }
+ }
+
+ handlePlaybackEnded(tile) {
+ if (tile.href && tile.settings.autoadvance) {
+ history.push(tile.href)
+ }
+ }
+
+ render() {
+ const { page, audio, popups, hidden, time } = this.state
+ if (this.state.roadblock) {
+ return this.renderRoadblock()
+ }
+ if (this.props.graph.loading || !page.id) {
+ return (
+ <div>
+ <div className='body'>
+ <div className='page loading'>
+ <Loader />
+ </div>
+ </div>
+ </div>
+ )
+ }
+ const { settings } = page
+ const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' }
+ const videoBounds = (page.tiles.length && page.tiles[0].type === 'video') ? {
+ width: page.tiles[0].settings.width,
+ height: page.tiles[0].settings.height,
+ } : this.state.bounds
+ // console.log(page)
+ return (
+ <div className='body'>
+ <div className='page' ref={this.pageRef} style={pageStyle}>
+ {page.tiles.map(tile => {
+ if (tile.settings.is_popup && !popups[tile.settings.popup_group]) return
+ if (tile.settings.appear_after && time < tile.settings.appear_after) return
+ if (tile.settings.hide_on_click && hidden[tile.id]) return
+ return (
+ <TileHandle
+ viewing
+ key={tile.id}
+ tile={tile}
+ audio={audio}
+ bounds={this.state.bounds}
+ videoBounds={videoBounds}
+ onMouseDown={e => this.handleMouseDown(e, tile)}
+ onPlaybackEnded={e => this.handlePlaybackEnded(e, tile)}
+ onDoubleClick={e => {}}
+ />
+ )
+ })}
+ </div>
+ </div>
+ )
+ }
+
+ removeRoadblock() {
+ console.log("remove roadblock")
+ actions.site.interact()
+ this.setState({ roadblock: false })
+ this.props.audio.player.playPage(this.state.page)
+ this.resetTimer(this.state.page)
+ }
+
+ renderRoadblock() {
+ const { title } = this.props.graph
+ return (
+ <div className='roadblock' onClick={this.removeRoadblock}>
+ <div>
+ <h2>{title}</h2>
+ <button>Enter</button>
+ </div>
+ </div>
+ )
+ }
+}
+
+const hasAutoplay = page => {
+ const hasAutoplayVideo = page.tiles.some(tile => {
+ return tile.type === 'video' && !tile.settings.muted
+ })
+ const hasAutoplayAudio = page.settings.background_audio_id > 0
+ return hasAutoplayAudio || hasAutoplayVideo
+}
+
+const mapStateToProps = state => ({
+ site: state.site,
+ audio: state.audio,
+ graph: state.site.graph,
+ interactive: state.site.interactive,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ViewerContainer)