diff options
| -rw-r--r-- | frontend/store.js | 4 | ||||
| -rw-r--r-- | frontend/views/graph/graph.css | 5 | ||||
| -rw-r--r-- | frontend/views/graph/graph.reducer.js (renamed from frontend/views/index/graph.reducer.js) | 2 | ||||
| -rw-r--r-- | frontend/views/index.js | 1 | ||||
| -rw-r--r-- | frontend/views/page/components/page.editor.js | 196 | ||||
| -rw-r--r-- | frontend/views/page/components/page.header.js | 31 | ||||
| -rw-r--r-- | frontend/views/page/components/tile.edit.js | 55 | ||||
| -rw-r--r-- | frontend/views/page/components/tile.form.js | 143 | ||||
| -rw-r--r-- | frontend/views/page/components/tile.new.js | 47 | ||||
| -rw-r--r-- | frontend/views/page/page.actions.js | 13 | ||||
| -rw-r--r-- | frontend/views/page/page.container.js | 81 | ||||
| -rw-r--r-- | frontend/views/page/page.css | 3 | ||||
| -rw-r--r-- | frontend/views/page/page.reducer.js | 78 |
13 files changed, 656 insertions, 3 deletions
diff --git a/frontend/store.js b/frontend/store.js index e0432c4..1fb7789 100644 --- a/frontend/store.js +++ b/frontend/store.js @@ -5,7 +5,8 @@ import thunk from 'redux-thunk' // import { login } from './util' import uploadReducer from './views/upload/upload.reducer' -import graphReducer from './views/index/graph.reducer' +import graphReducer from './views/graph/graph.reducer' +import pageReducer from './views/page/page.reducer' import siteReducer from './views/site/site.reducer' // import collectionReducer from './views/collection/collection.reducer' @@ -15,6 +16,7 @@ const createRootReducer = history => ( router: connectRouter(history), site: siteReducer, graph: graphReducer, + page: pageReducer, upload: uploadReducer, // collection: collectionReducer, }) diff --git a/frontend/views/graph/graph.css b/frontend/views/graph/graph.css index d24ce97..a2ab8a4 100644 --- a/frontend/views/graph/graph.css +++ b/frontend/views/graph/graph.css @@ -13,6 +13,8 @@ ); } +/* Sidebar boxes */ + .box { width: 15rem; position: absolute; @@ -23,7 +25,6 @@ border: 2px solid #000; box-shadow: 2px 2px 4px rgba(0,0,0,0.5); } - .box h1, .box h2 { font-size: 1rem; @@ -51,6 +52,8 @@ display: none; } +/* Graph handles */ + .handle { position: absolute; border: 2px solid #888; diff --git a/frontend/views/index/graph.reducer.js b/frontend/views/graph/graph.reducer.js index 9f42f52..9e682e4 100644 --- a/frontend/views/index/graph.reducer.js +++ b/frontend/views/graph/graph.reducer.js @@ -15,7 +15,7 @@ const initialState = crudState('graph', { const reducer = crudReducer('graph') export default function graphReducer(state = initialState, action) { - console.log(action.type, action) + // console.log(action.type, action) state = reducer(state, action) switch (action.type) { case types.graph.update_graph_page: diff --git a/frontend/views/index.js b/frontend/views/index.js index 07e7284..c50ab80 100644 --- a/frontend/views/index.js +++ b/frontend/views/index.js @@ -1,3 +1,4 @@ export { default as index } from './index/index.container' export { default as graph } from './graph/graph.container' +export { default as page } from './page/page.container' export { default as upload } from './upload/upload.container' diff --git a/frontend/views/page/components/page.editor.js b/frontend/views/page/components/page.editor.js new file mode 100644 index 0000000..952e45c --- /dev/null +++ b/frontend/views/page/components/page.editor.js @@ -0,0 +1,196 @@ +import React, { Component } from 'react' +import { Route } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { session } from '../../../session' +import actions from '../../../actions' +import * as pageActions from '../page.actions' + +import { Loader } from '../../../common' +import { clamp } from '../../../util' + +const defaultState = { + dragging: false, + bounds: null, + mouseX: 0, + mouseY: 0, + box: { + x: 0, y: 0, + w: 0, h: 0, + }, + tile: null, +} + +class PageEditor extends Component { + state = { + ...defaultState, + } + + constructor() { + super() + // bind these events in the constructor, so we can remove event listeners later + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleWindowResize = this.handleWindowResize.bind(this) + this.pageRef = React.createRef() + } + + getBoundingClientRect() { + if (!this.pageRef.current) return null + const rect = this.pageRef.current.getBoundingClientRect() + const scrollTop = document.body.scrollTop || document.body.parentNode.scrollTop + const scrollLeft = document.body.scrollLeft || document.body.parentNode.scrollLeft + const bounds = { + top: rect.top + scrollTop, + left: rect.left + scrollLeft, + width: rect.width, + height: rect.height, + } + // console.log(bounds) + return bounds + } + + componentDidMount() { + document.body.addEventListener('mousemove', this.handleMouseMove) + document.body.addEventListener('mouseup', this.handleMouseUp) + window.addEventListener('resize', this.handleWindowResize) + this.setState({ bounds: this.getBoundingClientRect() }) + } + + componentDidUpdate(prevProps) { + if (!this.state.bounds) { + this.setState({ bounds: this.getBoundingClientRect() }) + } + } + + handleWindowResize() { + this.setState({ bounds: this.getBoundingClientRect() }) + } + + handleMouseDown(e, page) { + const bounds = this.getBoundingClientRect() + const mouseX = e.pageX + const mouseY = e.pageY + let w = 128 / bounds.width + let h = 16 / bounds.height + let { x, y } = page.settings + x = clamp(x, 0, 1) + y = clamp(y, 0, 1) + this.setState({ + page, + draggingBox: true, + bounds, + mouseX, + mouseY, + box: { + x, y, + w, h, + }, + initialBox: { + x, y, + w, h, + } + }) + } + + handleMouseMove(e) { + const { + dragging, draggingBox, + bounds, mouseX, mouseY, initialBox, box + } = this.state + if (draggingBox) { + e.preventDefault() + let { x, y, w, h } = initialBox + let dx = (e.pageX - mouseX) / bounds.width + let dy = (e.pageY - mouseY) / bounds.height + this.setState({ + box: { + x: clamp(x + dx, 0, 1.0 - w), + y: clamp(y + dy, 0, 1.0 - h), + w, h, + } + }) + } + } + + handleMouseUp(e) { + // const { actions } = this.props + const { dragging, draggingBox, bounds, box, page } = this.state + if (!dragging && !draggingBox) return + e.preventDefault() + const { x, y, w, h } = box + let url = window.location.pathname + this.setState({ + page: null, + box: null, + initialBox: null, + dragging: false, + draggingBox: false, + }) + // console.log(page) + const updatedTile = { + ...tile, + settings: { + ...tile.settings, + x, y, + } + } + this.props.pageActions.updatePageTile(updatedTile) + actions.tile.update(updatedTile) + } + + render(){ + // console.log(this.props.page.show.res) + const currentTile = this.state.tile + const currentBox = this.state.box + const { res } = this.props.page.show + // console.log(res.tiles) + return ( + <div className='page' ref={this.pageRef}> + {this.state.bounds && res.tiles.map(tile => ( + <TileHandle + key={tile.id} + tile={tile} + bounds={this.state.bounds} + box={currentTile && tile.id === currentTile.id && currentBox} + onMouseDown={e => this.handleMouseDown(e, tile)} + /> + ))} + </div> + ) + } +} + +const TileHandle = ({ tile, bounds, box, onMouseDown }) => { + let style; + if (box) { + style = { + top: (bounds.height) * box.y, + left: (bounds.width) * box.x, + } + } else { + style = { + top: (bounds.height) * Math.min(tile.settings.y, 0.95), + left: (bounds.width) * Math.min(tile.settings.x, 0.95), + } + } + // console.log(style) + return ( + <div className='handle' onMouseDown={onMouseDown} style={style}> + {tile.title} + </div> + ) +} + +const mapStateToProps = state => ({ + graph: state.graph, + page: state.page, +}) + +const mapDispatchToProps = dispatch => ({ + pageActions: bindActionCreators({ ...pageActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PageEditor) diff --git a/frontend/views/page/components/page.header.js b/frontend/views/page/components/page.header.js new file mode 100644 index 0000000..a6c47ee --- /dev/null +++ b/frontend/views/page/components/page.header.js @@ -0,0 +1,31 @@ +import React from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { Link } from 'react-router-dom' + +import * as pageActions from '../page.actions' + +function PageHeader(props) { + return ( + <header> + <div> + <Link to="/" className="logo"><b>{props.site.siteTitle}</b></Link> + </div> + <div> + <button onClick={() => props.pageActions.showAddTileForm()}>+ Add tile</button> + </div> + </header> + ) +} + +const mapStateToProps = (state) => ({ + // auth: state.auth, + site: state.site, + // isAuthenticated: state.auth.isAuthenticated, +}) + +const mapDispatchToProps = (dispatch) => ({ + pageActions: bindActionCreators({ ...pageActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PageHeader) diff --git a/frontend/views/page/components/tile.edit.js b/frontend/views/page/components/tile.edit.js new file mode 100644 index 0000000..3420505 --- /dev/null +++ b/frontend/views/page/components/tile.edit.js @@ -0,0 +1,55 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from '../../../store' +import actions from '../../../actions' + +import { Loader } from '../../../common' + +import TileForm from '../components/tile.form' + +class TileEdit extends Component { + componentDidMount() { + console.log(this.props.match.params.id) + actions.tile.show(this.props.match.params.id) + } + + handleSubmit(data) { + actions.tile.update(data) + .then(response => { + // response + console.log(response) + }) + } + + render() { + const { show } = this.props.tile + if (show.loading || !show.res) { + return ( + <div className='form'> + <Loader /> + </div> + ) + } + return ( + <TileForm + data={show.res} + graph={this.props.graph.show.res} + onSubmit={this.handleSubmit.bind(this)} + /> + ) + } +} + +const mapStateToProps = state => ({ + graph: state.graph, + page: state.page, + tile: state.tile, +}) + +const mapDispatchToProps = dispatch => ({ + // searchActions: bindActionCreators({ ...searchActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(TileEdit) diff --git a/frontend/views/page/components/tile.form.js b/frontend/views/page/components/tile.form.js new file mode 100644 index 0000000..1e75cd8 --- /dev/null +++ b/frontend/views/page/components/tile.form.js @@ -0,0 +1,143 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' + +import { session } from '../../../session' + +import { TextInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' + +const newTile = (data) => ({ + path: '', + title: '', + username: session('username'), + description: '', + settings: { + x: 0.05, y: 0.05, + }, + ...data, +}) + +export default class TileForm extends Component { + state = { + title: "", + submitTitle: "", + data: { ...newTile() }, + errorFields: new Set([]), + } + + componentDidMount() { + const { graph, data, isNew } = this.props + const title = isNew ? 'new tile' : 'editing ' + data.title + const submitTitle = isNew ? "Create Tile" : "Save Changes" + this.setState({ + title, + submitTitle, + errorFields: new Set([]), + data: { + ...newTile({ graph_id: graph.id }), + ...data, + }, + }) + } + + handleChange(e) { + const { errorFields } = this.state + const { name, value } = e.target + if (errorFields.has(name)) { + errorFields.delete(name) + } + let sanitizedValue = value + if (name === 'path') { + sanitizedValue = sanitizedValue.toLowerCase().replace(/ /, '-').replace(/[!@#$%^&*()[\]{}]/, '-').replace(/-+/, '-') + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: sanitizedValue, + } + }) + } + + handleSelect(name, value) { + const { errorFields } = this.state + if (errorFields.has(name)) { + errorFields.delete(name) + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: value, + } + }) + } + + handleSubmit(e) { + e.preventDefault() + const { isNew, onSubmit } = this.props + const { data } = this.state + const requiredKeys = "path title".split(" ") + const validKeys = "graph_id path title username description settings".split(" ") + const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {}) + const errorFields = requiredKeys.filter(key => !validData[key]) + if (errorFields.length) { + console.log('error', errorFields, validData) + this.setState({ errorFields: new Set(errorFields) }) + } else { + if (isNew) { + // side effect: set username if we're creating a new tile + // session.set('username', data.username) + } else { + validData.id = data.id + } + console.log('submit', validData) + onSubmit(validData) + } + } + + render() { + const { graph, isNew } = this.props + const { title, submitTitle, errorFields, data } = this.state + return ( + <div className='box'> + <h1>{title}</h1> + <form onSubmit={this.handleSubmit.bind(this)}> + <TextInput + title="Path" + name="path" + required + data={data} + error={errorFields.has('path')} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Title" + name="title" + required + data={data} + error={errorFields.has('title')} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextArea + title="Description" + name="description" + data={data} + onChange={this.handleChange.bind(this)} + /> + <SubmitButton + title={submitTitle} + onClick={this.handleSubmit.bind(this)} + /> + {!!errorFields.size && + <label> + <span></span> + <span>Please complete the required fields =)</span> + </label> + } + </form> + </div> + ) + } +} diff --git a/frontend/views/page/components/tile.new.js b/frontend/views/page/components/tile.new.js new file mode 100644 index 0000000..7445f27 --- /dev/null +++ b/frontend/views/page/components/tile.new.js @@ -0,0 +1,47 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from '../../../store' +import actions from '../../../actions' + +import TileForm from '../components/tile.form' + +class TileNew extends Component { + handleSubmit(data) { + console.log(data) + actions.tile.create(data) + .then(res => { + console.log(res) + const graph = this.props.graph.show.res + if (res.res && res.res.id) { + history.push('/' + graph.path + '/' + res.res.path) + } + }) + .catch(err => { + console.error('error') + }) + } + + render() { + return ( + <TileForm + isNew + graph={this.props.graph.show.res} + data={{}} + onSubmit={this.handleSubmit.bind(this)} + /> + ) + } +} + +const mapStateToProps = state => ({ + graph: state.graph, + page: state.page, +}) + +const mapDispatchToProps = dispatch => ({ + // searchActions: bindActionCreators({ ...searchActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(TileNew) diff --git a/frontend/views/page/page.actions.js b/frontend/views/page/page.actions.js new file mode 100644 index 0000000..cfd0f98 --- /dev/null +++ b/frontend/views/page/page.actions.js @@ -0,0 +1,13 @@ +import * as types from '../../types' + +export const showAddTileForm = () => dispatch => { + dispatch({ type: types.graph.show_add_tile_form }) +} + +export const hideAddTileForm = () => dispatch => { + dispatch({ type: types.graph.hide_add_tile_form }) +} + +export const updatePageTile = tile => dispatch => { + dispatch({ type: types.graph.update_graph_tile, tile }) +} diff --git a/frontend/views/page/page.container.js b/frontend/views/page/page.container.js new file mode 100644 index 0000000..2de56bb --- /dev/null +++ b/frontend/views/page/page.container.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react' +import { Route } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import './page.css' + +import actions from '../../actions' +import { Loader } from '../../common' + +// import * as uploadActions from './upload.actions' + +import TileNew from './components/tile.new' +import TileEdit from './components/tile.edit' + +import PageHeader from './components/page.header' +import PageEditor from './components/page.editor' + +class PageContainer extends Component { + componentDidMount() { + if (this.shouldShowPage()) this.load() + } + componentDidUpdate(prevProps) { + if (this.shouldLoadPage(prevProps)) this.load() + } + shouldShowPage() { + const { graph_name, page_name } = this.props.match.params + // console.log(graph_name, page_name) + return (graph_name && page_name && graph_name !== 'index') + } + shouldLoadPage(prevProps) { + const { page, location } = this.props + const { key } = location + if (key === prevProps.location.key) return false + if (!this.shouldShowPage()) return false + return (page.show.name === prevProps.page.show.name) + } + load() { + actions.site.setSiteTitle("loading " + this.props.match.params.page_name + "...") + actions.page.show('name/' + this.props.match.params.graph_name + '/' + this.props.match.params.page_name) + .then(data => { + actions.site.setSiteTitle(data.res.title) + }) + } + render() { + if (!this.shouldShowPage()) return null + if (!this.props.page.show.res || this.props.page.show.loading) { + return ( + <div> + <PageHeader /> + <div className='body'> + <div className='page loading'> + <Loader /> + </div> + </div> + </div> + ) + } + return ( + <div> + <PageHeader /> + <div className='body'> + <PageEditor /> + {this.props.page.editor.addingTile && <TileNew />} + {this.props.page.editor.editingTile && <TileEdit />} + </div> + </div> + ) + } +} + +const mapStateToProps = state => ({ + graph: state.graph, + page: state.page, +}) + +const mapDispatchToProps = dispatch => ({ + // uploadActions: bindActionCreators({ ...uploadActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PageContainer) diff --git a/frontend/views/page/page.css b/frontend/views/page/page.css new file mode 100644 index 0000000..b0de716 --- /dev/null +++ b/frontend/views/page/page.css @@ -0,0 +1,3 @@ +.page.loading { + padding: 1rem; +} diff --git a/frontend/views/page/page.reducer.js b/frontend/views/page/page.reducer.js new file mode 100644 index 0000000..d1d6510 --- /dev/null +++ b/frontend/views/page/page.reducer.js @@ -0,0 +1,78 @@ +import * as types from '../../types' +// import { session, getDefault, getDefaultInt } from '../../session' + +import { crudState, crudReducer } from '../../api/crud.reducer' + +const initialState = crudState('page', { + editor: { + addingTile: false, + editingTile: false, + }, + options: { + } +}) + +const reducer = crudReducer('page') + +export default function pageReducer(state = initialState, action) { + console.log(action.type, action) + state = reducer(state, action) + switch (action.type) { + case types.page.update_page_tile: + return { + ...state, + show: { + ...state.show, + res: { + ...state.show.res, + tiles: state.show.res.tiles.map(tile => { + if (tile.id === action.tile.id) { + return { ...action.tile } + } else { + return tile + } + }), + } + } + } + + case types.page.show_add_tile_form: + return { + ...state, + editor: { + ...state.editor, + addingTile: true, + } + } + + case types.page.hide_add_tile_form: + return { + ...state, + editor: { + ...state.editor, + addingTile: false, + } + } + + case types.page.show_edit_tile_form: + return { + ...state, + editor: { + ...state.editor, + addingTile: true, + } + } + + case types.page.hide_edit_tile_form: + return { + ...state, + editor: { + ...state.editor, + addingTile: false, + } + } + + default: + return state + } +} |
