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 ColouringMap from './map';
import Header from './header';
import Legend from './legend';
import Overview from './overview';
import Login from './login';
import MyAccountPage from './my-account';
import SignUp from './signup';
import Welcome from './welcome';
import BuildingEditAny from './building-edit-any';
@ -83,18 +82,26 @@ class App extends React.Component {
<Route exact path="/">
<Welcome />
</Route>
<Route exact path="/select.html">
<BuildingEditAny user={this.state.user} />
</Route>
<Route exact path="/map/:map.html" component={Legend} />
<Route exact path="/building/:building.html" render={(props) => (
<Route exact path="/view/:cat.html" render={(props) => (
<Overview
{...props}
mode='view' user={this.state.user}
/>
) } />
<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
{...props}
{...this.state.building}
user={this.state.user}
/>
) } />
<Route exact path="/building/:building/edit.html" render={(props) => (
<Route exact path="/edit/:cat/building/:building.html" render={(props) => (
<BuildingEdit
{...props}
{...this.state.building}
@ -106,7 +113,7 @@ class App extends React.Component {
</CSSTransition>
</TransitionGroup>
<Switch>
<Route exact path="/(select.html|map.*|building.*)?" render={(props) => (
<Route exact path="/(edit.*|view.*)?" render={(props) => (
<ColouringMap
{...props}
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 { Link, NavLink, Redirect } from 'react-router-dom';
import queryString from 'query-string';
import ErrorBox from './error-box';
import InfoBox from './info-box';
@ -26,23 +25,32 @@ const BuildingEdit = (props) => {
);
}
const search = (props.location && props.location.search)?
queryString.parse(props.location.search):
{};
const cat = get_cat(props.match.url);
return (
<Sidebar title={`You are editing`}
back={search.cat? `/building/${props.building_id}.html?cat=${search.cat}`: `/building//${props.building_id}.html`}>
<Sidebar
key={props.building_id}
title={`You are editing`}
back={`/edit/${cat}.html`}>
{
CONFIG.map((conf_props) => {
return <EditForm
{...conf_props} {...props}
search={search} key={conf_props.slug} />
cat={cat} key={conf_props.slug} />
})
}
</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 {
constructor(props) {
super(props);
@ -116,7 +124,7 @@ class EditForm extends Component {
this.setState({error: res.error})
} else {
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}`);
}
}.bind(this)).catch(
@ -125,14 +133,17 @@ class EditForm extends Component {
}
render() {
const match = this.props.search.cat === this.props.slug;
if (!match) {
return null
}
const match = this.props.cat === this.props.slug;
return (
<section className={(this.props.inactive)? "data-section inactive": "data-section"}>
<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">
{
this.props.help?
@ -142,31 +153,34 @@ class EditForm extends Component {
: 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"
to={`/building/${this.props.building_id}.html?cat=${this.props.slug}`}>
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
Done
<SaveIcon />
</NavLink>
:
match? (
<Fragment>
<NavLink className="icon-button save" title="Save Changes"
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
<SaveIcon />
</NavLink>
<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
<CloseIcon />
</NavLink>
</Fragment>
): null
}
</nav>
</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}>
<ErrorBox msg={this.state.error} />
{
@ -216,6 +230,8 @@ class EditForm extends Component {
</div>
}
</form>
) : null
}
</section>
)
}

View File

@ -1,6 +1,5 @@
import React, { Fragment } from 'react';
import { Link, NavLink } from 'react-router-dom';
import queryString from 'query-string';
import Sidebar from './sidebar';
import Tooltip from './tooltip';
@ -16,18 +15,18 @@ const BuildingView = (props) => {
<Sidebar title="Building Not Found">
<InfoBox msg="We can't find that one anywhere - try the map again?" />
<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>
</Sidebar>
);
}
const search = (props.location && props.location.search)? queryString.parse(props.location.search): {};
const cat = get_cat(props.match.url);
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 => (
<DataSection
key={section_props.slug} search={search}
key={section_props.slug} cat={cat}
building_id={props.building_id}
{...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 match = props.search.cat === props.slug;
const match = props.cat === props.slug;
return (
<section id={props.slug} className={(props.inactive)? "data-section inactive": "data-section"}>
<header className={(match? "active " : "") + " section-header view"}>
<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.' :
(match)? 'Hide details' : 'Show details'}
isActive={() => match}>
@ -77,7 +86,7 @@ const DataSection = (props) => {
{
!props.inactive?
<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
<EditIcon />
</NavLink>

View File

@ -19,10 +19,10 @@ const Header = (props) => (
<a className="nav-link" href="https://pages.colouring.london/about">More about</a>
</li>
<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 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 className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/buildingcategories">Building Categories</a>

View File

@ -1,33 +1,17 @@
import React, { Fragment } from 'react';
import { NavLink } from 'react-router-dom';
import React from 'react';
import Sidebar from './sidebar';
import { EditIcon } from './icons';
import './legend.css';
import CONFIG from './fields-config.json';
import InfoBox from './info-box';
const LEGEND_CONFIG = {
location: [
{
title: 'Location Information (number of data entries)',
slug: 'location',
elements: [
{ color: '#f0f9e8', text: '>5' },
{ color: '#bae4bc', text: '4' },
{ color: '#7bccc4', text: '3' },
{ color: '#43a2ca', text: '2' },
{ color: '#0868ac', text: '1' }
]
}
],
age: [
{
title: 'Year Built',
slug: 'date_year',
elements: [
{ color: '#f0eaba', text: '≥2000' },
{ color: '#fae269', text: '19802000' },
{ color: '#fbaf27', text: '19601980' },
@ -47,14 +31,8 @@ const LEGEND_CONFIG = {
{ color: '#d0c291', text: '16801700' },
{ color: '#918158', text: '16601680' },
{ color: '#7a5732', text: '<1660' },
]
}
],
size: [
{
title: 'Number of storeys',
slug: 'size_storeys',
elements: [
{ color: '#ffffcc', text: '≥20' },
{ color: '#ffeda0', text: '15-20' },
{ color: '#fed976', text: '1015' },
@ -64,99 +42,31 @@ const LEGEND_CONFIG = {
{ color: '#e31a1c', text: '3' },
{ color: '#bd0026', text: '2' },
{ color: '#800026', text: '1' },
]
}
],
like: [
{
title: 'Which buildings do you like?',
slug: 'like',
elements: [
{ color: '#f65400', text: 'We like these buildings 👍 🎉 +1' },
]
}
]
};
const Legend = (props) => {
var data_layer = undefined;
if (props.match && props.match.params && props.match.params.map) {
data_layer = props.match.params.map;
}
let elements = LEGEND_CONFIG[props.slug];
return (
<Sidebar title="View Maps">
<InfoBox msg="Click on the map to see more information about a building&hellip;" />
{
CONFIG.map((data_group) => (
<LegendGroup {...data_group} maps={LEGEND_CONFIG[data_group.slug]}
data_layer={data_layer} key={data_group.slug} />
))
}
</Sidebar>
);
}
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 }
{
elements.map((data_item) => (
<LegendItem {...data_item} key={data_item.color} />
))
}
</ul>
</dd>
</Fragment>
);
</dl>
);
}
const LegendItem = (props) => (
<li>

View File

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