summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/app/sql/models/graph.py5
-rw-r--r--cli/app/utils/file_utils.py12
-rw-r--r--cli/commands/site/export.py74
-rw-r--r--frontend/api/crud.types.js2
-rw-r--r--frontend/site/actions.js19
-rw-r--r--frontend/site/app.js33
-rw-r--r--frontend/site/index.js17
-rw-r--r--frontend/site/site/site.actions.js23
-rw-r--r--frontend/site/site/site.reducer.js29
-rw-r--r--frontend/site/store.js38
-rw-r--r--frontend/site/types.js11
-rw-r--r--frontend/site/viewer/viewer.container.js96
-rw-r--r--frontend/views/page/components/tile.handle.js37
-rw-r--r--static/site.html17
-rw-r--r--webpack.config.site.js54
15 files changed, 445 insertions, 22 deletions
diff --git a/cli/app/sql/models/graph.py b/cli/app/sql/models/graph.py
index fbfb09c..8e068a0 100644
--- a/cli/app/sql/models/graph.py
+++ b/cli/app/sql/models/graph.py
@@ -42,6 +42,11 @@ class Graph(Base):
data['pages'] = [ page.toLinkJSON() for page in self.pages ]
return data
+ def toSiteJSON(self):
+ data = self.toJSON()
+ data['pages'] = [ page.toFullJSON() for page in self.pages ]
+ return data
+
class GraphForm(ModelForm):
class Meta:
model = Graph
diff --git a/cli/app/utils/file_utils.py b/cli/app/utils/file_utils.py
index 7f1f417..0e672fc 100644
--- a/cli/app/utils/file_utils.py
+++ b/cli/app/utils/file_utils.py
@@ -195,12 +195,14 @@ def load_yaml(fp_in):
cfg = yaml.load(fp, Loader=yaml.Loader)
return cfg
-def load_text(fp_in):
+def load_text(fp_in, split=True):
"""Load a text file into an array
:param fp_in: (str) filepath
"""
with open(fp_in, 'rt') as fp:
- lines = fp.read().rstrip('\n').split('\n')
+ lines = fp.read().rstrip('\n')
+ if split:
+ lines = lines.split('\n')
return lines
def load_line_lookup(fp_in):
@@ -264,16 +266,16 @@ def write_pickle(data, fp_out, ensure_path=True):
pickle.dump(data, fp)
-def write_json(data, fp_out, minify=True, ensure_path=True, sort_keys=True, verbose=False):
+def write_json(data, fp_out, minify=True, ensure_path=True, sort_keys=True, verbose=False, default=None):
"""
"""
if ensure_path:
mkdirs(fp_out)
with open(fp_out, 'w') as fp:
if minify:
- json.dump(data, fp, separators=(',',':'), sort_keys=sort_keys)
+ json.dump(data, fp, separators=(',',':'), sort_keys=sort_keys, default=default)
else:
- json.dump(data, fp, indent=2, sort_keys=sort_keys)
+ json.dump(data, fp, indent=2, sort_keys=sort_keys, default=default)
if verbose:
log.info('Wrote JSON: {}'.format(fp_out))
diff --git a/cli/commands/site/export.py b/cli/commands/site/export.py
index 8212f55..c8e687a 100644
--- a/cli/commands/site/export.py
+++ b/cli/commands/site/export.py
@@ -1,6 +1,8 @@
import click
from app.settings import app_cfg
+from app.utils.file_utils import load_text, write_json, write_text
+from os.path import join
@click.command('info')
@click.option('-g', '--graph', 'opt_graph_path', required=True,
@@ -14,12 +16,10 @@ def cli(ctx, opt_graph_path, opt_output_dir):
# ------------------------------------------------
# imports
- from os.path import join
-
from app.sql.common import db, Session, Graph, Page, Tile
# ------------------------------------------------
- # generate HTML for all pages
+ # generate HTML for index and all pages
session = Session()
graph = session.query(Graph).filter(Graph.path == opt_graph_path).first()
@@ -27,12 +27,76 @@ def cli(ctx, opt_graph_path, opt_output_dir):
print(f"Not a graph: {opt_graph_path}")
return
+ print(f"Output site to {opt_output_dir}")
+
+ site_data = { 'graph': sanitize_graph(graph.toSiteJSON()) }
+
+ index_html = load_text(join(app_cfg.DIR_STATIC, 'site.html'), split=False)
+ write_json(site_data, join(opt_output_dir, graph.path, 'index.json'), default=str)
+ write_index(graph, None, index_html, join(opt_output_dir, graph.path, 'index.html'))
+
+ index_path = ""
for page in graph.pages:
page_path = f'{graph.path}/{page.path}'
if page.id == graph.home_page_id:
+ index_path = page_path
print(f'/{page_path} [index]')
else:
print(f'/{page_path}')
- #
+ write_index(graph, page, index_html, join(opt_output_dir, graph.path, page.path, 'index.html'))
+
# ------------------------------------------------
- # cat all the relevant CSS from the main site
+ # generate javascript...
+
+ # NODE_ENV=production webpack --config ./webpack.config.site.js -o ./data_store/exports/asdf/bundle.js
+
+def write_index(graph, page, index_html, fp_out):
+ if page is None:
+ page_title = graph.title
+ else:
+ page_title = page.title
+ index_html = index_html.replace('BUNDLE_PATH', join('/', graph.path, 'bundle.js'))
+ index_html = index_html.replace('PAGE_TITLE', page_title)
+ write_text(index_html, fp_out)
+
+def sanitize_graph(graph):
+ page_path_lookup = {}
+ page_lookup = {}
+ for page in graph['pages']:
+ page_path_lookup[page['id']] = join('/', graph['path'], page['path'])
+ for page in graph['pages']:
+ sanitize_page(page)
+ for tile in page['tiles']:
+ if tile['target_page_id']:
+ if tile['target_page_id'] == -1:
+ tile['href'] = tile['external_link_url']
+ elif tile['target_page_id'] > 0:
+ tile['href'] = page_path_lookup[tile['target_page_id']]
+ sanitize_tile(tile)
+ page_path = page_path_lookup[page['id']]
+ page_lookup[page_path] = page
+ graph['pages'] = page_lookup
+ graph['home_page'] = page_path_lookup[graph['home_page_id']]
+ return graph
+
+def sanitize_page(data):
+ if 'created_at' in data:
+ del data['created_at']
+ if 'updated_at' in data:
+ del data['updated_at']
+ if 'graph_id' in data:
+ del data['graph_id']
+
+def sanitize_tile(data):
+ if 'created_at' in data:
+ del data['created_at']
+ if 'updated_at' in data:
+ del data['updated_at']
+ if 'username' in data:
+ del data['username']
+ if 'graph_id' in data:
+ del data['graph_id']
+ if 'page_id' in data:
+ del data['page_id']
+ if 'target_page_id' in data:
+ del data['target_page_id']
diff --git a/frontend/api/crud.types.js b/frontend/api/crud.types.js
index 7a64f5c..7b24811 100644
--- a/frontend/api/crud.types.js
+++ b/frontend/api/crud.types.js
@@ -1,7 +1,7 @@
export const as_type = (a, b) => [a, b].join('_').toUpperCase()
-export const with_type = (type, actions) =>
+export const with_type = (type, actions) =>
actions.reduce((a, b) => (a[b] = as_type(type, b)) && a, {})
export const crud_type = (type, actions=[]) =>
diff --git a/frontend/site/actions.js b/frontend/site/actions.js
new file mode 100644
index 0000000..e672028
--- /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.actions'
+
+import { store } from './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..cf52460
--- /dev/null
+++ b/frontend/site/app.js
@@ -0,0 +1,33 @@
+import React, { Component } from 'react'
+import { ConnectedRouter } from 'connected-react-router'
+import { Route } from 'react-router'
+
+import ViewerContainer from './viewer/viewer.container'
+import actions from './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, path_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/index.js b/frontend/site/index.js
new file mode 100644
index 0000000..6f1a0a5
--- /dev/null
+++ b/frontend/site/index.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { Provider } from 'react-redux'
+
+import App from './app'
+
+import { store, history } from './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/site.actions.js b/frontend/site/site/site.actions.js
new file mode 100644
index 0000000..1d92de1
--- /dev/null
+++ b/frontend/site/site/site.actions.js
@@ -0,0 +1,23 @@
+import * as types from '../types'
+import { api } from '../../util'
+import { history } from '../store'
+// import actions from '../../actions'
+// import { session } from '../../session'
+
+export const setSiteTitle = title => dispatch => {
+ document.querySelector('title').innerText = title
+ dispatch({ type: types.site.set_site_title, payload: title })
+}
+
+export const loadSite = (graph_name, path_name) => dispatch => (
+ api(dispatch, types.site, 'site', '/' + graph_name + '/index.json')
+ .then(res => {
+ const { graph } = res.data
+ // console.log(graph)
+ // console.log(graph.home_page)
+ const first_path = ["", graph_name, path_name].join("/")
+ if (!path_name || !(first_path in graph.pages)) {
+ setTimeout(() => history.push(graph.home_page), 10)
+ }
+ })
+)
diff --git a/frontend/site/site/site.reducer.js b/frontend/site/site/site.reducer.js
new file mode 100644
index 0000000..4975089
--- /dev/null
+++ b/frontend/site/site/site.reducer.js
@@ -0,0 +1,29 @@
+import * as types from '../types'
+// import { session, getDefault, getDefaultInt } from '../../session'
+
+const initialState = {
+ siteTitle: 'swimmer',
+ 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,
+ }
+
+ default:
+ return state
+ }
+}
diff --git a/frontend/site/store.js b/frontend/site/store.js
new file mode 100644
index 0000000..cd77990
--- /dev/null
+++ b/frontend/site/store.js
@@ -0,0 +1,38 @@
+import { applyMiddleware, compose, combineReducers, createStore } from 'redux'
+import { connectRouter, routerMiddleware } from 'connected-react-router'
+import { createBrowserHistory } from 'history'
+import thunk from 'redux-thunk'
+// import { login } from './util'
+
+import siteReducer from './site/site.reducer'
+
+const createRootReducer = history => (
+ combineReducers({
+ auth: (state = {}) => state,
+ router: connectRouter(history),
+ 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..8388eff
--- /dev/null
+++ b/frontend/site/types.js
@@ -0,0 +1,11 @@
+import { with_type, crud_type } from '../api/crud.types'
+
+export const site = with_type('site', [
+ 'set_site_title', 'loading', 'loaded', 'error',
+])
+
+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..e0a0079
--- /dev/null
+++ b/frontend/site/viewer/viewer.container.js
@@ -0,0 +1,96 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import actions from '../actions'
+import { Loader } from '../../common'
+import TileHandle from '../../views/page/components/tile.handle'
+
+import '../../views/page/page.css'
+
+class ViewerContainer extends Component {
+ state = {
+ page: {},
+ }
+
+ constructor(props) {
+ super(props)
+ this.pageRef = React.createRef()
+ this.handleMouseDown = this.handleMouseDown.bind(this)
+ }
+
+ 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()
+ }
+ }
+
+ 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] })
+ } else {
+ // console.log(page)
+ console.log(page_path)
+ this.setState({ page })
+ }
+ }
+
+ handleMouseDown(e, tile) {
+ // console.log(tile)
+ }
+
+ render() {
+ const { page } = this.state
+ 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' }
+ // console.log(page)
+ return (
+ <div className='body'>
+ <div className='page' ref={this.pageRef} style={pageStyle}>
+ {page.tiles.map(tile => {
+ return (
+ <TileHandle
+ viewing
+ key={tile.id}
+ tile={tile}
+ bounds={this.state.bounds}
+ onMouseDown={e => this.handleMouseDown(e, tile)}
+ onDoubleClick={e => {}}
+ />
+ )
+ })}
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ site: state.site,
+ graph: state.site.graph,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ViewerContainer)
diff --git a/frontend/views/page/components/tile.handle.js b/frontend/views/page/components/tile.handle.js
index dd89282..f3a700b 100644
--- a/frontend/views/page/components/tile.handle.js
+++ b/frontend/views/page/components/tile.handle.js
@@ -1,6 +1,7 @@
import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
-const TileHandle = ({ tile, bounds, box, onMouseDown, onDoubleClick }) => {
+const TileHandle = ({ tile, bounds, box, viewing, onMouseDown, onDoubleClick }) => {
// console.log(tile)
const { width, height } = tile.settings
const style = {
@@ -66,16 +67,30 @@ const TileHandle = ({ tile, bounds, box, onMouseDown, onDoubleClick }) => {
style.height = tile.settings.height ? tile.settings.height + 'px' : 'auto'
break
}
- return (
- <div
- className={className}
- onMouseDown={onMouseDown}
- onDoubleClick={onDoubleClick}
- style={style}
- >
- {content}
- </div>
- )
+ if (viewing && tile.href) {
+ return (
+ <Link to={tile.href}>
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ style={style}
+ >
+ {content}
+ </div>
+ </Link>
+ )
+ } else {
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+ }
}
const generateTransform = (tile, box) => {
diff --git a/static/site.html b/static/site.html
new file mode 100644
index 0000000..18bd0d0
--- /dev/null
+++ b/static/site.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>PAGE_TITLE</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
+ <style>
+ html { background: #000; }
+ </style>
+</head>
+<body>
+<script>
+ var s = document.createElement('script');
+ s.setAttribute('src', 'BUNDLE_PATH?' + (Date.now() / 3600))
+ document.body.appendChild(s)
+</script>
+</html>
diff --git a/webpack.config.site.js b/webpack.config.site.js
new file mode 100644
index 0000000..8a62c75
--- /dev/null
+++ b/webpack.config.site.js
@@ -0,0 +1,54 @@
+require('dotenv').config()
+
+const webpack = require('webpack')
+const path = require('path')
+
+// print stack-trace of deprecations in webpack plugins, if something causes this
+// process.traceDeprecation = true
+
+module.exports = {
+ mode: "development",
+ entry: {
+ main: './frontend/site/index.js'
+ },
+ output: {
+ path: path.resolve(__dirname, 'static/js/dist'),
+ filename: 'bundle.js'
+ },
+ devtool: 'cheap-module-eval-source-map',
+ resolve: {
+ alias: {
+ // "react": "preact/compat",
+ // "react-dom/test-utils": "preact/test-utils",
+ // "react-dom": "preact/compat",
+ }
+ },
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': '"development"',
+ '__REACT_DEVTOOLS_GLOBAL_HOOK__': '({ isDisabled: true })'
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ },
+ {
+ test: /\.js$/,
+ // include: path.resolve(__dirname, 'client'),
+ exclude: /(node_modules|bower_components|build)/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/preset-env'],
+ plugins: [
+ "@babel/plugin-transform-runtime",
+ ],
+ }
+ }
+ }
+ ]
+ }
+}