Sketch out new flow (with URL change)

This commit is contained in:
Tom Russell 2018-11-29 22:00:53 +00:00
parent 09b59dbf6d
commit 377149975b
9 changed files with 201 additions and 218 deletions

View File

@ -10,12 +10,11 @@ import BuildingEdit from './building-edit';
import BuildingView from './building-view'; import BuildingView from './building-view';
import ColouringMap from './map'; import ColouringMap from './map';
import Header from './header'; import Header from './header';
import Legend from './legend'; import Overview from './overview';
import Login from './login'; import Login from './login';
import MyAccountPage from './my-account'; import MyAccountPage from './my-account';
import SignUp from './signup'; import SignUp from './signup';
import Welcome from './welcome'; import Welcome from './welcome';
import BuildingEditAny from './building-edit-any';
@ -83,18 +82,26 @@ class App extends React.Component {
<Route exact path="/"> <Route exact path="/">
<Welcome /> <Welcome />
</Route> </Route>
<Route exact path="/select.html"> <Route exact path="/view/:cat.html" render={(props) => (
<BuildingEditAny user={this.state.user} /> <Overview
</Route> {...props}
<Route exact path="/map/:map.html" component={Legend} /> mode='view' user={this.state.user}
<Route exact path="/building/:building.html" render={(props) => ( />
) } />
<Route exact path="/edit/:cat.html" render={(props) => (
<Overview
{...props}
mode='edit' user={this.state.user}
/>
) } />
<Route exact path="/view/:cat/building/:building.html" render={(props) => (
<BuildingView <BuildingView
{...props} {...props}
{...this.state.building} {...this.state.building}
user={this.state.user} user={this.state.user}
/> />
) } /> ) } />
<Route exact path="/building/:building/edit.html" render={(props) => ( <Route exact path="/edit/:cat/building/:building.html" render={(props) => (
<BuildingEdit <BuildingEdit
{...props} {...props}
{...this.state.building} {...this.state.building}
@ -106,7 +113,7 @@ class App extends React.Component {
</CSSTransition> </CSSTransition>
</TransitionGroup> </TransitionGroup>
<Switch> <Switch>
<Route exact path="/(select.html|map.*|building.*)?" render={(props) => ( <Route exact path="/(edit.*|view.*)?" render={(props) => (
<ColouringMap <ColouringMap
{...props} {...props}
building={this.state.building} building={this.state.building}

View File

@ -1,17 +0,0 @@
import React from 'react';
import Sidebar from './sidebar';
import InfoBox from './info-box';
import { Redirect } from 'react-router-dom';
const BuildingEditAny = (props) => {
if (!props.user){
return <Redirect to="/sign-up.html" />
}
return (
<Sidebar title="Edit data">
<InfoBox msg="Select a building to edit by clicking on the map&hellip;" />
</Sidebar>
);
}
export default BuildingEditAny;

View File

@ -1,6 +1,5 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { Link, NavLink, Redirect } from 'react-router-dom'; import { Link, NavLink, Redirect } from 'react-router-dom';
import queryString from 'query-string';
import ErrorBox from './error-box'; import ErrorBox from './error-box';
import InfoBox from './info-box'; import InfoBox from './info-box';
@ -26,23 +25,32 @@ const BuildingEdit = (props) => {
); );
} }
const search = (props.location && props.location.search)? const cat = get_cat(props.match.url);
queryString.parse(props.location.search):
{};
return ( return (
<Sidebar title={`You are editing`} <Sidebar
back={search.cat? `/building/${props.building_id}.html?cat=${search.cat}`: `/building//${props.building_id}.html`}> key={props.building_id}
title={`You are editing`}
back={`/edit/${cat}.html`}>
{ {
CONFIG.map((conf_props) => { CONFIG.map((conf_props) => {
return <EditForm return <EditForm
{...conf_props} {...props} {...conf_props} {...props}
search={search} key={conf_props.slug} /> cat={cat} key={conf_props.slug} />
}) })
} }
</Sidebar> </Sidebar>
); );
} }
function get_cat(url) {
if (url === "/") {
return "age"
}
const matches = /^\/(view|edit)\/([^\/.]+)/.exec(url);
const cat = (matches && matches.length > 2)? matches[2] : "age";
return cat;
}
class EditForm extends Component { class EditForm extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -116,7 +124,7 @@ class EditForm extends Component {
this.setState({error: res.error}) this.setState({error: res.error})
} else { } else {
this.props.selectBuilding(res); this.props.selectBuilding(res);
const new_cat = this.props.search.cat; const new_cat = this.props.cat;
this.props.history.push(`/building/${res.building_id}.html?cat=${new_cat}`); this.props.history.push(`/building/${res.building_id}.html?cat=${new_cat}`);
} }
}.bind(this)).catch( }.bind(this)).catch(
@ -125,14 +133,17 @@ class EditForm extends Component {
} }
render() { render() {
const match = this.props.search.cat === this.props.slug; const match = this.props.cat === this.props.slug;
if (!match) {
return null
}
return ( return (
<section className={(this.props.inactive)? "data-section inactive": "data-section"}> <section className={(this.props.inactive)? "data-section inactive": "data-section"}>
<header className={(match? "active " : "") + " section-header edit"}> <header className={(match? "active " : "") + " section-header edit"}>
<a><h3 className="h3">{this.props.title}</h3></a> <NavLink
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}
title={(this.props.inactive)? 'Coming soon… Click the ? for more info.' :
(match)? 'Hide details' : 'Show details'}
isActive={() => match}>
<h3 className="h3">{this.props.title}</h3>
</NavLink>
<nav className="icon-buttons"> <nav className="icon-buttons">
{ {
this.props.help? this.props.help?
@ -142,31 +153,34 @@ class EditForm extends Component {
: null : null
} }
{ {
(this.props.slug === 'like')? // special-case for likes (match && this.props.slug === 'like')? // special-case for likes
<NavLink className="icon-button save" title="Done" <NavLink className="icon-button save" title="Done"
to={`/building/${this.props.building_id}.html?cat=${this.props.slug}`}> to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
Done Done
<SaveIcon /> <SaveIcon />
</NavLink> </NavLink>
: :
match? (
<Fragment> <Fragment>
<NavLink className="icon-button save" title="Save Changes" <NavLink className="icon-button save" title="Save Changes"
onClick={this.handleSubmit} onClick={this.handleSubmit}
to={`/building/${this.props.building_id}.html?cat=${this.props.slug}`}> to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
Save Save
<SaveIcon /> <SaveIcon />
</NavLink> </NavLink>
<NavLink className="icon-button close-edit" title="Cancel" <NavLink className="icon-button close-edit" title="Cancel"
to={`/building/${this.props.building_id}.html?cat=${this.props.slug}`}> to={`/view/${this.props.slug}/building/${this.props.building_id}.html`}>
Cancel Cancel
<CloseIcon /> <CloseIcon />
</NavLink> </NavLink>
</Fragment> </Fragment>
): null
} }
</nav> </nav>
</header> </header>
{
<form action={`/building/${this.props.building_id}.html?cat=${this.props.slug}`} match? (
<form action={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}
method="GET" onSubmit={this.handleSubmit}> method="GET" onSubmit={this.handleSubmit}>
<ErrorBox msg={this.state.error} /> <ErrorBox msg={this.state.error} />
{ {
@ -216,6 +230,8 @@ class EditForm extends Component {
</div> </div>
} }
</form> </form>
) : null
}
</section> </section>
) )
} }

View File

@ -1,6 +1,5 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import queryString from 'query-string';
import Sidebar from './sidebar'; import Sidebar from './sidebar';
import Tooltip from './tooltip'; import Tooltip from './tooltip';
@ -16,18 +15,18 @@ const BuildingView = (props) => {
<Sidebar title="Building Not Found"> <Sidebar title="Building Not Found">
<InfoBox msg="We can't find that one anywhere - try the map again?" /> <InfoBox msg="We can't find that one anywhere - try the map again?" />
<div className="buttons-container with-space"> <div className="buttons-container with-space">
<Link to="/map/age.html" className="btn btn-secondary">Back to maps</Link> <Link to="/view/age.html" className="btn btn-secondary">Back to maps</Link>
</div> </div>
</Sidebar> </Sidebar>
); );
} }
const search = (props.location && props.location.search)? queryString.parse(props.location.search): {}; const cat = get_cat(props.match.url);
return ( return (
<Sidebar title={`Data available for this building`} back={search.cat? `/map/${search.cat}.html` : "/map/age.html"}> <Sidebar title={`Data available for this building`} back={`/view/${cat}.html`}>
{ {
CONFIG.map(section_props => ( CONFIG.map(section_props => (
<DataSection <DataSection
key={section_props.slug} search={search} key={section_props.slug} cat={cat}
building_id={props.building_id} building_id={props.building_id}
{...section_props}> {...section_props}>
{ {
@ -54,13 +53,23 @@ const BuildingView = (props) => {
} }
function get_cat(url) {
if (url === "/") {
return "age"
}
const matches = /^\/(view|edit)\/([^\/.]+)/.exec(url);
const cat = (matches && matches.length > 2)? matches[2] : "age";
return cat;
}
const DataSection = (props) => { const DataSection = (props) => {
const match = props.search.cat === props.slug; const match = props.cat === props.slug;
return ( return (
<section id={props.slug} className={(props.inactive)? "data-section inactive": "data-section"}> <section id={props.slug} className={(props.inactive)? "data-section inactive": "data-section"}>
<header className={(match? "active " : "") + " section-header view"}> <header className={(match? "active " : "") + " section-header view"}>
<NavLink <NavLink
to={`/building/${props.building_id}.html` + ((match)? '': `?cat=${props.slug}`)} to={`/view/${props.slug}/building/${props.building_id}.html`}
title={(props.inactive)? 'Coming soon… Click the ? for more info.' : title={(props.inactive)? 'Coming soon… Click the ? for more info.' :
(match)? 'Hide details' : 'Show details'} (match)? 'Hide details' : 'Show details'}
isActive={() => match}> isActive={() => match}>
@ -77,7 +86,7 @@ const DataSection = (props) => {
{ {
!props.inactive? !props.inactive?
<NavLink className="icon-button edit" title="Edit data" <NavLink className="icon-button edit" title="Edit data"
to={`/building/${props.building_id}/edit.html?cat=${props.slug}`}> to={`/edit/${props.slug}/building/${props.building_id}.html`}>
Edit Edit
<EditIcon /> <EditIcon />
</NavLink> </NavLink>

View File

@ -19,10 +19,10 @@ const Header = (props) => (
<a className="nav-link" href="https://pages.colouring.london/about">More about</a> <a className="nav-link" href="https://pages.colouring.london/about">More about</a>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<NavLink to="/map/age.html" className="nav-link">View Maps</NavLink> <NavLink to="/view/age.html" className="nav-link">View Maps</NavLink>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<NavLink to="/select.html" className="nav-link">Add/Edit Data</NavLink> <NavLink to="/edit/age.html" className="nav-link">Add/Edit Data</NavLink>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/buildingcategories">Building Categories</a> <a className="nav-link" href="https://pages.colouring.london/buildingcategories">Building Categories</a>

View File

@ -1,163 +1,73 @@
import React, { Fragment } from 'react'; import React from 'react';
import { NavLink } from 'react-router-dom';
import Sidebar from './sidebar';
import { EditIcon } from './icons';
import './legend.css'; import './legend.css';
import CONFIG from './fields-config.json';
import InfoBox from './info-box';
const LEGEND_CONFIG = { const LEGEND_CONFIG = {
location: [ location: [
{ { color: '#f0f9e8', text: '>5' },
title: 'Location Information (number of data entries)', { color: '#bae4bc', text: '4' },
slug: 'location', { color: '#7bccc4', text: '3' },
elements: [ { color: '#43a2ca', text: '2' },
{ color: '#f0f9e8', text: '>5' }, { color: '#0868ac', text: '1' }
{ color: '#bae4bc', text: '4' },
{ color: '#7bccc4', text: '3' },
{ color: '#43a2ca', text: '2' },
{ color: '#0868ac', text: '1' }
]
}
], ],
age: [ age: [
{ { color: '#f0eaba', text: '≥2000' },
title: 'Year Built', { color: '#fae269', text: '19802000' },
slug: 'date_year', { color: '#fbaf27', text: '19601980' },
elements: [ { color: '#e6711d', text: '19401960' },
{ color: '#f0eaba', text: '≥2000' }, { color: '#d73d3a', text: '19201940' },
{ color: '#fae269', text: '19802000' }, { color: '#ba221c', text: '19001920' },
{ color: '#fbaf27', text: '19601980' }, { color: '#bb859b', text: '18801900' },
{ color: '#e6711d', text: '19401960' }, { color: '#8b3654', text: '18601880' },
{ color: '#d73d3a', text: '19201940' }, { color: '#8f5385', text: '18401860' },
{ color: '#ba221c', text: '19001920' }, { color: '#56619b', text: '18201840' },
{ color: '#bb859b', text: '18801900' }, { color: '#6793b2', text: '18001820' },
{ color: '#8b3654', text: '18601880' }, { color: '#83c3b3', text: '17801800' },
{ color: '#8f5385', text: '18401860' }, { color: '#adc88f', text: '17601780' },
{ color: '#56619b', text: '18201840' }, { color: '#83a663', text: '17401760' },
{ color: '#6793b2', text: '18001820' }, { color: '#77852d', text: '17201740' },
{ color: '#83c3b3', text: '17801800' }, { color: '#69814e', text: '17001720' },
{ color: '#adc88f', text: '17601780' }, { color: '#d0c291', text: '16801700' },
{ color: '#83a663', text: '17401760' }, { color: '#918158', text: '16601680' },
{ color: '#77852d', text: '17201740' }, { color: '#7a5732', text: '<1660' },
{ color: '#69814e', text: '17001720' },
{ color: '#d0c291', text: '16801700' },
{ color: '#918158', text: '16601680' },
{ color: '#7a5732', text: '<1660' },
]
}
], ],
size: [ size: [
{ { color: '#ffffcc', text: '≥20' },
title: 'Number of storeys', { color: '#ffeda0', text: '15-20' },
slug: 'size_storeys', { color: '#fed976', text: '1015' },
elements: [ { color: '#feb24c', text: '610' },
{ color: '#ffffcc', text: '≥20' }, { color: '#fd8d3c', text: '5' },
{ color: '#ffeda0', text: '15-20' }, { color: '#fc4e2a', text: '4' },
{ color: '#fed976', text: '1015' }, { color: '#e31a1c', text: '3' },
{ color: '#feb24c', text: '610' }, { color: '#bd0026', text: '2' },
{ color: '#fd8d3c', text: '5' }, { color: '#800026', text: '1' },
{ color: '#fc4e2a', text: '4' },
{ color: '#e31a1c', text: '3' },
{ color: '#bd0026', text: '2' },
{ color: '#800026', text: '1' },
]
}
], ],
like: [ like: [
{ { color: '#f65400', text: 'We like these buildings 👍 🎉 +1' },
title: 'Which buildings do you like?',
slug: 'like',
elements: [
{ color: '#f65400', text: 'We like these buildings 👍 🎉 +1' },
]
}
] ]
}; };
const Legend = (props) => { const Legend = (props) => {
var data_layer = undefined; let elements = LEGEND_CONFIG[props.slug];
if (props.match && props.match.params && props.match.params.map) {
data_layer = props.match.params.map;
}
return ( return (
<Sidebar title="View Maps"> <dl className="data-list">
<InfoBox msg="Click on the map to see more information about a building&hellip;" /> <dt>
{ { props.title }
CONFIG.map((data_group) => ( </dt>
<LegendGroup {...data_group} maps={LEGEND_CONFIG[data_group.slug]} <dd>
data_layer={data_layer} key={data_group.slug} /> <ul className="data-legend">
)) {
} elements.map((data_item) => (
</Sidebar> <LegendItem {...data_item} key={data_item.color} />
))
}
</ul>
</dd>
</dl>
); );
} }
const LegendGroup = (props) => {
const match = props.data_layer === props.slug;
const inactive = props.inactive || !props.maps;
return (
<section className={(inactive? "inactive ": "") + "data-section legend"}>
<header className={(match? "active " : "") + " section-header view"}>
<NavLink
to={match? "/map/base.html": `/map/${props.slug}.html`}
isActive={() => match}
title={(inactive)? 'Coming soon… Click the ? for more info.' :
(match)? '' : 'Show on map'}>
<h3 className="h3">{props.title}</h3>
</NavLink>
<nav className="icon-buttons">
{
props.help?
<a className="icon-button help" href={props.help}>
More info
</a>
: null
}
<NavLink className="icon-button edit" title="Edit data"
to={`/select.html?cat=${props.slug}`}>
Edit
<EditIcon />
</NavLink>
</nav>
</header>
<dl className="data-list">
{
(match && props.maps)?
props.maps.map((data_section) => (
<LegendSection {...data_section} key={data_section.slug}>
{
data_section.elements.map((data_item) => (
<LegendItem {...data_item} key={data_item.color} />
))
}
</LegendSection>
))
: null
}
</dl>
</section>
)
};
const LegendSection = (props) => (
<Fragment>
<dt>
{ props.title }
</dt>
<dd>
<ul className="data-legend">
{ props.children }
</ul>
</dd>
</Fragment>
);
const LegendItem = (props) => ( const LegendItem = (props) => (
<li> <li>
<span className="key" style={ { background: props.color } }>-</span> <span className="key" style={ { background: props.color } }>-</span>

View File

@ -1,6 +1,5 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal'; import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal';
import queryString from 'query-string';
import '../../node_modules/leaflet/dist/leaflet.css' import '../../node_modules/leaflet/dist/leaflet.css'
import './map.css' import './map.css'
@ -26,14 +25,12 @@ class ColouringMap extends Component {
} }
handleClick(e) { handleClick(e) {
if (this.props.match.url.match('edit')){ const is_edit = this.props.match.url.match('edit')
// don't navigate away from edit view const mode = is_edit? 'edit': 'view';
return const lat = e.latlng.lat
} const lng = e.latlng.lng
var lat = e.latlng.lat const new_cat = get_cat(this.props.match.url);
var lng = e.latlng.lng const map_cat = new_cat || 'age';
const is_building = /building/.test(this.props.match.url);
const new_cat = get_cat(is_building, this.props.location, this.props.match.url);
fetch( fetch(
'/buildings/locate?lat='+lat+'&lng='+lng '/buildings/locate?lat='+lat+'&lng='+lng
).then( ).then(
@ -42,12 +39,11 @@ class ColouringMap extends Component {
if (data && data.length){ if (data && data.length){
const building = data[0]; const building = data[0];
this.props.selectBuilding(building); this.props.selectBuilding(building);
this.props.history.push(`/building/${building.building_id}.html`); this.props.history.push(`/${mode}/${map_cat}/building/${building.building_id}.html`);
} else { } else {
// deselect but keep/return to expected colour theme // deselect but keep/return to expected colour theme
this.props.selectBuilding(undefined); this.props.selectBuilding(undefined);
const map_cat = new_cat || 'age'; this.props.history.push(`/${mode}/${map_cat}.html`);
this.props.history.push(`/map/${map_cat}.html`);
} }
}.bind(this)).catch( }.bind(this)).catch(
(err) => console.error(err) (err) => console.error(err)
@ -72,7 +68,7 @@ class ColouringMap extends Component {
// colour-data tiles // colour-data tiles
const is_building = /building/.test(this.props.match.url); const is_building = /building/.test(this.props.match.url);
const cat = get_cat(is_building, this.props.location, this.props.match.url); const cat = get_cat(this.props.match.url);
const tileset_by_cat = { const tileset_by_cat = {
age: 'date_year', age: 'date_year',
size: 'size_storeys', size: 'size_storeys',
@ -122,18 +118,12 @@ class ColouringMap extends Component {
} }
}; };
function get_cat(is_building, location, url) { function get_cat(url) {
if (url === "/") { if (url === "/") {
return "age" return "age"
} }
const search = (location && location.search)? queryString.parse(location.search) : {}; const matches = /^\/(view|edit)\/([^\/.]+)/.exec(url);
var cat, matches; const cat = (matches && matches.length > 2)? matches[2] : "age";
if (is_building) {
cat = search.cat;
} else {
matches = /\/map\/([^.]+).html/.exec(url);
cat = (matches && matches.length > 1)? matches[1] : "";
}
return cat; return cat;
} }

View File

@ -0,0 +1,68 @@
import React from 'react';
import { NavLink, Redirect } from 'react-router-dom';
import Sidebar from './sidebar';
import { EditIcon } from './icons';
import CONFIG from './fields-config.json';
const Overview = (props) => {
var data_layer = 'age'; // always default
if (props.match && props.match.params && props.match.params.cat) {
data_layer = props.match.params.cat;
}
if (props.mode === 'edit' && !props.user){
return <Redirect to="/sign-up.html" />
}
let title = (props.mode === 'view')? 'View maps' : 'Add or edit data';
return (
<Sidebar title={title}>
{
CONFIG.map((data_group) => (
<OverviewSection {...data_group}
data_layer={data_layer} key={data_group.slug} mode={props.mode} />
))
}
</Sidebar>
);
}
const OverviewSection = (props) => {
const match = props.data_layer === props.slug;
const inactive = props.inactive;
return (
<section className={(inactive? "inactive ": "") + "data-section legend"}>
<header className={(match? "active " : "") + " section-header view"}>
<NavLink
to={`/${props.mode}/${props.slug}.html`}
isActive={() => match}
title={(inactive)? 'Coming soon… Click the ? for more info.' :
(match)? '' : 'Show on map'}>
<h3 className="h3">{props.title}</h3>
</NavLink>
<nav className="icon-buttons">
{
props.help?
<a className="icon-button help" href={props.help}>
More info
</a>
: null
}
{
props.mode === 'view'?
<NavLink className="icon-button edit" title="Edit data"
to={`/edit/${props.slug}.html`}>
Edit
<EditIcon />
</NavLink>
: null
}
</nav>
</header>
</section>
)
};
export default Overview;

View File

@ -15,7 +15,7 @@ function strictParseInt(value) {
function parseBuildingURL(url){ function parseBuildingURL(url){
const re = /^\/building\/([^\/]+)(\/edit)?.html/; const re = /\/building\/([^\/]+).html/;
const matches = re.exec(url); const matches = re.exec(url);
if (matches && matches.length >= 2) { if (matches && matches.length >= 2) {