Refactor map app with hooks and separated config
This commit is contained in:
parent
a881b66d76
commit
305f2f1671
@ -9,7 +9,7 @@ describe('<App />', () => {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<App revisionId={0} />
|
<App revisionId="0" />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
div
|
div
|
||||||
);
|
);
|
||||||
|
@ -5,10 +5,10 @@ import 'bootstrap/dist/css/bootstrap.min.css';
|
|||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import { AuthRoute, PrivateRoute } from './route';
|
import { AuthRoute, PrivateRoute } from './route';
|
||||||
import { AuthContext, AuthProvider } from './auth-context';
|
import { AuthProvider } from './auth-context';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import MapApp from './map-app';
|
import { MapApp } from './map-app';
|
||||||
import { Building } from './models/building';
|
import { Building, UserVerified } from './models/building';
|
||||||
import { User } from './models/user';
|
import { User } from './models/user';
|
||||||
import AboutPage from './pages/about';
|
import AboutPage from './pages/about';
|
||||||
import ChangesPage from './pages/changes';
|
import ChangesPage from './pages/changes';
|
||||||
@ -33,8 +33,8 @@ interface AppProps {
|
|||||||
user?: User;
|
user?: User;
|
||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user_verified?: object;
|
user_verified?: UserVerified;
|
||||||
revisionId: number;
|
revisionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,20 +80,14 @@ export const App: React.FC<AppProps> = props => {
|
|||||||
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
||||||
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
||||||
<Route exact path="/history.html" component={ChangesPage} />
|
<Route exact path="/history.html" component={ChangesPage} />
|
||||||
<Route exact path={mapAppPaths} render={(routeProps) => (
|
<Route exact path={mapAppPaths} >
|
||||||
<AuthContext.Consumer>
|
|
||||||
{({user}) =>
|
|
||||||
<MapApp
|
<MapApp
|
||||||
{...routeProps}
|
|
||||||
building={props.building}
|
building={props.building}
|
||||||
building_like={props.building_like}
|
building_like={props.building_like}
|
||||||
user_verified={props.user_verified}
|
user_verified={props.user_verified}
|
||||||
user={user}
|
|
||||||
revisionId={props.revisionId}
|
revisionId={props.revisionId}
|
||||||
/>
|
/>
|
||||||
}
|
</Route>
|
||||||
</AuthContext.Consumer>
|
|
||||||
)} />
|
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
@ -1,30 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useAuth } from '../auth-context';
|
||||||
|
|
||||||
import { Building } from '../models/building';
|
import { categoriesConfig, Category } from '../config/categories-config';
|
||||||
|
import { categoryUiConfig } from '../config/category-ui-config';
|
||||||
|
import { Building, UserVerified } from '../models/building';
|
||||||
|
|
||||||
import BuildingNotFound from './building-not-found';
|
import BuildingNotFound from './building-not-found';
|
||||||
import AgeContainer from './data-containers/age';
|
|
||||||
import CommunityContainer from './data-containers/community';
|
|
||||||
import ConstructionContainer from './data-containers/construction';
|
|
||||||
import DynamicsContainer from './data-containers/dynamics';
|
|
||||||
import LocationContainer from './data-containers/location';
|
|
||||||
import PlanningContainer from './data-containers/planning';
|
|
||||||
import SizeContainer from './data-containers/size';
|
|
||||||
import StreetscapeContainer from './data-containers/streetscape';
|
|
||||||
import SustainabilityContainer from './data-containers/sustainability';
|
|
||||||
import TeamContainer from './data-containers/team';
|
|
||||||
import TypeContainer from './data-containers/type';
|
|
||||||
import UseContainer from './data-containers/use';
|
|
||||||
|
|
||||||
|
|
||||||
interface BuildingViewProps {
|
interface BuildingViewProps {
|
||||||
cat: string;
|
cat: Category;
|
||||||
mode: 'view' | 'edit';
|
mode: 'view' | 'edit';
|
||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user?: any;
|
|
||||||
selectBuilding: (building: Building) => void;
|
|
||||||
user_verified?: any;
|
user_verified?: any;
|
||||||
|
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
||||||
|
onBuildingLikeUpdate: (buildingId: number, updatedData: boolean) => void;
|
||||||
|
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,100 +24,30 @@ interface BuildingViewProps {
|
|||||||
* @param props
|
* @param props
|
||||||
*/
|
*/
|
||||||
const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
|
const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
|
||||||
switch (props.cat) {
|
const { user } = useAuth();
|
||||||
case 'location':
|
const DataContainer = categoryUiConfig[props.cat];
|
||||||
return <LocationContainer
|
|
||||||
{...props}
|
const categoryConfig = categoriesConfig[props.cat];
|
||||||
title="Location"
|
|
||||||
help="https://pages.colouring.london/location"
|
if(categoryConfig == undefined) {
|
||||||
intro="Where are the buildings? Address, location and cross-references."
|
|
||||||
/>;
|
|
||||||
case 'use':
|
|
||||||
return <UseContainer
|
|
||||||
{...props}
|
|
||||||
inactive={false}
|
|
||||||
title="Current Use"
|
|
||||||
intro="How are buildings used, and how does use change over time? Coming soon…"
|
|
||||||
help="https://pages.colouring.london/use"
|
|
||||||
/>;
|
|
||||||
case 'type':
|
|
||||||
return <TypeContainer
|
|
||||||
{...props}
|
|
||||||
inactive={false}
|
|
||||||
title="Type"
|
|
||||||
intro="How were buildings previously used?"
|
|
||||||
help="https://www.pages.colouring.london/buildingtypology"
|
|
||||||
/>;
|
|
||||||
case 'age':
|
|
||||||
return <AgeContainer
|
|
||||||
{...props}
|
|
||||||
title="Age"
|
|
||||||
help="https://pages.colouring.london/age"
|
|
||||||
intro="Building age data can support energy analysis and help predict long-term change."
|
|
||||||
/>;
|
|
||||||
case 'size':
|
|
||||||
return <SizeContainer
|
|
||||||
{...props}
|
|
||||||
title="Size & Shape"
|
|
||||||
intro="How big are buildings?"
|
|
||||||
help="https://pages.colouring.london/shapeandsize"
|
|
||||||
/>;
|
|
||||||
case 'construction':
|
|
||||||
return <ConstructionContainer
|
|
||||||
{...props}
|
|
||||||
title="Construction"
|
|
||||||
intro="How are buildings built?"
|
|
||||||
help="https://pages.colouring.london/construction"
|
|
||||||
/>;
|
|
||||||
case 'team':
|
|
||||||
return <TeamContainer
|
|
||||||
{...props}
|
|
||||||
title="Team"
|
|
||||||
intro="Who built the buildings? Coming soon…"
|
|
||||||
help="https://pages.colouring.london/team"
|
|
||||||
inactive={true}
|
|
||||||
/>;
|
|
||||||
case 'sustainability':
|
|
||||||
return <SustainabilityContainer
|
|
||||||
{...props}
|
|
||||||
title="Sustainability"
|
|
||||||
intro="Are buildings energy efficient?"
|
|
||||||
help="https://pages.colouring.london/sustainability"
|
|
||||||
inactive={false}
|
|
||||||
/>;
|
|
||||||
case 'streetscape':
|
|
||||||
return <StreetscapeContainer
|
|
||||||
{...props}
|
|
||||||
title="Streetscape"
|
|
||||||
intro="What's the building's context? Coming soon…"
|
|
||||||
help="https://pages.colouring.london/streetscape"
|
|
||||||
inactive={true}
|
|
||||||
/>;
|
|
||||||
case 'community':
|
|
||||||
return <CommunityContainer
|
|
||||||
{...props}
|
|
||||||
title="Community"
|
|
||||||
intro="How does this building work for the local community?"
|
|
||||||
help="https://pages.colouring.london/community"
|
|
||||||
/>;
|
|
||||||
case 'planning':
|
|
||||||
return <PlanningContainer
|
|
||||||
{...props}
|
|
||||||
title="Planning"
|
|
||||||
intro="Planning controls relating to protection and reuse."
|
|
||||||
help="https://pages.colouring.london/planning"
|
|
||||||
/>;
|
|
||||||
case 'dynamics':
|
|
||||||
return <DynamicsContainer
|
|
||||||
{...props}
|
|
||||||
title="Dynamics"
|
|
||||||
intro="How has the site of this building changed over time?"
|
|
||||||
help="https://pages.colouring.london/buildingcategories"
|
|
||||||
inactive={true}
|
|
||||||
/>;
|
|
||||||
default:
|
|
||||||
return <BuildingNotFound mode="view" />;
|
return <BuildingNotFound mode="view" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
aboutUrl,
|
||||||
|
intro,
|
||||||
|
inactive = false
|
||||||
|
} = categoryConfig;
|
||||||
|
|
||||||
|
return <DataContainer
|
||||||
|
{...props}
|
||||||
|
title={name}
|
||||||
|
help={aboutUrl}
|
||||||
|
intro={intro}
|
||||||
|
inactive={inactive}
|
||||||
|
user={user}
|
||||||
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BuildingView;
|
export default BuildingView;
|
||||||
|
@ -4,6 +4,7 @@ import { CategoryLink } from './category-link';
|
|||||||
import { ListWrapper } from '../components/list-wrapper';
|
import { ListWrapper } from '../components/list-wrapper';
|
||||||
|
|
||||||
import './categories.css';
|
import './categories.css';
|
||||||
|
import { categoriesOrder, categoriesConfig } from '../config/categories-config';
|
||||||
|
|
||||||
interface CategoriesProps {
|
interface CategoriesProps {
|
||||||
mode: 'view' | 'edit' | 'multi-edit';
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
@ -12,102 +13,24 @@ interface CategoriesProps {
|
|||||||
|
|
||||||
const Categories: React.FC<CategoriesProps> = (props) => (
|
const Categories: React.FC<CategoriesProps> = (props) => (
|
||||||
<ListWrapper className='data-category-list'>
|
<ListWrapper className='data-category-list'>
|
||||||
<CategoryLink
|
{categoriesOrder.map(category => {
|
||||||
title="Location"
|
const {
|
||||||
slug="location"
|
name,
|
||||||
help="https://pages.colouring.london/location"
|
slug,
|
||||||
inactive={false}
|
aboutUrl,
|
||||||
mode={props.mode}
|
inactive = false
|
||||||
building_id={props.building_id}
|
} = categoriesConfig[category];
|
||||||
/>
|
|
||||||
<CategoryLink
|
return <CategoryLink
|
||||||
title="Current Use"
|
key={category}
|
||||||
slug="use"
|
title={name}
|
||||||
help="https://pages.colouring.london/use"
|
slug={slug}
|
||||||
inactive={true}
|
help={aboutUrl}
|
||||||
mode={props.mode}
|
inactive={inactive}
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Type"
|
|
||||||
slug="type"
|
|
||||||
help="https://pages.colouring.london/buildingtypology"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Age"
|
|
||||||
slug="age"
|
|
||||||
help="https://pages.colouring.london/age"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Size & Shape"
|
|
||||||
slug="size"
|
|
||||||
help="https://pages.colouring.london/shapeandsize"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Construction"
|
|
||||||
slug="construction"
|
|
||||||
help="https://pages.colouring.london/construction"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Streetscape"
|
|
||||||
slug="streetscape"
|
|
||||||
help="https://pages.colouring.london/greenery"
|
|
||||||
inactive={true}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Team"
|
|
||||||
slug="team"
|
|
||||||
help="https://pages.colouring.london/team"
|
|
||||||
inactive={true}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Planning"
|
|
||||||
slug="planning"
|
|
||||||
help="https://pages.colouring.london/planning"
|
|
||||||
inactive={true}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Sustainability"
|
|
||||||
slug="sustainability"
|
|
||||||
help="https://pages.colouring.london/sustainability"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Dynamics"
|
|
||||||
slug="dynamics"
|
|
||||||
help="https://pages.colouring.london/dynamics"
|
|
||||||
inactive={true}
|
|
||||||
mode={props.mode}
|
|
||||||
building_id={props.building_id}
|
|
||||||
/>
|
|
||||||
<CategoryLink
|
|
||||||
title="Community"
|
|
||||||
slug="community"
|
|
||||||
help="https://pages.colouring.london/community"
|
|
||||||
inactive={false}
|
|
||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
building_id={props.building_id}
|
building_id={props.building_id}
|
||||||
/>
|
/>
|
||||||
|
})}
|
||||||
</ListWrapper>
|
</ListWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
|
import { Category } from '../../config/categories-config';
|
||||||
|
|
||||||
|
|
||||||
interface LikeDataEntryProps {
|
interface LikeDataEntryProps {
|
||||||
@ -19,7 +20,7 @@ const LikeDataEntry: React.FunctionComponent<LikeDataEntryProps> = (props) => {
|
|||||||
<Tooltip text="People who like the building and think it contributes to the city." />
|
<Tooltip text="People who like the building and think it contributes to the city." />
|
||||||
<div className="icon-buttons">
|
<div className="icon-buttons">
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/multi-edit/like?data=${data_string}`}
|
to={`/multi-edit/${Category.Community}?data=${data_string}`}
|
||||||
className="icon-button like">
|
className="icon-button like">
|
||||||
Like more
|
Like more
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import Verification from './verification';
|
import Verification from './verification';
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import { CopyProps } from '../data-containers/category-view-props';
|
import { CopyProps } from '../data-containers/category-view-props';
|
||||||
|
|
||||||
import NumericDataEntry from './numeric-data-entry';
|
import NumericDataEntry from './numeric-data-entry';
|
||||||
|
@ -6,7 +6,7 @@ import { apiPost } from '../apiHelpers';
|
|||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import { compareObjects } from '../helpers';
|
import { compareObjects } from '../helpers';
|
||||||
import { Building } from '../models/building';
|
import { Building, UserVerified } from '../models/building';
|
||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
|
|
||||||
import ContainerHeader from './container-header';
|
import ContainerHeader from './container-header';
|
||||||
@ -26,7 +26,9 @@ interface DataContainerProps {
|
|||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user_verified?: any;
|
user_verified?: any;
|
||||||
selectBuilding: (building: Building) => void;
|
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
||||||
|
onBuildingLikeUpdate: (buildingId: number, updatedData: boolean) => void;
|
||||||
|
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataContainerState {
|
interface DataContainerState {
|
||||||
@ -38,6 +40,8 @@ interface DataContainerState {
|
|||||||
buildingEdits: Partial<Building>;
|
buildingEdits: Partial<Building>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataContainerType = React.ComponentType<DataContainerProps>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared functionality for view/edit forms
|
* Shared functionality for view/edit forms
|
||||||
*
|
*
|
||||||
@ -46,7 +50,7 @@ interface DataContainerState {
|
|||||||
*
|
*
|
||||||
* @param WrappedComponent
|
* @param WrappedComponent
|
||||||
*/
|
*/
|
||||||
const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>) => {
|
const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContainerType = (WrappedComponent: React.ComponentType<CategoryViewProps>) => {
|
||||||
return class DataContainer extends React.Component<DataContainerProps, DataContainerState> {
|
return class DataContainer extends React.Component<DataContainerProps, DataContainerState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -174,8 +178,9 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.setState({error: data.error});
|
this.setState({error: data.error});
|
||||||
} else {
|
} else {
|
||||||
this.props.selectBuilding(data);
|
// like endpoint returns whole building data so we can update both
|
||||||
this.updateBuildingState('likes_total', data.likes_total);
|
this.props.onBuildingUpdate(this.props.building.building_id, data);
|
||||||
|
this.props.onBuildingLikeUpdate(this.props.building.building_id, like);
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.setState({error: err});
|
this.setState({error: err});
|
||||||
@ -195,7 +200,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.setState({error: data.error});
|
this.setState({error: data.error});
|
||||||
} else {
|
} else {
|
||||||
this.props.selectBuilding(data);
|
this.props.onBuildingUpdate(this.props.building.building_id, data);
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.setState({error: err});
|
this.setState({error: err});
|
||||||
@ -227,7 +232,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
zIndex: 2000
|
zIndex: 2000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.props.selectBuilding(this.props.building);
|
this.props.onUserVerifiedUpdate(this.props.building.building_id, data);
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this.setState({error: err});
|
this.setState({error: err});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry';
|
import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Building } from '../../models/building';
|
||||||
|
|
||||||
interface CopyProps {
|
interface CopyProps {
|
||||||
copying: boolean;
|
copying: boolean;
|
||||||
toggleCopying: () => void;
|
toggleCopying: () => void;
|
||||||
@ -7,7 +9,7 @@ interface CopyProps {
|
|||||||
|
|
||||||
interface CategoryViewProps {
|
interface CategoryViewProps {
|
||||||
intro: string;
|
intro: string;
|
||||||
building: any; // TODO: add Building type with all fields
|
building: Building;
|
||||||
building_like: boolean;
|
building_like: boolean;
|
||||||
mode: 'view' | 'edit' | 'multi-edit';
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import InfoBox from '../../components/info-box';
|
import InfoBox from '../../components/info-box';
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import UPRNsDataEntry from '../data-components/uprns-data-entry';
|
import UPRNsDataEntry from '../data-components/uprns-data-entry';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import InfoBox from '../../components/info-box';
|
import InfoBox from '../../components/info-box';
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
import Verification from '../data-components/verification';
|
import Verification from '../data-components/verification';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import InfoBox from '../../components/info-box';
|
import InfoBox from '../../components/info-box';
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../config/data-fields-config';
|
||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry';
|
import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
@ -3,7 +3,8 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import './building-edit-summary.css';
|
import './building-edit-summary.css';
|
||||||
|
|
||||||
import { Category, DataFieldDefinition, dataFields } from '../../data_fields';
|
import { Category } from '../../config/categories-config';
|
||||||
|
import { DataFieldDefinition, dataFields } from '../../config/data-fields-config';
|
||||||
import { arrayToDictionary, parseDate } from '../../helpers';
|
import { arrayToDictionary, parseDate } from '../../helpers';
|
||||||
import { EditHistoryEntry } from '../../models/edit-history-entry';
|
import { EditHistoryEntry } from '../../models/edit-history-entry';
|
||||||
|
|
||||||
@ -30,11 +31,15 @@ function enrichHistoryEntries(forwardPatch: object, reversePatch: object) {
|
|||||||
return Object
|
return Object
|
||||||
.entries(forwardPatch)
|
.entries(forwardPatch)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const info = dataFields[key] || {} as DataFieldDefinition;
|
const {
|
||||||
|
title = `Unknown field (${key})`,
|
||||||
|
category = undefined
|
||||||
|
} = dataFields[key] as DataFieldDefinition ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: info.title || `Unknown field (${key})`,
|
title,
|
||||||
category: info.category || Category.Unknown,
|
category,
|
||||||
value: value,
|
value,
|
||||||
oldValue: reversePatch && reversePatch[key]
|
oldValue: reversePatch && reversePatch[key]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -65,6 +70,7 @@ const BuildingEditSummary: React.FunctionComponent<BuildingEditSummaryProps> = (
|
|||||||
{
|
{
|
||||||
Object.entries(entriesByCategory).map(([category, fields]) =>
|
Object.entries(entriesByCategory).map(([category, fields]) =>
|
||||||
<CategoryEditSummary
|
<CategoryEditSummary
|
||||||
|
key={category}
|
||||||
category={category as keyof typeof Category} // https://github.com/microsoft/TypeScript/issues/14106
|
category={category as keyof typeof Category} // https://github.com/microsoft/TypeScript/issues/14106
|
||||||
fields={fields}
|
fields={fields}
|
||||||
hyperlinkCategory={hyperlinkCategories}
|
hyperlinkCategory={hyperlinkCategories}
|
||||||
|
@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import './category-edit-summary.css';
|
import './category-edit-summary.css';
|
||||||
|
|
||||||
import { categories, Category } from '../../data_fields';
|
import { categoriesConfig, Category } from '../../config/categories-config';
|
||||||
|
|
||||||
import { FieldEditSummary } from './field-edit-summary';
|
import { FieldEditSummary } from './field-edit-summary';
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ interface CategoryEditSummaryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CategoryEditSummary: React.FunctionComponent<CategoryEditSummaryProps> = props => {
|
const CategoryEditSummary: React.FunctionComponent<CategoryEditSummaryProps> = props => {
|
||||||
const category = Category[props.category];
|
const {
|
||||||
const categoryInfo = categories[category] || {name: undefined, slug: undefined};
|
name: categoryName = 'Unknown category',
|
||||||
const categoryName = categoryInfo.name || 'Unknown category';
|
slug: categorySlug = 'categories'
|
||||||
const categorySlug = categoryInfo.slug || 'categories';
|
} = categoriesConfig[props.category] ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='edit-history-category-summary'>
|
<div className='edit-history-category-summary'>
|
||||||
|
@ -1,80 +1,54 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { parseJsonOrDefault } from '../../helpers';
|
import { useMultiEditData } from '../hooks/use-multi-edit-data';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import { dataFields } from '../data_fields';
|
import { dataFields } from '../config/data-fields-config';
|
||||||
import { User } from '../models/user';
|
|
||||||
|
|
||||||
import DataEntry from './data-components/data-entry';
|
import DataEntry from './data-components/data-entry';
|
||||||
import Sidebar from './sidebar';
|
import { Category } from '../config/categories-config';
|
||||||
import Categories from './categories';
|
|
||||||
|
|
||||||
interface MultiEditProps {
|
interface MultiEditProps {
|
||||||
user?: User;
|
|
||||||
category: string;
|
category: string;
|
||||||
dataString: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultiEdit: React.FC<MultiEditProps> = (props) => {
|
const MultiEdit: React.FC<MultiEditProps> = (props) => {
|
||||||
if (props.category === 'like') {
|
const [data, error] = useMultiEditData();
|
||||||
// special case for likes
|
|
||||||
return (
|
|
||||||
<Sidebar>
|
|
||||||
<Categories mode={'view'} />
|
|
||||||
<section className='data-section'>
|
|
||||||
<header className={`section-header view ${props.category} background-${props.category}`}>
|
|
||||||
<h2 className="h2">Like me!</h2>
|
|
||||||
</header>
|
|
||||||
<form className='buttons-container'>
|
|
||||||
<InfoBox msg='Click all the buildings that you like and think contribute to the city!' />
|
|
||||||
|
|
||||||
<Link to='/view/like' className='btn btn-secondary'>Back to view</Link>
|
const isLike = props.category === Category.Community;
|
||||||
<Link to='/edit/like' className='btn btn-secondary'>Back to edit</Link>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = parseJsonOrDefault(props.dataString);
|
|
||||||
|
|
||||||
let error: string;
|
|
||||||
if(data == null) {
|
|
||||||
error = 'Invalid parameters supplied';
|
|
||||||
data = {};
|
|
||||||
} else if(Object.values(data).some(x => x == undefined)) {
|
|
||||||
error = 'Cannot copy empty values';
|
|
||||||
data = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
|
||||||
<Categories mode={'view'} />
|
|
||||||
<section className='data-section'>
|
<section className='data-section'>
|
||||||
<header className={`section-header view ${props.category} background-${props.category}`}>
|
<header className={`section-header view ${props.category} background-${props.category}`}>
|
||||||
<h2 className="h2">Copy {props.category} data</h2>
|
<h2 className="h2">{
|
||||||
|
isLike ?
|
||||||
|
<>Like Me!</> :
|
||||||
|
<>Copy {props.category} data</>
|
||||||
|
}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div className="section-body">
|
<div className="section-body">
|
||||||
<form>
|
<form>
|
||||||
{
|
{
|
||||||
error ?
|
error ?
|
||||||
<ErrorBox msg={error} /> :
|
<ErrorBox msg={error} /> :
|
||||||
<InfoBox msg='Click buildings one at a time to colour using the data below' />
|
<InfoBox msg={
|
||||||
|
isLike ?
|
||||||
|
'Click all the buildings that you like and think contribute to the city!' :
|
||||||
|
'Click buildings one at a time to colour using the data below'
|
||||||
|
} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
Object.keys(data).map((key => {
|
!isLike && data &&
|
||||||
const info = dataFields[key] || {};
|
Object.keys(data).map((key => (
|
||||||
return (
|
|
||||||
<DataEntry
|
<DataEntry
|
||||||
title={info.title || `Unknown field (${key})`}
|
key={key}
|
||||||
|
title={dataFields[key]?.title ?? `Unknown field (${key})`}
|
||||||
slug={key}
|
slug={key}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
value={data[key]}
|
value={data[key]}
|
||||||
/>
|
/>
|
||||||
);
|
)))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
<form className='buttons-container'>
|
<form className='buttons-container'>
|
||||||
@ -83,7 +57,6 @@ const MultiEdit: React.FC<MultiEditProps> = (props) => {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Sidebar>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface ErrorBoxProps {
|
interface ErrorBoxProps {
|
||||||
msg: string;
|
msg: string;
|
||||||
@ -8,22 +8,16 @@ const ErrorBox: React.FC<ErrorBoxProps> = (props) => {
|
|||||||
if (props.msg) {
|
if (props.msg) {
|
||||||
console.error(props.msg);
|
console.error(props.msg);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Fragment>
|
return props.msg ?
|
||||||
{
|
|
||||||
(props.msg)?
|
|
||||||
(
|
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
{
|
{
|
||||||
typeof props.msg === 'string' ?
|
typeof props.msg === 'string' ?
|
||||||
props.msg
|
props.msg
|
||||||
: 'Unexpected error'
|
: 'Unexpected error'
|
||||||
}
|
}
|
||||||
</div>
|
</div> :
|
||||||
) : null
|
null;
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ErrorBox;
|
export default ErrorBox;
|
||||||
|
124
app/src/frontend/config/categories-config.ts
Normal file
124
app/src/frontend/config/categories-config.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* An enumeration of all categories in the system.
|
||||||
|
* The string value is also the category URL slug.
|
||||||
|
*/
|
||||||
|
export enum Category {
|
||||||
|
Location = 'location',
|
||||||
|
LandUse = 'use',
|
||||||
|
Type = 'type',
|
||||||
|
Age = 'age',
|
||||||
|
SizeShape = 'size',
|
||||||
|
Construction = 'construction',
|
||||||
|
Streetscape = 'streetscape',
|
||||||
|
Team = 'team',
|
||||||
|
Planning = 'planning',
|
||||||
|
Sustainability = 'sustainability',
|
||||||
|
Dynamics = 'dynamics',
|
||||||
|
Community = 'community',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the sole configuration variable that defines the order of the categories
|
||||||
|
* in the category grid. The order in the enum defition or the other configs does
|
||||||
|
* not affect the order of the grid.
|
||||||
|
*/
|
||||||
|
export const categoriesOrder: Category[] = [
|
||||||
|
Category.Location,
|
||||||
|
Category.LandUse,
|
||||||
|
Category.Type,
|
||||||
|
Category.Age,
|
||||||
|
Category.SizeShape,
|
||||||
|
Category.Construction,
|
||||||
|
Category.Streetscape,
|
||||||
|
Category.Team,
|
||||||
|
Category.Planning,
|
||||||
|
Category.Sustainability,
|
||||||
|
Category.Dynamics,
|
||||||
|
Category.Community,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface CategoryDefinition {
|
||||||
|
inactive?: boolean;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
aboutUrl: string;
|
||||||
|
intro: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoriesConfig: {[key in Category]: CategoryDefinition} = {
|
||||||
|
[Category.Age]: {
|
||||||
|
slug: 'age',
|
||||||
|
name: 'Age',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/age',
|
||||||
|
intro: 'Building age data can support energy analysis and help predict long-term change.',
|
||||||
|
},
|
||||||
|
[Category.SizeShape]: {
|
||||||
|
slug: 'size',
|
||||||
|
name: 'Size & Shape',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/shapeandsize',
|
||||||
|
intro: 'How big are buildings?',
|
||||||
|
},
|
||||||
|
[Category.Team]: {
|
||||||
|
inactive: true,
|
||||||
|
slug: 'team',
|
||||||
|
name: 'Team',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/team',
|
||||||
|
intro: 'Who built the buildings? Coming soon…',
|
||||||
|
},
|
||||||
|
[Category.Construction]: {
|
||||||
|
slug: 'construction',
|
||||||
|
name: 'Construction',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/construction',
|
||||||
|
intro: 'How are buildings built?',
|
||||||
|
},
|
||||||
|
[Category.Location]: {
|
||||||
|
slug: 'location',
|
||||||
|
name: 'Location',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/location',
|
||||||
|
intro: 'Where are the buildings? Address, location and cross-references.',
|
||||||
|
},
|
||||||
|
[Category.Community]: {
|
||||||
|
slug: 'community',
|
||||||
|
name: 'Community',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/community',
|
||||||
|
intro: 'How does this building work for the local community?',
|
||||||
|
},
|
||||||
|
[Category.Planning]: {
|
||||||
|
slug: 'planning',
|
||||||
|
name: 'Planning',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/planning',
|
||||||
|
intro: 'Planning controls relating to protection and reuse.',
|
||||||
|
},
|
||||||
|
[Category.Sustainability]: {
|
||||||
|
slug: 'sustainability',
|
||||||
|
name: 'Sustainability',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/sustainability',
|
||||||
|
intro: 'Are buildings energy efficient?',
|
||||||
|
},
|
||||||
|
[Category.Type]: {
|
||||||
|
slug: 'type',
|
||||||
|
name: 'Type',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/buildingtypology',
|
||||||
|
intro: 'How were buildings previously used?',
|
||||||
|
},
|
||||||
|
[Category.LandUse]: {
|
||||||
|
slug: 'use',
|
||||||
|
name: 'Current Use',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/use',
|
||||||
|
intro: 'How are buildings used, and how does use change over time?',
|
||||||
|
},
|
||||||
|
[Category.Streetscape]: {
|
||||||
|
inactive: true,
|
||||||
|
slug: 'streetscape',
|
||||||
|
name: 'Streetscape',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/greenery',
|
||||||
|
intro: "What's the building's context? Coming soon…",
|
||||||
|
},
|
||||||
|
[Category.Dynamics]: {
|
||||||
|
inactive: true,
|
||||||
|
slug: 'dynamics',
|
||||||
|
name: 'Dynamics',
|
||||||
|
aboutUrl: 'https://pages.colouring.london/dynamics',
|
||||||
|
intro: 'How has the site of this building changed over time?'
|
||||||
|
},
|
||||||
|
};
|
195
app/src/frontend/config/category-maps-config.ts
Normal file
195
app/src/frontend/config/category-maps-config.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { Category } from './categories-config';
|
||||||
|
|
||||||
|
export type LegendElement = {
|
||||||
|
color: string;
|
||||||
|
border?: string;
|
||||||
|
text: string;
|
||||||
|
} | {
|
||||||
|
subtitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LegendConfig {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
disclaimer?: string;
|
||||||
|
elements: LegendElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryMapDefinition {
|
||||||
|
mapStyle: string;
|
||||||
|
legend: LegendConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMapCategory = Category.Age;
|
||||||
|
|
||||||
|
export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
||||||
|
[Category.Age]: {
|
||||||
|
mapStyle: 'date_year',
|
||||||
|
legend: {
|
||||||
|
title: 'Age',
|
||||||
|
elements: [
|
||||||
|
{ color: '#fff9b8', text: '>2020' },
|
||||||
|
{ color: '#fae269', text: '2000-2019' },
|
||||||
|
{ color: '#fbaf27', text: '1980-1999' },
|
||||||
|
{ color: '#e6711d', text: '1960-1979' },
|
||||||
|
{ color: '#cc1212', text: '1940-1959' },
|
||||||
|
{ color: '#8f0303', text: '1920-1939' },
|
||||||
|
{ color: '#8f5385', text: '1900-1919' },
|
||||||
|
{ color: '#c3e1eb', text: '1880-1899' },
|
||||||
|
{ color: '#6a9dba', text: '1860-1879' },
|
||||||
|
{ color: '#3b74a3', text: '1840-1859' },
|
||||||
|
{ color: '#95ded8', text: '1820-1839' },
|
||||||
|
{ color: '#68aba5', text: '1800-1819' },
|
||||||
|
{ color: '#acc98f', text: '1750-1799' },
|
||||||
|
{ color: '#6d8a51', text: '1700-1749' },
|
||||||
|
{ color: '#d0c291', text: '<1700' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.SizeShape]: {
|
||||||
|
mapStyle: 'size_height',
|
||||||
|
legend: {
|
||||||
|
title: 'Height to apex',
|
||||||
|
elements: [
|
||||||
|
{ color: '#f7f4f9', text: '0-5.55'},
|
||||||
|
{ color: '#e7e1ef', text: '5.55-7.73'},
|
||||||
|
{ color: '#d4b9da', text: '7.73-11.38'},
|
||||||
|
{ color: '#c994c7', text: '11.38-18.45'},
|
||||||
|
{ color: '#df65b0', text: '18.45-35.05'},
|
||||||
|
{ color: '#e7298a', text: '35.05-89.30'},
|
||||||
|
{ color: '#ce1256', text: '89.30-152'},
|
||||||
|
{ color: '#980043', text: '≥152'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Team]: {
|
||||||
|
mapStyle: undefined,
|
||||||
|
legend: {
|
||||||
|
title: 'Team',
|
||||||
|
elements: []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Construction]: {
|
||||||
|
mapStyle: 'construction_core_material',
|
||||||
|
legend: {
|
||||||
|
title: 'Construction',
|
||||||
|
elements: [
|
||||||
|
{ color: "#96613b", text: "Wood" },
|
||||||
|
{ color: "#ffffe3", text: "Stone" },
|
||||||
|
{ color: "#f5d96b", text: "Brick" },
|
||||||
|
{ color: "#beffe8", text: "Steel" },
|
||||||
|
{ color: "#fca89d", text: "Reinforced Concrete" },
|
||||||
|
{ color: "#5c8970", text: "Other Metal" },
|
||||||
|
{ color: "#b5a859", text: "Other Natural Material" },
|
||||||
|
{ color: "#c48a85", text: "Other Man-Made Material" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Location]: {
|
||||||
|
mapStyle: 'location',
|
||||||
|
legend: {
|
||||||
|
title: 'Location',
|
||||||
|
description: '% data collected',
|
||||||
|
elements: [
|
||||||
|
{ color: '#084081', text: '≥80%' },
|
||||||
|
{ color: '#0868ac', text: '60–80%' },
|
||||||
|
{ color: '#43a2ca', text: '40–60%' },
|
||||||
|
{ color: '#7bccc4', text: '20–40%' },
|
||||||
|
{ color: '#bae4bc', text: '<20%' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Community]: {
|
||||||
|
mapStyle: 'likes',
|
||||||
|
legend: {
|
||||||
|
title: 'Like Me',
|
||||||
|
elements: [
|
||||||
|
{ color: '#bd0026', text: '👍👍👍👍 100+' },
|
||||||
|
{ color: '#e31a1c', text: '👍👍👍 50–99' },
|
||||||
|
{ color: '#fc4e2a', text: '👍👍 20–49' },
|
||||||
|
{ color: '#fd8d3c', text: '👍👍 10–19' },
|
||||||
|
{ color: '#feb24c', text: '👍 3–9' },
|
||||||
|
{ color: '#fed976', text: '👍 2' },
|
||||||
|
{ color: '#ffe8a9', text: '👍 1'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[Category.Planning]: {
|
||||||
|
mapStyle: 'planning_combined',
|
||||||
|
legend: {
|
||||||
|
title: 'Statutory protections',
|
||||||
|
disclaimer: 'All data relating to designated buildings should be checked on the National Heritage List for England or local authority websites where used for planning or development purposes',
|
||||||
|
elements: [
|
||||||
|
{ color: '#95beba', text: 'In conservation area'},
|
||||||
|
{ color: '#c72e08', text: 'Grade I listed'},
|
||||||
|
{ color: '#e75b42', text: 'Grade II* listed'},
|
||||||
|
{ color: '#ffbea1', text: 'Grade II listed'},
|
||||||
|
{ color: '#858ed4', text: 'Locally listed'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Sustainability]: {
|
||||||
|
mapStyle: 'sust_Dec',
|
||||||
|
legend: {
|
||||||
|
title: 'Sustainability',
|
||||||
|
description: 'DEC Rating',
|
||||||
|
elements: [
|
||||||
|
{ color: "#007f3d", text: 'A' },
|
||||||
|
{ color: "#2c9f29", text: 'B' },
|
||||||
|
{ color: "#9dcb3c", text: 'C' },
|
||||||
|
{ color: "#fff200", text: 'D' },
|
||||||
|
{ color: "#f7af1d", text: 'E' },
|
||||||
|
{ color: "#ed6823", text: 'F' },
|
||||||
|
{ color: "#e31d23", text: 'G' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Type]: {
|
||||||
|
mapStyle: 'building_attachment_form',
|
||||||
|
legend: {
|
||||||
|
title: 'Type',
|
||||||
|
elements: [
|
||||||
|
{ color: "#f2a2b9", text: "Detached" },
|
||||||
|
{ color: "#ab8fb0", text: "Semi-Detached" },
|
||||||
|
{ color: "#3891d1", text: "End-Terrace" },
|
||||||
|
{ color: "#226291", text: "Mid-Terrace" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.LandUse]: {
|
||||||
|
mapStyle: 'landuse',
|
||||||
|
legend: {
|
||||||
|
title: 'Land Use',
|
||||||
|
elements: [
|
||||||
|
{ color: '#e5050d', text: 'Mixed Use' },
|
||||||
|
{ subtitle: 'Single use:'},
|
||||||
|
{ color: '#4a54a6', text: 'Residential' },
|
||||||
|
{ color: '#ff8c00', text: 'Retail' },
|
||||||
|
{ color: '#f5f58f', text: 'Industry & Business' },
|
||||||
|
{ color: '#73ccd1', text: 'Community Services' },
|
||||||
|
{ color: '#ffbfbf', text: 'Recreation & Leisure' },
|
||||||
|
{ color: '#b3de69', text: 'Transport' },
|
||||||
|
{ color: '#cccccc', text: 'Utilities & Infrastructure' },
|
||||||
|
{ color: '#898944', text: 'Defence' },
|
||||||
|
{ color: '#fa667d', text: 'Agriculture' },
|
||||||
|
{ color: '#53f5dd', text: 'Minerals' },
|
||||||
|
{ color: '#ffffff', text: 'Vacant & Derelict' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Streetscape]: {
|
||||||
|
mapStyle: undefined,
|
||||||
|
legend: {
|
||||||
|
title: 'Streetscape',
|
||||||
|
elements: []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Category.Dynamics]: {
|
||||||
|
mapStyle: undefined,
|
||||||
|
legend: {
|
||||||
|
title: 'Dynamics',
|
||||||
|
elements: []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
32
app/src/frontend/config/category-ui-config.ts
Normal file
32
app/src/frontend/config/category-ui-config.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Category } from './categories-config';
|
||||||
|
|
||||||
|
import AgeContainer from '../building/data-containers/age';
|
||||||
|
import CommunityContainer from '../building/data-containers/community';
|
||||||
|
import ConstructionContainer from '../building/data-containers/construction';
|
||||||
|
import DynamicsContainer from '../building/data-containers/dynamics';
|
||||||
|
import LocationContainer from '../building/data-containers/location';
|
||||||
|
import PlanningContainer from '../building/data-containers/planning';
|
||||||
|
import SizeContainer from '../building/data-containers/size';
|
||||||
|
import StreetscapeContainer from '../building/data-containers/streetscape';
|
||||||
|
import SustainabilityContainer from '../building/data-containers/sustainability';
|
||||||
|
import TeamContainer from '../building/data-containers/team';
|
||||||
|
import TypeContainer from '../building/data-containers/type';
|
||||||
|
import UseContainer from '../building/data-containers/use';
|
||||||
|
|
||||||
|
import { DataContainerType } from '../building/data-container';
|
||||||
|
|
||||||
|
export const categoryUiConfig: {[key in Category]: DataContainerType} = {
|
||||||
|
[Category.Location]: LocationContainer,
|
||||||
|
[Category.LandUse]: UseContainer,
|
||||||
|
[Category.Type]: TypeContainer,
|
||||||
|
[Category.Age]: AgeContainer,
|
||||||
|
[Category.SizeShape]: SizeContainer,
|
||||||
|
[Category.Construction]: ConstructionContainer,
|
||||||
|
[Category.Streetscape]: StreetscapeContainer,
|
||||||
|
[Category.Team]: TeamContainer,
|
||||||
|
[Category.Planning]: PlanningContainer,
|
||||||
|
[Category.Sustainability]: SustainabilityContainer,
|
||||||
|
[Category.Dynamics]: DynamicsContainer,
|
||||||
|
[Category.Community]: CommunityContainer,
|
||||||
|
};
|
||||||
|
|
@ -1,85 +1,4 @@
|
|||||||
export enum Category {
|
import { Category } from './categories-config';
|
||||||
Location = 'Location',
|
|
||||||
LandUse = 'LandUse',
|
|
||||||
Type = 'Type',
|
|
||||||
Age = 'Age',
|
|
||||||
SizeShape = 'SizeShape',
|
|
||||||
Construction = 'Construction',
|
|
||||||
Streetscape = 'Streetscape',
|
|
||||||
Team = 'Team',
|
|
||||||
Sustainability = 'Sustainability',
|
|
||||||
Community = 'Community',
|
|
||||||
Planning = 'Planning',
|
|
||||||
Like = 'Like',
|
|
||||||
|
|
||||||
Unknown = 'Unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const categories = {
|
|
||||||
[Category.Location]: {
|
|
||||||
slug: 'location',
|
|
||||||
name: 'Location'
|
|
||||||
},
|
|
||||||
[Category.LandUse]: {
|
|
||||||
slug: 'use',
|
|
||||||
name: 'Land Use'
|
|
||||||
},
|
|
||||||
[Category.Type]: {
|
|
||||||
slug: 'type',
|
|
||||||
name: 'Type'
|
|
||||||
},
|
|
||||||
[Category.Age]: {
|
|
||||||
slug: 'age',
|
|
||||||
name: 'Age'
|
|
||||||
},
|
|
||||||
[Category.SizeShape]: {
|
|
||||||
slug: 'size',
|
|
||||||
name: 'Size & Shape'
|
|
||||||
},
|
|
||||||
[Category.Construction]: {
|
|
||||||
slug: 'construction',
|
|
||||||
name: 'Construction'
|
|
||||||
},
|
|
||||||
[Category.Streetscape]: {
|
|
||||||
slug: 'streetscape',
|
|
||||||
name: 'Streetscape'
|
|
||||||
},
|
|
||||||
[Category.Team]: {
|
|
||||||
slug: 'team',
|
|
||||||
name: 'Team'
|
|
||||||
},
|
|
||||||
[Category.Sustainability]: {
|
|
||||||
slug: 'sustainability',
|
|
||||||
name: 'Sustainability'
|
|
||||||
},
|
|
||||||
[Category.Community]: {
|
|
||||||
slug: 'community',
|
|
||||||
name: 'Community'
|
|
||||||
},
|
|
||||||
[Category.Planning]: {
|
|
||||||
slug: 'planning',
|
|
||||||
name: 'Planning'
|
|
||||||
},
|
|
||||||
[Category.Like]: {
|
|
||||||
slug: 'like',
|
|
||||||
name: 'Like Me!'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const categoriesOrder: Category[] = [
|
|
||||||
Category.Location,
|
|
||||||
Category.LandUse,
|
|
||||||
Category.Type,
|
|
||||||
Category.Age,
|
|
||||||
Category.SizeShape,
|
|
||||||
Category.Construction,
|
|
||||||
Category.Streetscape,
|
|
||||||
Category.Team,
|
|
||||||
Category.Sustainability,
|
|
||||||
Category.Community,
|
|
||||||
Category.Planning,
|
|
||||||
Category.Like,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface is used only in code which uses dataFields, not in the dataFields definition itself
|
* This interface is used only in code which uses dataFields, not in the dataFields definition itself
|
||||||
@ -91,42 +10,51 @@ export interface DataFieldDefinition {
|
|||||||
category: Category;
|
category: Category;
|
||||||
title: string;
|
title: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
|
properties?: { [key: string]: DataFieldDefinition};
|
||||||
|
example: any; // the example field is used to automatically determine the type of the properties in the Building interface
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataFields = {
|
export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||||
location_name: {
|
location_name: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Building Name",
|
title: "Building Name",
|
||||||
tooltip: "May not be needed for many buildings.",
|
tooltip: "May not be needed for many buildings.",
|
||||||
|
example: "The Cruciform",
|
||||||
},
|
},
|
||||||
location_number: {
|
location_number: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Building number",
|
title: "Building number",
|
||||||
|
example: 12,
|
||||||
},
|
},
|
||||||
location_street: {
|
location_street: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Street",
|
title: "Street",
|
||||||
|
example: "Gower Street",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
location_line_two: {
|
location_line_two: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Address line 2",
|
title: "Address line 2",
|
||||||
|
example: "Flat 21",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
location_town: {
|
location_town: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Town",
|
title: "Town",
|
||||||
|
example: "London",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
location_postcode: {
|
location_postcode: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Postcode",
|
title: "Postcode",
|
||||||
|
example: "W1W 6TR",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
ref_toid: {
|
ref_toid: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "TOID",
|
title: "TOID",
|
||||||
tooltip: "Ordnance Survey Topography Layer ID (to be filled automatically)",
|
tooltip: "Ordnance Survey Topography Layer ID (to be filled automatically)",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,43 +64,51 @@ export const dataFields = {
|
|||||||
uprns: {
|
uprns: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "UPRNs",
|
title: "UPRNs",
|
||||||
tooltip: "Unique Property Reference Numbers (to be filled automatically)"
|
tooltip: "Unique Property Reference Numbers (to be filled automatically)",
|
||||||
|
example: [{uprn: "", parent_uprn: "" }, {uprn: "", parent_uprn: "" }],
|
||||||
},
|
},
|
||||||
|
|
||||||
ref_osm_id: {
|
ref_osm_id: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "OSM ID",
|
title: "OSM ID",
|
||||||
tooltip: "OpenStreetMap feature ID",
|
tooltip: "OpenStreetMap feature ID",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
location_latitude: {
|
location_latitude: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Latitude",
|
title: "Latitude",
|
||||||
|
example: 12.4564,
|
||||||
},
|
},
|
||||||
location_longitude: {
|
location_longitude: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "Longitude",
|
title: "Longitude",
|
||||||
|
example: 0.12124,
|
||||||
},
|
},
|
||||||
|
|
||||||
current_landuse_group: {
|
current_landuse_group: {
|
||||||
category: Category.LandUse,
|
category: Category.LandUse,
|
||||||
title: "Current Land Use (Group)",
|
title: "Current Land Use (Group)",
|
||||||
tooltip: "Land use Groups as classified by [NLUD](https://www.gov.uk/government/statistics/national-land-use-database-land-use-and-land-cover-classification)"
|
tooltip: "Land use Groups as classified by [NLUD](https://www.gov.uk/government/statistics/national-land-use-database-land-use-and-land-cover-classification)",
|
||||||
|
example: ["", ""],
|
||||||
},
|
},
|
||||||
current_landuse_order: {
|
current_landuse_order: {
|
||||||
category: Category.LandUse,
|
category: Category.LandUse,
|
||||||
title: "Current Land Use (Order)",
|
title: "Current Land Use (Order)",
|
||||||
tooltip: "Land use Order as classified by [NLUD](https://www.gov.uk/government/statistics/national-land-use-database-land-use-and-land-cover-classification)"
|
tooltip: "Land use Order as classified by [NLUD](https://www.gov.uk/government/statistics/national-land-use-database-land-use-and-land-cover-classification)",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
building_attachment_form: {
|
building_attachment_form: {
|
||||||
category: Category.Type,
|
category: Category.Type,
|
||||||
title: "Building configuration (attachment)?",
|
title: "Building configuration (attachment)?",
|
||||||
tooltip: "We have prepopulated these based on their current attachment. A building can either be detached, semi-detached or part of a terrace (middle or end)",
|
tooltip: "We have prepopulated these based on their current attachment. A building can either be detached, semi-detached or part of a terrace (middle or end)",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
date_change_building_use: {
|
date_change_building_use: {
|
||||||
category: Category.Type,
|
category: Category.Type,
|
||||||
title:"When did use change?",
|
title:"When did use change?",
|
||||||
tooltip: "This is the date the building stopped being used for for the function it was built for. I.e. if it was Victorian warehouse which is now an office this would be when it became an office or if it was something before that, maybe a garage then the date that happened",
|
tooltip: "This is the date the building stopped being used for for the function it was built for. I.e. if it was Victorian warehouse which is now an office this would be when it became an office or if it was something before that, maybe a garage then the date that happened",
|
||||||
|
example: 1920,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* original_building_use does not exist in database yet.
|
* original_building_use does not exist in database yet.
|
||||||
@ -182,101 +118,121 @@ export const dataFields = {
|
|||||||
category: Category.Type,
|
category: Category.Type,
|
||||||
title: "Original building use",
|
title: "Original building use",
|
||||||
tooltip: "What was the building originally used for when it was built? I.e. If it was Victorian warehouse which is now an office this would be warehouse",
|
tooltip: "What was the building originally used for when it was built? I.e. If it was Victorian warehouse which is now an office this would be warehouse",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
date_year: {
|
date_year: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Year built (best estimate)"
|
title: "Year built (best estimate)",
|
||||||
|
example: 1924,
|
||||||
},
|
},
|
||||||
date_lower : {
|
date_lower : {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Earliest possible start date",
|
title: "Earliest possible start date",
|
||||||
tooltip: "This should be the earliest year in which building could have started."
|
tooltip: "This should be the earliest year in which building could have started.",
|
||||||
|
example: 1900,
|
||||||
},
|
},
|
||||||
date_upper: {
|
date_upper: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Latest possible start year",
|
title: "Latest possible start year",
|
||||||
tooltip: "This should be the latest year in which building could have started."
|
tooltip: "This should be the latest year in which building could have started.",
|
||||||
|
example: 2000,
|
||||||
},
|
},
|
||||||
facade_year: {
|
facade_year: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Facade year",
|
title: "Facade year",
|
||||||
tooltip: "Best estimate"
|
tooltip: "Best estimate",
|
||||||
|
example: 1900,
|
||||||
},
|
},
|
||||||
date_source: {
|
date_source: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Source of information",
|
title: "Source of information",
|
||||||
tooltip: "Source for the main start date"
|
tooltip: "Source for the main start date",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
date_source_detail: {
|
date_source_detail: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Source details",
|
title: "Source details",
|
||||||
tooltip: "References for date source (max 500 characters)"
|
tooltip: "References for date source (max 500 characters)",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
date_link: {
|
date_link: {
|
||||||
category: Category.Age,
|
category: Category.Age,
|
||||||
title: "Text and Image Links",
|
title: "Text and Image Links",
|
||||||
tooltip: "URL for age and date reference",
|
tooltip: "URL for age and date reference",
|
||||||
|
example: ["", "", ""],
|
||||||
},
|
},
|
||||||
|
|
||||||
size_storeys_core: {
|
size_storeys_core: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Core storeys",
|
title: "Core storeys",
|
||||||
tooltip: "How many storeys between the pavement and start of roof?",
|
tooltip: "How many storeys between the pavement and start of roof?",
|
||||||
|
example: 10,
|
||||||
},
|
},
|
||||||
size_storeys_attic: {
|
size_storeys_attic: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Attic storeys",
|
title: "Attic storeys",
|
||||||
tooltip: "How many storeys above start of roof?",
|
tooltip: "How many storeys above start of roof?",
|
||||||
|
example: 1,
|
||||||
},
|
},
|
||||||
size_storeys_basement: {
|
size_storeys_basement: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Basement storeys",
|
title: "Basement storeys",
|
||||||
tooltip: "How many storeys below pavement level?",
|
tooltip: "How many storeys below pavement level?",
|
||||||
|
example: 1,
|
||||||
},
|
},
|
||||||
size_height_apex: {
|
size_height_apex: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Height to apex (m)",
|
title: "Height to apex (m)",
|
||||||
|
example: 100.5,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_height_eaves: {
|
size_height_eaves: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Height to eaves (m)",
|
title: "Height to eaves (m)",
|
||||||
|
example: 20.33,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_floor_area_ground: {
|
size_floor_area_ground: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Ground floor area (m²)",
|
title: "Ground floor area (m²)",
|
||||||
|
example: 1245.6,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_floor_area_total: {
|
size_floor_area_total: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Total floor area (m²)",
|
title: "Total floor area (m²)",
|
||||||
|
example: 2001.7,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_width_frontage: {
|
size_width_frontage: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Frontage Width (m)",
|
title: "Frontage Width (m)",
|
||||||
|
example: 12.2,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_plot_area_total: {
|
size_plot_area_total: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Total area of plot (m²)",
|
title: "Total area of plot (m²)",
|
||||||
|
example: 123.02,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_far_ratio: {
|
size_far_ratio: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "FAR ratio (percentage of plot covered by building)",
|
title: "FAR ratio (percentage of plot covered by building)",
|
||||||
|
example: 0.1,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_configuration: {
|
size_configuration: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Configuration (semi/detached, end/terrace)",
|
title: "Configuration (semi/detached, end/terrace)",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
size_roof_shape: {
|
size_roof_shape: {
|
||||||
category: Category.SizeShape,
|
category: Category.SizeShape,
|
||||||
title: "Roof shape",
|
title: "Roof shape",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -284,155 +240,172 @@ export const dataFields = {
|
|||||||
category: Category.Construction,
|
category: Category.Construction,
|
||||||
title: "Core Material",
|
title: "Core Material",
|
||||||
tooltip:"The main structural material",
|
tooltip:"The main structural material",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
construction_secondary_materials: {
|
construction_secondary_materials: {
|
||||||
category: Category.Construction,
|
category: Category.Construction,
|
||||||
title: "Secondary Construction Material/s",
|
title: "Secondary Construction Material/s",
|
||||||
tooltip:"Other construction materials",
|
tooltip:"Other construction materials",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
construction_roof_covering: {
|
construction_roof_covering: {
|
||||||
category: Category.Construction,
|
category: Category.Construction,
|
||||||
title: "Main Roof Covering",
|
title: "Main Roof Covering",
|
||||||
tooltip:'Main roof covering material',
|
tooltip:'Main roof covering material',
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
sust_breeam_rating: {
|
sust_breeam_rating: {
|
||||||
category: Category.Sustainability,
|
category: Category.Sustainability,
|
||||||
title: "BREEAM Rating",
|
title: "BREEAM Rating",
|
||||||
tooltip: "(Building Research Establishment Environmental Assessment Method) May not be present for many buildings",
|
tooltip: "(Building Research Establishment Environmental Assessment Method) May not be present for many buildings",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
sust_dec: {
|
sust_dec: {
|
||||||
category: Category.Sustainability,
|
category: Category.Sustainability,
|
||||||
title: "DEC Rating",
|
title: "DEC Rating",
|
||||||
tooltip: "(Display Energy Certificate) Any public building should have (and display) a DEC. Showing how the energy use for that building compares to other buildings with same use",
|
tooltip: "(Display Energy Certificate) Any public building should have (and display) a DEC. Showing how the energy use for that building compares to other buildings with same use",
|
||||||
|
example: "G",
|
||||||
},
|
},
|
||||||
sust_aggregate_estimate_epc: {
|
sust_aggregate_estimate_epc: {
|
||||||
category: Category.Sustainability,
|
category: Category.Sustainability,
|
||||||
title: "EPC Rating",
|
title: "EPC Rating",
|
||||||
tooltip: "(Energy Performance Certifcate) Any premises sold or rented is required to have an EPC to show how energy efficient it is. Only buildings rate grade E or higher maybe rented",
|
tooltip: "(Energy Performance Certifcate) Any premises sold or rented is required to have an EPC to show how energy efficient it is. Only buildings rate grade E or higher maybe rented",
|
||||||
|
example: "",
|
||||||
},
|
},
|
||||||
sust_retrofit_date: {
|
sust_retrofit_date: {
|
||||||
category: Category.Sustainability,
|
category: Category.Sustainability,
|
||||||
title: "Last significant retrofit",
|
title: "Last significant retrofit",
|
||||||
tooltip: "Date of last major building refurbishment",
|
tooltip: "Date of last major building refurbishment",
|
||||||
|
example: 1920,
|
||||||
},
|
},
|
||||||
sust_life_expectancy: {
|
sust_life_expectancy: {
|
||||||
category: Category.Sustainability,
|
category: Category.Sustainability,
|
||||||
title: "Expected lifespan for typology",
|
title: "Expected lifespan for typology",
|
||||||
|
example: 123,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
|
||||||
planning_portal_link: {
|
planning_portal_link: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Planning portal link",
|
title: "Planning portal link",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_conservation_area: {
|
planning_in_conservation_area: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "In a conservation area?",
|
title: "In a conservation area?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_conservation_area_name: {
|
planning_conservation_area_name: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Conservation area name",
|
title: "Conservation area name",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_list: {
|
planning_in_list: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is listed on the National Heritage List for England?",
|
title: "Is listed on the National Heritage List for England?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_list_id: {
|
planning_list_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "National Heritage List for England list id",
|
title: "National Heritage List for England list id",
|
||||||
|
example: "121436",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_list_cat: {
|
planning_list_cat: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "National Heritage List for England list type",
|
title: "National Heritage List for England list type",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_list_grade: {
|
planning_list_grade: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Listing grade",
|
title: "Listing grade",
|
||||||
|
example: "II",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_heritage_at_risk_id: {
|
planning_heritage_at_risk_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Heritage at risk list id",
|
title: "Heritage at risk list id",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_world_list_id: {
|
planning_world_list_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "World heritage list id",
|
title: "World heritage list id",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_glher: {
|
planning_in_glher: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "In the Greater London Historic Environment Record?",
|
title: "In the Greater London Historic Environment Record?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_glher_url: {
|
planning_glher_url: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Greater London Historic Environment Record link",
|
title: "Greater London Historic Environment Record link",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_apa: {
|
planning_in_apa: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "In an Architectural Priority Area?",
|
title: "In an Architectural Priority Area?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_apa_name: {
|
planning_apa_name: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Architectural Priority Area name",
|
title: "Architectural Priority Area name",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_apa_tier: {
|
planning_apa_tier: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Architectural Priority Area tier",
|
title: "Architectural Priority Area tier",
|
||||||
|
example: "2",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_local_list: {
|
planning_in_local_list: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is locally listed?",
|
title: "Is locally listed?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_local_list_url: {
|
planning_local_list_url: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Local list link",
|
title: "Local list link",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_in_historic_area_assessment: {
|
planning_in_historic_area_assessment: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Within a historic area assessment?",
|
title: "Within a historic area assessment?",
|
||||||
|
example: true,
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_historic_area_assessment_url: {
|
planning_historic_area_assessment_url: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Historic area assessment link",
|
title: "Historic area assessment link",
|
||||||
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_demolition_proposed: {
|
planning_demolition_proposed: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is the building proposed for demolition?",
|
title: "Is the building proposed for demolition?",
|
||||||
//tooltip: ,
|
example: true,
|
||||||
},
|
|
||||||
planning_demolition_complete: {
|
|
||||||
category: Category.Planning,
|
|
||||||
title: "Has the building been demolished?",
|
|
||||||
//tooltip: ,
|
|
||||||
},
|
|
||||||
planning_demolition_history: {
|
|
||||||
category: Category.Planning,
|
|
||||||
title: "Dates of construction and demolition of previous buildings on site",
|
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
|
||||||
likes_total: {
|
likes_total: {
|
||||||
category: Category.Like,
|
category: Category.Community,
|
||||||
title: "Total number of likes"
|
title: "Total number of likes",
|
||||||
}
|
example: 100,
|
||||||
|
},
|
||||||
};
|
};
|
47
app/src/frontend/hooks/use-building-data.ts
Normal file
47
app/src/frontend/hooks/use-building-data.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Building } from '../models/building';
|
||||||
|
import { apiGet } from '../apiHelpers';
|
||||||
|
|
||||||
|
export function useBuildingData(buildingId: number, preloadedData: Building): [Building, (updatedBuilding: Building) => void, () => void] {
|
||||||
|
const [buildingData, setBuildingData] = useState<Building>(preloadedData);
|
||||||
|
const [isOld, setIsOld] = useState<boolean>(preloadedData == undefined);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if(buildingId == undefined) {
|
||||||
|
setBuildingData(undefined);
|
||||||
|
setIsOld(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [building, buildingUprns] = await Promise.all([
|
||||||
|
apiGet(`/api/buildings/${buildingId}.json`),
|
||||||
|
apiGet(`/api/buildings/${buildingId}/uprns.json`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
building.uprns = buildingUprns.uprns;
|
||||||
|
|
||||||
|
setBuildingData(building);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
// TODO: add UI for API errors
|
||||||
|
}
|
||||||
|
setIsOld(false);
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsOld(true);
|
||||||
|
};
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isOld) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [isOld]);
|
||||||
|
|
||||||
|
const reloadData = useCallback(() => setIsOld(true), []);
|
||||||
|
|
||||||
|
return [buildingData, setBuildingData, reloadData];
|
||||||
|
}
|
43
app/src/frontend/hooks/use-building-like-data.ts
Normal file
43
app/src/frontend/hooks/use-building-like-data.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { apiGet } from '../apiHelpers';
|
||||||
|
|
||||||
|
export function useBuildingLikeData(buildingId: number, preloadedData: boolean): [boolean, (updatedBuildingLike: boolean) => void, () => void] {
|
||||||
|
const [buildingLikeData, setBuildingLikeData] = useState<boolean>(preloadedData);
|
||||||
|
// const [fetchedId, setFetchedId] = useState(preloadedData == undefined ? undefined : buildingId);
|
||||||
|
const [isOld, setIsOld] = useState<boolean>(preloadedData == undefined);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if(buildingId == undefined) {
|
||||||
|
setBuildingLikeData(undefined);
|
||||||
|
setIsOld(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { like } = await apiGet(`/api/buildings/${buildingId}/like.json`);
|
||||||
|
|
||||||
|
setBuildingLikeData(like);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
// TODO: add UI for API errors
|
||||||
|
}
|
||||||
|
setIsOld(false);
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsOld(true);
|
||||||
|
setBuildingLikeData(undefined);
|
||||||
|
};
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isOld) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [isOld]);
|
||||||
|
|
||||||
|
const reloadData = useCallback(() => setIsOld(true), []);
|
||||||
|
|
||||||
|
return [buildingLikeData, setBuildingLikeData, reloadData];
|
||||||
|
}
|
7
app/src/frontend/hooks/use-last-not-empty.ts
Normal file
7
app/src/frontend/hooks/use-last-not-empty.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { usePrevious } from './use-previous';
|
||||||
|
|
||||||
|
export function useLastNotEmpty<T>(value: T): T {
|
||||||
|
const previousValue = usePrevious(value);
|
||||||
|
|
||||||
|
return value ?? previousValue;
|
||||||
|
}
|
27
app/src/frontend/hooks/use-multi-edit-data.ts
Normal file
27
app/src/frontend/hooks/use-multi-edit-data.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useQuery } from './use-query';
|
||||||
|
import { parseJsonOrDefault } from '../../helpers';
|
||||||
|
|
||||||
|
export function useMultiEditData(): [object, string] {
|
||||||
|
const query = useQuery();
|
||||||
|
|
||||||
|
let data: object, error: string;
|
||||||
|
|
||||||
|
const dataString = query.data;
|
||||||
|
if(dataString == undefined) {
|
||||||
|
return [undefined, undefined];
|
||||||
|
}
|
||||||
|
if(Array.isArray(dataString)) {
|
||||||
|
return [undefined, 'Invalid parameters supplied'];
|
||||||
|
}
|
||||||
|
|
||||||
|
data = parseJsonOrDefault(dataString);
|
||||||
|
|
||||||
|
if(data == undefined) {
|
||||||
|
error = 'Invalid parameters supplied';
|
||||||
|
} else if(Object.values(data).some(x => x == undefined)) {
|
||||||
|
error = 'Cannot copy empty values';
|
||||||
|
data = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [data, error];
|
||||||
|
}
|
10
app/src/frontend/hooks/use-previous.ts
Normal file
10
app/src/frontend/hooks/use-previous.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export function usePrevious<T>(value: T) {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
6
app/src/frontend/hooks/use-query.ts
Normal file
6
app/src/frontend/hooks/use-query.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useLocation } from 'react-router';
|
||||||
|
import { parse as parseQuery, ParsedQuery } from 'query-string';
|
||||||
|
|
||||||
|
export function useQuery(): ParsedQuery {
|
||||||
|
return parseQuery(useLocation().search);
|
||||||
|
}
|
33
app/src/frontend/hooks/use-revision.tsx
Normal file
33
app/src/frontend/hooks/use-revision.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { apiGet } from '../apiHelpers';
|
||||||
|
|
||||||
|
export function useRevisionId(initialRevisionId: string): [string, (newId: string) => void, () => void] {
|
||||||
|
const [revisionId, setRevisionId] = useState(initialRevisionId ?? '0');
|
||||||
|
const [isOld, setIsOld] = useState(initialRevisionId == undefined);
|
||||||
|
|
||||||
|
const updateRevisionId = useCallback(
|
||||||
|
(newId: string) => newId != undefined && +newId > +revisionId && setRevisionId(newId),
|
||||||
|
[revisionId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchLatestRevision() {
|
||||||
|
try {
|
||||||
|
const { revision_id: latestRevisionId } = await apiGet(`/api/buildings/revision`);
|
||||||
|
updateRevisionId(latestRevisionId);
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
setIsOld(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isOld) {
|
||||||
|
fetchLatestRevision();
|
||||||
|
}
|
||||||
|
}, [isOld]);
|
||||||
|
|
||||||
|
const reloadRevisionId = useCallback(() => setIsOld(true), []);
|
||||||
|
|
||||||
|
return [revisionId, updateRevisionId, reloadRevisionId];
|
||||||
|
}
|
40
app/src/frontend/hooks/use-user-verified-data.ts
Normal file
40
app/src/frontend/hooks/use-user-verified-data.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { UserVerified } from '../models/building';
|
||||||
|
import { apiGet } from '../apiHelpers';
|
||||||
|
|
||||||
|
export function useUserVerifiedData(buildingId: number, preloadedData: UserVerified): [UserVerified, (updatedBuilding: UserVerified) => void, () => void] {
|
||||||
|
const [userVerifyData, setUserVerifyData] = useState<UserVerified>(preloadedData);
|
||||||
|
const [isOld, setIsOld] = useState<boolean>(preloadedData == undefined);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if(buildingId == undefined) {
|
||||||
|
setUserVerifyData(undefined);
|
||||||
|
setIsOld(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const userVerify = await apiGet(`/api/buildings/${buildingId}/verify.json`);
|
||||||
|
|
||||||
|
setUserVerifyData(userVerify);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
// TODO: add UI for API errors
|
||||||
|
}
|
||||||
|
setIsOld(false);
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsOld(true);
|
||||||
|
}
|
||||||
|
}, [buildingId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isOld) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [isOld])
|
||||||
|
|
||||||
|
return [userVerifyData, setUserVerifyData, () => setIsOld(true)];
|
||||||
|
}
|
@ -1,267 +1,170 @@
|
|||||||
import { parse as parseQuery } from 'query-string';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import React, { Fragment } from 'react';
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { parseJsonOrDefault } from '../helpers';
|
import { useRevisionId } from './hooks/use-revision';
|
||||||
import { strictParseInt } from '../parse';
|
import { useBuildingData } from './hooks/use-building-data';
|
||||||
|
import { useBuildingLikeData } from './hooks/use-building-like-data';
|
||||||
import { apiGet, apiPost } from './apiHelpers';
|
import { useUserVerifiedData } from './hooks/use-user-verified-data';
|
||||||
|
import { useUrlBuildingParam } from './nav/use-url-building-param';
|
||||||
|
import { useUrlCategoryParam } from './nav/use-url-category-param';
|
||||||
|
import { useUrlModeParam } from './nav/use-url-mode-param';
|
||||||
|
import { apiPost } from './apiHelpers';
|
||||||
import BuildingView from './building/building-view';
|
import BuildingView from './building/building-view';
|
||||||
import Categories from './building/categories';
|
import Categories from './building/categories';
|
||||||
import { EditHistory } from './building/edit-history/edit-history';
|
import { EditHistory } from './building/edit-history/edit-history';
|
||||||
import MultiEdit from './building/multi-edit';
|
import MultiEdit from './building/multi-edit';
|
||||||
import Sidebar from './building/sidebar';
|
import Sidebar from './building/sidebar';
|
||||||
import ColouringMap from './map/map';
|
import ColouringMap from './map/map';
|
||||||
import { Building } from './models/building';
|
import { Building, UserVerified } from './models/building';
|
||||||
import Welcome from './pages/welcome';
|
import Welcome from './pages/welcome';
|
||||||
import { PrivateRoute } from './route';
|
import { PrivateRoute } from './route';
|
||||||
|
import { useLastNotEmpty } from './hooks/use-last-not-empty';
|
||||||
|
import { Category } from './config/categories-config';
|
||||||
|
import { defaultMapCategory } from './config/category-maps-config';
|
||||||
|
import { useMultiEditData } from './hooks/use-multi-edit-data';
|
||||||
|
|
||||||
interface MapAppRouteParams {
|
interface MapAppProps {
|
||||||
mode: 'view' | 'edit' | 'multi-edit';
|
|
||||||
category: string;
|
|
||||||
building?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
|
||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user?: any;
|
revisionId?: string;
|
||||||
revisionId?: number;
|
|
||||||
user_verified?: object;
|
user_verified?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapAppState {
|
/** Returns first argument, unless it's equal to the second argument - then returns undefined */
|
||||||
category: string;
|
function unless<V extends string, U extends V>(value: V, unlessValue: U): Exclude<V, U> {
|
||||||
revision_id: number;
|
return value === unlessValue ? undefined : value as Exclude<V, U>;
|
||||||
building: Building;
|
|
||||||
building_like: boolean;
|
|
||||||
user_verified: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
/** Returns the new value, unless it is equal to the current value - then returns undefined */
|
||||||
constructor(props: Readonly<MapAppProps>) {
|
function setOrToggle<T>(currentValue: T, newValue: T): T {
|
||||||
super(props);
|
if(newValue == undefined || newValue === currentValue){
|
||||||
|
return undefined;
|
||||||
this.state = {
|
|
||||||
category: this.getCategory(props.match.params.category),
|
|
||||||
revision_id: props.revisionId || 0,
|
|
||||||
building: props.building,
|
|
||||||
building_like: props.building_like,
|
|
||||||
user_verified: props.user_verified || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectBuilding = this.selectBuilding.bind(this);
|
|
||||||
this.colourBuilding = this.colourBuilding.bind(this);
|
|
||||||
this.increaseRevision = this.increaseRevision.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(props: Readonly<MapAppProps>) {
|
|
||||||
const newCategory = this.getCategory(props.match.params.category);
|
|
||||||
if (newCategory != undefined) {
|
|
||||||
this.setState({ category: newCategory });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.fetchLatestRevision();
|
|
||||||
|
|
||||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
|
||||||
this.fetchBuildingData(strictParseInt(this.props.match.params.building));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchLatestRevision() {
|
|
||||||
try {
|
|
||||||
const {latestRevisionId} = await apiGet(`/api/buildings/revision`);
|
|
||||||
|
|
||||||
this.increaseRevision(latestRevisionId);
|
|
||||||
} catch(error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches building data if a building is selected but no data provided through
|
|
||||||
* props (from server-side rendering)
|
|
||||||
*/
|
|
||||||
async fetchBuildingData(buildingId: number) {
|
|
||||||
try {
|
|
||||||
// TODO: simplify API calls, create helpers for fetching data
|
|
||||||
let [building, building_uprns, building_like, user_verified] = await Promise.all([
|
|
||||||
apiGet(`/api/buildings/${buildingId}.json`),
|
|
||||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
|
||||||
apiGet(`/api/buildings/${buildingId}/like.json`),
|
|
||||||
apiGet(`/api/buildings/${buildingId}/verify.json`)
|
|
||||||
]);
|
|
||||||
|
|
||||||
building.uprns = building_uprns.uprns;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
building: building,
|
|
||||||
building_like: building_like.like,
|
|
||||||
user_verified: user_verified
|
|
||||||
});
|
|
||||||
|
|
||||||
this.increaseRevision(building.revision_id);
|
|
||||||
|
|
||||||
} catch(error) {
|
|
||||||
console.error(error);
|
|
||||||
// TODO: add UI for API errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategory(category: string) {
|
|
||||||
if (category === 'categories') return undefined;
|
|
||||||
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMultiEditDataString(): string {
|
|
||||||
const q = parseQuery(this.props.location.search);
|
|
||||||
if(Array.isArray(q.data)) {
|
|
||||||
throw new Error('Invalid format');
|
|
||||||
} else return q.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
increaseRevision(revisionId) {
|
|
||||||
revisionId = +revisionId;
|
|
||||||
// bump revision id, only ever increasing
|
|
||||||
if (revisionId > this.state.revision_id) {
|
|
||||||
this.setState({ revision_id: revisionId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectBuilding(building: Building) {
|
|
||||||
const mode = this.props.match.params.mode || 'view';
|
|
||||||
const category = this.props.match.params.category || 'age';
|
|
||||||
|
|
||||||
if (building == undefined) {
|
|
||||||
this.setState({ building: undefined });
|
|
||||||
this.props.history.push(`/${mode}/${category}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchBuildingData(building.building_id);
|
|
||||||
|
|
||||||
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colour building
|
|
||||||
*
|
|
||||||
* Used in multi-edit mode to colour buildings on map click
|
|
||||||
*
|
|
||||||
* Pulls data from URL to form update
|
|
||||||
*
|
|
||||||
* @param {Building} building
|
|
||||||
*/
|
|
||||||
colourBuilding(building: Building) {
|
|
||||||
const cat = this.props.match.params.category;
|
|
||||||
|
|
||||||
if (cat === 'like') {
|
|
||||||
this.likeBuilding(building.building_id);
|
|
||||||
} else {
|
} else {
|
||||||
const data = parseJsonOrDefault(this.getMultiEditDataString());
|
return newValue;
|
||||||
|
|
||||||
|
|
||||||
if (data != undefined && !Object.values(data).some(x => x == undefined)) {
|
|
||||||
this.updateBuilding(building.building_id, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
likeBuilding(buildingId) {
|
export const MapApp: React.FC<MapAppProps> = props => {
|
||||||
apiPost(`/api/buildings/${buildingId}/like.json`, { like: true })
|
const [categoryUrlParam] = useUrlCategoryParam();
|
||||||
.then(res => {
|
const [selectedBuildingId, setSelectedBuildingId] = useUrlBuildingParam();
|
||||||
|
const [mode] = useUrlModeParam();
|
||||||
|
|
||||||
|
const [currentCategory, setCategory] = useState<Category>();
|
||||||
|
useEffect(() => setCategory(unless(categoryUrlParam, 'categories')), [categoryUrlParam]);
|
||||||
|
|
||||||
|
const displayCategory = useLastNotEmpty(currentCategory) ?? defaultMapCategory;
|
||||||
|
|
||||||
|
const [building, updateBuilding, reloadBuilding] = useBuildingData(selectedBuildingId, props.building);
|
||||||
|
const [buildingLike, updateBuildingLike] = useBuildingLikeData(selectedBuildingId, props.building_like);
|
||||||
|
const [userVerified, updateUserVerified, reloadUserVerified] = useUserVerifiedData(selectedBuildingId, props.user_verified);
|
||||||
|
|
||||||
|
const [revisionId, updateRevisionId] = useRevisionId(props.revisionId);
|
||||||
|
useEffect(() => {
|
||||||
|
updateRevisionId(building?.revision_id)
|
||||||
|
}, [building]);
|
||||||
|
|
||||||
|
const viewEditMode = unless(mode, 'multi-edit');
|
||||||
|
|
||||||
|
const [multiEditData, multiEditError] = useMultiEditData();
|
||||||
|
|
||||||
|
const selectBuilding = useCallback((selectedBuilding: Building) => {
|
||||||
|
updateBuilding(Object.assign({}, building, selectedBuilding));
|
||||||
|
setSelectedBuildingId(setOrToggle(selectedBuildingId, selectedBuilding?.building_id));
|
||||||
|
}, [selectedBuildingId, setSelectedBuildingId, updateBuilding, building]);
|
||||||
|
|
||||||
|
const colourBuilding = useCallback(async (building: Building) => {
|
||||||
|
const buildingId = building?.building_id;
|
||||||
|
|
||||||
|
if(buildingId != undefined && multiEditError == undefined) {
|
||||||
|
const isLike = currentCategory === Category.Community;
|
||||||
|
const endpoint = isLike ?
|
||||||
|
`/api/buildings/${buildingId}/like.json`:
|
||||||
|
`/api/buildings/${buildingId}.json`;
|
||||||
|
|
||||||
|
const payload = isLike ? {like: true} : multiEditData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiPost(endpoint, payload);
|
||||||
if(res.error) {
|
if(res.error) {
|
||||||
console.error({ error: res.error });
|
console.error({ error: res.error });
|
||||||
} else {
|
} else {
|
||||||
this.increaseRevision(res.revision_id);
|
updateRevisionId(res.revision_id);
|
||||||
}
|
}
|
||||||
}).catch(
|
} catch(error) {
|
||||||
(err) => console.error({ error: err })
|
console.error({ error });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}, [multiEditError, multiEditData, currentCategory]);
|
||||||
|
|
||||||
updateBuilding(buildingId, data) {
|
const handleBuildingUpdate = useCallback((buildingId: number, updatedData: Building) => {
|
||||||
apiPost(`/api/buildings/${buildingId}.json`, data)
|
// only update current building data if the IDs match
|
||||||
.then(res => {
|
if(buildingId === selectedBuildingId) {
|
||||||
if (res.error) {
|
updateBuilding(Object.assign({}, building, updatedData));
|
||||||
console.error({ error: res.error });
|
|
||||||
} else {
|
} else {
|
||||||
this.increaseRevision(res.revision_id);
|
// otherwise, still update the latest revision ID
|
||||||
|
updateRevisionId(updatedData.revision_id);
|
||||||
}
|
}
|
||||||
}).catch(
|
}, [selectedBuildingId, building, updateBuilding, updateRevisionId]);
|
||||||
(err) => console.error({ error: err })
|
|
||||||
);
|
const handleBuildingLikeUpdate = useCallback((buildingId: number, updatedData: boolean) => {
|
||||||
|
// only update current building data if the IDs match
|
||||||
|
if(buildingId === selectedBuildingId) {
|
||||||
|
updateBuildingLike(updatedData);
|
||||||
}
|
}
|
||||||
|
}, [selectedBuildingId, updateBuildingLike]);
|
||||||
|
|
||||||
render() {
|
const handleUserVerifiedUpdate = useCallback((buildingId: number, updatedData: UserVerified) => {
|
||||||
const mode = this.props.match.params.mode;
|
// only update current building data if the IDs match
|
||||||
const viewEditMode = mode === 'multi-edit' ? undefined : mode;
|
if(buildingId === selectedBuildingId) {
|
||||||
|
updateUserVerified(Object.assign({}, userVerified, updatedData)); // quickly show added verifications
|
||||||
let category = this.state.category || 'age';
|
reloadBuilding();
|
||||||
|
reloadUserVerified(); // but still reload from server to reflect removed verifications
|
||||||
const building_id = this.state.building && this.state.building.building_id;
|
}
|
||||||
|
}, [selectedBuildingId, updateUserVerified, reloadBuilding, userVerified]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<Switch>
|
|
||||||
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
|
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
|
||||||
</Switch>
|
<Sidebar>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Sidebar>
|
|
||||||
<Welcome />
|
<Welcome />
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/:mode/categories/:building?">
|
<Route exact path="/multi-edit/:cat">
|
||||||
<Sidebar>
|
<MultiEdit category={displayCategory} />
|
||||||
<Categories mode={mode || 'view'} building_id={building_id} />
|
</Route>
|
||||||
</Sidebar>
|
<Route path="/:mode/:cat">
|
||||||
|
<Categories mode={mode || 'view'} building_id={selectedBuildingId} />
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/:mode/:cat/:building/history">
|
||||||
|
<EditHistory building={building} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/multi-edit/:cat" render={(props) => (
|
|
||||||
<MultiEdit
|
|
||||||
category={category}
|
|
||||||
dataString={this.getMultiEditDataString()}
|
|
||||||
user={this.props.user}
|
|
||||||
/>
|
|
||||||
)} />
|
|
||||||
<Route exact path="/:mode/:cat/:building?">
|
<Route exact path="/:mode/:cat/:building?">
|
||||||
<Sidebar>
|
|
||||||
<Categories mode={mode || 'view'} building_id={building_id} />
|
|
||||||
<BuildingView
|
<BuildingView
|
||||||
mode={viewEditMode}
|
mode={viewEditMode}
|
||||||
cat={category}
|
cat={displayCategory}
|
||||||
building={this.state.building}
|
building={building}
|
||||||
building_like={this.state.building_like}
|
building_like={buildingLike}
|
||||||
user_verified={this.state.user_verified}
|
user_verified={userVerified ?? {}}
|
||||||
selectBuilding={this.selectBuilding}
|
onBuildingUpdate={handleBuildingUpdate}
|
||||||
user={this.props.user}
|
onBuildingLikeUpdate={handleBuildingLikeUpdate}
|
||||||
|
onUserVerifiedUpdate={handleUserVerifiedUpdate}
|
||||||
/>
|
/>
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/:mode/:cat/:building/history">
|
</Switch>
|
||||||
<Sidebar>
|
|
||||||
<Categories mode={mode || 'view'} building_id={building_id} />
|
|
||||||
<EditHistory building={this.state.building} />
|
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/:mode(view|edit|multi-edit)"
|
<Route exact path="/:mode(view|edit|multi-edit)"
|
||||||
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
|
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</Sidebar>
|
||||||
<ColouringMap
|
<ColouringMap
|
||||||
building={this.state.building}
|
selectedBuildingId={selectedBuildingId}
|
||||||
mode={mode || 'basic'}
|
mode={mode || 'basic'}
|
||||||
category={category}
|
category={displayCategory}
|
||||||
revision_id={this.state.revision_id}
|
revisionId={revisionId}
|
||||||
selectBuilding={this.selectBuilding}
|
onBuildingAction={mode === 'multi-edit' ? colourBuilding : selectBuilding}
|
||||||
colourBuilding={this.colourBuilding}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default MapApp;
|
|
||||||
|
@ -4,145 +4,10 @@ import './legend.css';
|
|||||||
|
|
||||||
import { DownIcon, UpIcon } from '../components/icons';
|
import { DownIcon, UpIcon } from '../components/icons';
|
||||||
import { Logo } from '../components/logo';
|
import { Logo } from '../components/logo';
|
||||||
|
import { LegendConfig } from '../config/category-maps-config';
|
||||||
const LEGEND_CONFIG = {
|
|
||||||
location: {
|
|
||||||
title: 'Location',
|
|
||||||
description: '% data collected',
|
|
||||||
elements: [
|
|
||||||
{ color: '#084081', text: '≥80%' },
|
|
||||||
{ color: '#0868ac', text: '60–80%' },
|
|
||||||
{ color: '#43a2ca', text: '40–60%' },
|
|
||||||
{ color: '#7bccc4', text: '20–40%' },
|
|
||||||
{ color: '#bae4bc', text: '<20%' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
use: {
|
|
||||||
title: 'Land Use',
|
|
||||||
elements: [
|
|
||||||
{ color: '#e5050d', text: 'Mixed Use' },
|
|
||||||
{ subtitle: 'Single use:'},
|
|
||||||
{ color: '#4a54a6', text: 'Residential' },
|
|
||||||
{ color: '#ff8c00', text: 'Retail' },
|
|
||||||
{ color: '#f5f58f', text: 'Industry & Business' },
|
|
||||||
{ color: '#73ccd1', text: 'Community Services' },
|
|
||||||
{ color: '#ffbfbf', text: 'Recreation & Leisure' },
|
|
||||||
{ color: '#b3de69', text: 'Transport' },
|
|
||||||
{ color: '#cccccc', text: 'Utilities & Infrastructure' },
|
|
||||||
{ color: '#898944', text: 'Defence' },
|
|
||||||
{ color: '#fa667d', text: 'Agriculture' },
|
|
||||||
{ color: '#53f5dd', text: 'Minerals' },
|
|
||||||
{ color: '#ffffff', text: 'Vacant & Derelict' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
title: 'Type',
|
|
||||||
elements: [
|
|
||||||
{ color: "#f2a2b9", text: "Detached" },
|
|
||||||
{ color: "#ab8fb0", text: "Semi-Detached" },
|
|
||||||
{ color: "#3891d1", text: "End-Terrace" },
|
|
||||||
{ color: "#226291", text: "Mid-Terrace" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
age: {
|
|
||||||
title: 'Age',
|
|
||||||
elements: [
|
|
||||||
{ color: '#fff9b8', text: '>2020' },
|
|
||||||
{ color: '#fae269', text: '2000-2019' },
|
|
||||||
{ color: '#fbaf27', text: '1980-1999' },
|
|
||||||
{ color: '#e6711d', text: '1960-1979' },
|
|
||||||
{ color: '#cc1212', text: '1940-1959' },
|
|
||||||
{ color: '#8f0303', text: '1920-1939' },
|
|
||||||
{ color: '#8f5385', text: '1900-1919' },
|
|
||||||
{ color: '#c3e1eb', text: '1880-1899' },
|
|
||||||
{ color: '#6a9dba', text: '1860-1879' },
|
|
||||||
{ color: '#3b74a3', text: '1840-1859' },
|
|
||||||
{ color: '#95ded8', text: '1820-1839' },
|
|
||||||
{ color: '#68aba5', text: '1800-1819' },
|
|
||||||
{ color: '#acc98f', text: '1750-1799' },
|
|
||||||
{ color: '#6d8a51', text: '1700-1749' },
|
|
||||||
{ color: '#d0c291', text: '<1700' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
title: 'Height to apex',
|
|
||||||
elements: [
|
|
||||||
{ color: '#f7f4f9', text: '0-5.55'},
|
|
||||||
{ color: '#e7e1ef', text: '5.55-7.73'},
|
|
||||||
{ color: '#d4b9da', text: '7.73-11.38'},
|
|
||||||
{ color: '#c994c7', text: '11.38-18.45'},
|
|
||||||
{ color: '#df65b0', text: '18.45-35.05'},
|
|
||||||
{ color: '#e7298a', text: '35.05-89.30'},
|
|
||||||
{ color: '#ce1256', text: '89.30-152'},
|
|
||||||
{ color: '#980043', text: '≥152'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
construction: {
|
|
||||||
title: 'Construction',
|
|
||||||
elements: [
|
|
||||||
{ color: "#96613b", text: "Wood" },
|
|
||||||
{ color: "#ffffe3", text: "Stone" },
|
|
||||||
{ color: "#f5d96b", text: "Brick" },
|
|
||||||
{ color: "#beffe8", text: "Steel" },
|
|
||||||
{ color: "#fca89d", text: "Reinforced Concrete" },
|
|
||||||
{ color: "#5c8970", text: "Other Metal" },
|
|
||||||
{ color: "#b5a859", text: "Other Natural Material" },
|
|
||||||
{ color: "#c48a85", text: "Other Man-Made Material" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
title: 'Team',
|
|
||||||
elements: []
|
|
||||||
},
|
|
||||||
sustainability: {
|
|
||||||
title: 'Sustainability',
|
|
||||||
description: 'DEC Rating',
|
|
||||||
elements: [
|
|
||||||
{ color: "#007f3d", text: 'A' },
|
|
||||||
{ color: "#2c9f29", text: 'B' },
|
|
||||||
{ color: "#9dcb3c", text: 'C' },
|
|
||||||
{ color: "#fff200", text: 'D' },
|
|
||||||
{ color: "#f7af1d", text: 'E' },
|
|
||||||
{ color: "#ed6823", text: 'F' },
|
|
||||||
{ color: "#e31d23", text: 'G' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
streetscape: {
|
|
||||||
title: 'Streetscape',
|
|
||||||
elements: []
|
|
||||||
},
|
|
||||||
planning: {
|
|
||||||
title: 'Statutory protections',
|
|
||||||
disclaimer: 'All data relating to designated buildings should be checked on the National Heritage List for England or local authority websites where used for planning or development purposes',
|
|
||||||
elements: [
|
|
||||||
{ color: '#95beba', text: 'In conservation area'},
|
|
||||||
{ color: '#c72e08', text: 'Grade I listed'},
|
|
||||||
{ color: '#e75b42', text: 'Grade II* listed'},
|
|
||||||
{ color: '#ffbea1', text: 'Grade II listed'},
|
|
||||||
{ color: '#858ed4', text: 'Locally listed'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
dynamics: {
|
|
||||||
title: 'Dynamics',
|
|
||||||
elements: []
|
|
||||||
},
|
|
||||||
community: {
|
|
||||||
title: 'Like Me',
|
|
||||||
elements: [
|
|
||||||
{ color: '#bd0026', text: '👍👍👍👍 100+' },
|
|
||||||
{ color: '#e31a1c', text: '👍👍👍 50–99' },
|
|
||||||
{ color: '#fc4e2a', text: '👍👍 20–49' },
|
|
||||||
{ color: '#fd8d3c', text: '👍👍 10–19' },
|
|
||||||
{ color: '#feb24c', text: '👍 3–9' },
|
|
||||||
{ color: '#fed976', text: '👍 2' },
|
|
||||||
{ color: '#ffe8a9', text: '👍 1'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
interface LegendProps {
|
interface LegendProps {
|
||||||
slug: string;
|
legendConfig: LegendConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegendState {
|
interface LegendState {
|
||||||
@ -184,59 +49,62 @@ class Legend extends React.Component<LegendProps, LegendState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const details = LEGEND_CONFIG[this.props.slug] || {};
|
const {
|
||||||
const title = details.title || "";
|
title = undefined,
|
||||||
const elements = details.elements || [];
|
elements = [],
|
||||||
|
description = undefined,
|
||||||
|
disclaimer = undefined
|
||||||
|
} = this.props.legendConfig ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-legend">
|
<div className="map-legend">
|
||||||
<Logo variant="default" />
|
<Logo variant="default" />
|
||||||
<h4 className="h4">
|
|
||||||
{ title }
|
|
||||||
</h4>
|
|
||||||
{
|
{
|
||||||
elements.length > 0 ?
|
title && <h4 className="h4">{title}</h4>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
elements.length > 0 &&
|
||||||
<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={this.handleClick} >
|
<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={this.handleClick} >
|
||||||
{
|
{
|
||||||
this.state.collapseList ?
|
this.state.collapseList ?
|
||||||
<UpIcon /> :
|
<UpIcon /> :
|
||||||
<DownIcon />
|
<DownIcon />
|
||||||
}
|
}
|
||||||
</button> :
|
</button>
|
||||||
null
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
details.description?
|
description && <p>{description}</p>
|
||||||
<p>{details.description} </p>
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.length?
|
elements.length === 0 ?
|
||||||
|
<p className="data-intro">Coming soon…</p> :
|
||||||
<ul className={this.state.collapseList ? 'collapse data-legend' : 'data-legend'} >
|
<ul className={this.state.collapseList ? 'collapse data-legend' : 'data-legend'} >
|
||||||
{
|
{
|
||||||
details.disclaimer &&
|
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
||||||
<p className='legend-disclaimer'>{details.disclaimer}</p>
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.map((item) => {
|
elements.map((item) => {
|
||||||
if(item.subtitle != undefined) {
|
let key: string,
|
||||||
return (<li key={item.subtitle}>
|
content: React.ReactElement;
|
||||||
<h6>{item.subtitle}</h6>
|
|
||||||
</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if('subtitle' in item) {
|
||||||
|
key = item.subtitle;
|
||||||
<li key={item.color} >
|
content = <h6>{item.subtitle}</h6>;
|
||||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
} else {
|
||||||
|
key = `${item.text}-${item.color}`;
|
||||||
|
content = <>
|
||||||
|
<div className="key" style={ { background: item.color, border: item.border } } />
|
||||||
{ item.text }
|
{ item.text }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{content}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
: <p className="data-intro">Coming soon…</p>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,21 +7,22 @@ import './map.css';
|
|||||||
|
|
||||||
import { apiGet } from '../apiHelpers';
|
import { apiGet } from '../apiHelpers';
|
||||||
import { HelpIcon } from '../components/icons';
|
import { HelpIcon } from '../components/icons';
|
||||||
import { Building } from '../models/building';
|
|
||||||
|
|
||||||
import Legend from './legend';
|
import Legend from './legend';
|
||||||
import SearchBox from './search-box';
|
import SearchBox from './search-box';
|
||||||
import ThemeSwitcher from './theme-switcher';
|
import ThemeSwitcher from './theme-switcher';
|
||||||
|
import { categoryMapsConfig } from '../config/category-maps-config';
|
||||||
|
import { Category } from '../config/categories-config';
|
||||||
|
import { Building } from '../models/building';
|
||||||
|
|
||||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||||
|
|
||||||
interface ColouringMapProps {
|
interface ColouringMapProps {
|
||||||
building?: Building;
|
selectedBuildingId: number;
|
||||||
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||||
category: string;
|
category: Category;
|
||||||
revision_id: number;
|
revisionId: string;
|
||||||
selectBuilding: (building: Building) => void;
|
onBuildingAction: (building: Building) => void;
|
||||||
colourBuilding: (building: Building) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColouringMapState {
|
interface ColouringMapState {
|
||||||
@ -58,30 +59,12 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
const mode = this.props.mode;
|
|
||||||
const { lat, lng } = e.latlng;
|
const { lat, lng } = e.latlng;
|
||||||
apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
|
apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data && data.length){
|
const building = data?.[0];
|
||||||
const building = data[0];
|
this.props.onBuildingAction(building);
|
||||||
if (mode === 'multi-edit') {
|
}).catch(err => console.error(err));
|
||||||
// colour building directly
|
|
||||||
this.props.colourBuilding(building);
|
|
||||||
} else if (this.props.building == undefined || building.building_id !== this.props.building.building_id){
|
|
||||||
this.props.selectBuilding(building);
|
|
||||||
} else {
|
|
||||||
this.props.selectBuilding(undefined);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mode !== 'multi-edit') {
|
|
||||||
// deselect but keep/return to expected colour theme
|
|
||||||
// except if in multi-edit (never select building, only colour on click)
|
|
||||||
this.props.selectBuilding(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(
|
|
||||||
(err) => console.error(err)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
themeSwitch(e) {
|
themeSwitch(e) {
|
||||||
@ -103,6 +86,8 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const categoryMapDefinition = categoryMapsConfig[this.props.category];
|
||||||
|
|
||||||
const position: [number, number] = [this.state.lat, this.state.lng];
|
const position: [number, number] = [this.state.lat, this.state.lng];
|
||||||
|
|
||||||
// baselayer
|
// baselayer
|
||||||
@ -121,55 +106,37 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}{r}.png`;
|
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}{r}.png`;
|
||||||
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} maxZoom={19}/>;
|
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} maxZoom={19}/>;
|
||||||
|
|
||||||
|
|
||||||
const boundaryStyleFn = () => ({color: '#bbb', fill: false});
|
|
||||||
const boundaryLayer = this.state.boundary &&
|
const boundaryLayer = this.state.boundary &&
|
||||||
<GeoJSON data={this.state.boundary} style={boundaryStyleFn}/>;
|
<GeoJSON data={this.state.boundary} style={{color: '#bbb', fill: false}}/>;
|
||||||
|
|
||||||
// colour-data tiles
|
const tileset = categoryMapDefinition.mapStyle;
|
||||||
const cat = this.props.category;
|
const dataLayer = tileset != undefined &&
|
||||||
const tilesetByCat = {
|
|
||||||
age: 'date_year',
|
|
||||||
size: 'size_height',
|
|
||||||
construction: 'construction_core_material',
|
|
||||||
location: 'location',
|
|
||||||
community: 'likes',
|
|
||||||
planning: 'planning_combined',
|
|
||||||
sustainability: 'sust_dec',
|
|
||||||
type: 'building_attachment_form',
|
|
||||||
use: 'landuse'
|
|
||||||
};
|
|
||||||
const tileset = tilesetByCat[cat];
|
|
||||||
// pick revision id to bust browser cache
|
|
||||||
const rev = this.props.revision_id;
|
|
||||||
const dataLayer = tileset != undefined ?
|
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={tileset}
|
key={tileset}
|
||||||
url={`/tiles/${tileset}/{z}/{x}/{y}{r}.png?rev=${rev}`}
|
url={`/tiles/${tileset}/{z}/{x}/{y}{r}.png?rev=${this.props.revisionId}`}
|
||||||
minZoom={9}
|
minZoom={9}
|
||||||
maxZoom={19}
|
maxZoom={19}
|
||||||
/>
|
/>;
|
||||||
: null;
|
|
||||||
|
|
||||||
// highlight
|
// highlight
|
||||||
const highlightLayer = this.props.building != undefined ?
|
const highlightLayer = this.props.selectedBuildingId != undefined &&
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={this.props.building.building_id}
|
key={this.props.selectedBuildingId}
|
||||||
url={`/tiles/highlight/{z}/{x}/{y}{r}.png?highlight=${this.props.building.building_id}&base=${tileset}`}
|
url={`/tiles/highlight/{z}/{x}/{y}{r}.png?highlight=${this.props.selectedBuildingId}&base=${tileset}`}
|
||||||
minZoom={13}
|
minZoom={13}
|
||||||
maxZoom={19}
|
maxZoom={19}
|
||||||
zIndex={100}
|
zIndex={100}
|
||||||
/>
|
/>;
|
||||||
: null;
|
|
||||||
|
|
||||||
const numbersLayer = <TileLayer
|
const numbersLayer = <TileLayer
|
||||||
key={this.state.theme}
|
key={this.state.theme}
|
||||||
url={`/tiles/number_labels/{z}/{x}/{y}{r}.png?rev=${rev}`}
|
url={`/tiles/number_labels/{z}/{x}/{y}{r}.png?rev=${this.props.revisionId}`}
|
||||||
zIndex={200}
|
zIndex={200}
|
||||||
minZoom={17}
|
minZoom={17}
|
||||||
maxZoom={19}
|
maxZoom={19}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
const hasSelection = this.props.selectedBuildingId != undefined;
|
||||||
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -195,20 +162,18 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
<AttributionControl prefix=""/>
|
<AttributionControl prefix=""/>
|
||||||
</Map>
|
</Map>
|
||||||
{
|
{
|
||||||
this.props.mode !== 'basic'? (
|
this.props.mode !== 'basic' &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{
|
{
|
||||||
this.props.building == undefined ?
|
!hasSelection &&
|
||||||
<div className="map-notice">
|
<div className="map-notice">
|
||||||
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||||
</div>
|
</div>
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
<Legend slug={cat} />
|
<Legend legendConfig={categoryMapDefinition?.legend} />
|
||||||
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
||||||
<SearchBox onLocate={this.handleLocate} />
|
<SearchBox onLocate={this.handleLocate} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : null
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
interface Building {
|
import { dataFields } from '../config/data-fields-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type representing the types of a building's attributes.
|
||||||
|
* This is derived automatically from the "example" fields in dataFieldsConfig.
|
||||||
|
* If a TS error starting with "Type 'example' cannot be used to index type [...]" appears here,
|
||||||
|
* that means an example field is most probably missing on one of the config definitions in dataFieldsConfig.
|
||||||
|
*/
|
||||||
|
export type BuildingAttributes = {[key in keyof typeof dataFields]: (typeof dataFields)[key]['example']};
|
||||||
|
|
||||||
|
export type BuildingAttributeVerificationCounts = {[key in keyof typeof dataFields]: number};
|
||||||
|
|
||||||
|
export type UserVerified = {[key in keyof BuildingAttributes]?: BuildingAttributes[key]};
|
||||||
|
|
||||||
|
export interface Building extends BuildingAttributes {
|
||||||
building_id: number;
|
building_id: number;
|
||||||
geometry_id: number;
|
geometry_id: number;
|
||||||
revision_id: number;
|
revision_id: string;
|
||||||
|
|
||||||
uprns: string[];
|
verified: BuildingAttributeVerificationCounts;
|
||||||
// TODO: add other fields as needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
Building
|
|
||||||
};
|
|
||||||
|
16
app/src/frontend/nav/url-param-transform.ts
Normal file
16
app/src/frontend/nav/url-param-transform.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export interface UrlParamTransform<T> {
|
||||||
|
fromParam: (x: string) => T;
|
||||||
|
toParam: (x: T) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity: <T>(x: T) => T = (x) => x;
|
||||||
|
|
||||||
|
export const stringParamTransform: UrlParamTransform<string> = {
|
||||||
|
fromParam: identity,
|
||||||
|
toParam: identity
|
||||||
|
};
|
||||||
|
|
||||||
|
export const intParamTransform: UrlParamTransform<number> = {
|
||||||
|
fromParam: x => parseInt(x, 10),
|
||||||
|
toParam: x => x.toString()
|
||||||
|
};
|
6
app/src/frontend/nav/use-url-building-param.ts
Normal file
6
app/src/frontend/nav/use-url-building-param.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { intParamTransform } from './url-param-transform';
|
||||||
|
import { useUrlParam } from './use-url-param';
|
||||||
|
|
||||||
|
export function useUrlBuildingParam() {
|
||||||
|
return useUrlParam('building', intParamTransform);
|
||||||
|
}
|
14
app/src/frontend/nav/use-url-category-param.ts
Normal file
14
app/src/frontend/nav/use-url-category-param.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Category } from '../config/categories-config';
|
||||||
|
import { useUrlParam } from './use-url-param';
|
||||||
|
|
||||||
|
export function useUrlCategoryParam() {
|
||||||
|
return useUrlParam('category', {
|
||||||
|
fromParam: (x: string) => {
|
||||||
|
// TODO: add validation
|
||||||
|
return x as Category | 'categories';
|
||||||
|
},
|
||||||
|
toParam: (x: Category | 'categories') => {
|
||||||
|
return x as string;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
10
app/src/frontend/nav/use-url-mode-param.ts
Normal file
10
app/src/frontend/nav/use-url-mode-param.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useUrlParam } from './use-url-param';
|
||||||
|
|
||||||
|
type Mode = 'view' | 'edit' | 'multi-edit';
|
||||||
|
|
||||||
|
export function useUrlModeParam() {
|
||||||
|
return useUrlParam<Mode>('mode', {
|
||||||
|
fromParam: (x) => x as Mode,
|
||||||
|
toParam: (x) => x
|
||||||
|
});
|
||||||
|
}
|
33
app/src/frontend/nav/use-url-param.ts
Normal file
33
app/src/frontend/nav/use-url-param.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useHistory, useRouteMatch, generatePath } from 'react-router';
|
||||||
|
|
||||||
|
import { UrlParamTransform } from './url-param-transform';
|
||||||
|
|
||||||
|
export function useUrlParam<T>(
|
||||||
|
param: string,
|
||||||
|
paramTransform: UrlParamTransform<T>
|
||||||
|
): [T, (newParam: T) => void] {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [paramValue, setParamValue] = useState<T>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stringValue: string = match.params[param];
|
||||||
|
|
||||||
|
setParamValue(stringValue && paramTransform.fromParam(stringValue));
|
||||||
|
}, [param, paramTransform, match.url]);
|
||||||
|
|
||||||
|
const setUrlParam = useCallback((value: T) => {
|
||||||
|
const stringValue = value == undefined ? '' : paramTransform.toParam(value);
|
||||||
|
const newPath = generatePath(match.path, {
|
||||||
|
...match.params,
|
||||||
|
...{
|
||||||
|
[param]: stringValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
history.push(newPath);
|
||||||
|
}, [param, paramTransform, match.url]);
|
||||||
|
|
||||||
|
return [paramValue, setUrlParam];
|
||||||
|
}
|
@ -75,6 +75,7 @@ function renderHTML(context, data, req, res) {
|
|||||||
building={data.building}
|
building={data.building}
|
||||||
building_like={data.building_like}
|
building_like={data.building_like}
|
||||||
revisionId={data.latestRevisionId}
|
revisionId={data.latestRevisionId}
|
||||||
|
user_verified={data.userVerified}
|
||||||
/>
|
/>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user