From abd277bc02d36570038de4c0646a672f5585fa84 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 2 Jun 2020 22:34:16 +0200 Subject: stubbing in page editor --- frontend/store.js | 4 +- frontend/views/graph/graph.css | 5 +- frontend/views/graph/graph.reducer.js | 78 ++++++++++ frontend/views/index.js | 1 + frontend/views/index/graph.reducer.js | 78 ---------- frontend/views/page/components/page.editor.js | 196 ++++++++++++++++++++++++++ frontend/views/page/components/page.header.js | 31 ++++ frontend/views/page/components/tile.edit.js | 55 ++++++++ frontend/views/page/components/tile.form.js | 143 +++++++++++++++++++ frontend/views/page/components/tile.new.js | 47 ++++++ frontend/views/page/page.actions.js | 13 ++ frontend/views/page/page.container.js | 81 +++++++++++ frontend/views/page/page.css | 3 + frontend/views/page/page.reducer.js | 78 ++++++++++ 14 files changed, 733 insertions(+), 80 deletions(-) create mode 100644 frontend/views/graph/graph.reducer.js delete mode 100644 frontend/views/index/graph.reducer.js create mode 100644 frontend/views/page/components/page.editor.js create mode 100644 frontend/views/page/components/page.header.js create mode 100644 frontend/views/page/components/tile.edit.js create mode 100644 frontend/views/page/components/tile.form.js create mode 100644 frontend/views/page/components/tile.new.js create mode 100644 frontend/views/page/page.actions.js create mode 100644 frontend/views/page/page.container.js create mode 100644 frontend/views/page/page.css create mode 100644 frontend/views/page/page.reducer.js 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/graph/graph.reducer.js b/frontend/views/graph/graph.reducer.js new file mode 100644 index 0000000..9e682e4 --- /dev/null +++ b/frontend/views/graph/graph.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('graph', { + editor: { + addingPage: false, + editingPage: false, + }, + options: { + } +}) + +const reducer = crudReducer('graph') + +export default function graphReducer(state = initialState, action) { + // console.log(action.type, action) + state = reducer(state, action) + switch (action.type) { + case types.graph.update_graph_page: + return { + ...state, + show: { + ...state.show, + res: { + ...state.show.res, + pages: state.show.res.pages.map(page => { + if (page.id === action.page.id) { + return { ...action.page } + } else { + return page + } + }), + } + } + } + + case types.graph.show_add_page_form: + return { + ...state, + editor: { + ...state.editor, + addingPage: true, + } + } + + case types.graph.hide_add_page_form: + return { + ...state, + editor: { + ...state.editor, + addingPage: false, + } + } + + case types.graph.show_edit_page_form: + return { + ...state, + editor: { + ...state.editor, + addingPage: true, + } + } + + case types.graph.hide_edit_page_form: + return { + ...state, + editor: { + ...state.editor, + addingPage: false, + } + } + + default: + return state + } +} 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/index/graph.reducer.js b/frontend/views/index/graph.reducer.js deleted file mode 100644 index 9f42f52..0000000 --- a/frontend/views/index/graph.reducer.js +++ /dev/null @@ -1,78 +0,0 @@ -import * as types from '../../types' -// import { session, getDefault, getDefaultInt } from '../../session' - -import { crudState, crudReducer } from '../../api/crud.reducer' - -const initialState = crudState('graph', { - editor: { - addingPage: false, - editingPage: false, - }, - options: { - } -}) - -const reducer = crudReducer('graph') - -export default function graphReducer(state = initialState, action) { - console.log(action.type, action) - state = reducer(state, action) - switch (action.type) { - case types.graph.update_graph_page: - return { - ...state, - show: { - ...state.show, - res: { - ...state.show.res, - pages: state.show.res.pages.map(page => { - if (page.id === action.page.id) { - return { ...action.page } - } else { - return page - } - }), - } - } - } - - case types.graph.show_add_page_form: - return { - ...state, - editor: { - ...state.editor, - addingPage: true, - } - } - - case types.graph.hide_add_page_form: - return { - ...state, - editor: { - ...state.editor, - addingPage: false, - } - } - - case types.graph.show_edit_page_form: - return { - ...state, - editor: { - ...state.editor, - addingPage: true, - } - } - - case types.graph.hide_edit_page_form: - return { - ...state, - editor: { - ...state.editor, - addingPage: false, - } - } - - default: - return state - } -} 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 ( +
+ {this.state.bounds && res.tiles.map(tile => ( + this.handleMouseDown(e, tile)} + /> + ))} +
+ ) + } +} + +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 ( +
+ {tile.title} +
+ ) +} + +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 ( +
+
+ {props.site.siteTitle} +
+
+ +
+
+ ) +} + +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 ( +
+ +
+ ) + } + return ( + + ) + } +} + +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 ( +
+

{title}

+
+ + +