Add verification for Facade Data, thread info through frontend
This commit is contained in:
parent
b30e882669
commit
2d6a18f81b
app/src
client.tsx
frontend
app.tsx
frontendRoute.tsxbuilding
components
map-app.tsxmap
@ -16,6 +16,7 @@ hydrate(
|
||||
user={data.user}
|
||||
building={data.building}
|
||||
building_like={data.building_like}
|
||||
user_verified={data.user_verified}
|
||||
revisionId={data.latestRevisionId}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
|
@ -30,6 +30,7 @@ interface AppProps {
|
||||
user?: User;
|
||||
building?: Building;
|
||||
building_like?: boolean;
|
||||
user_verified?: object;
|
||||
revisionId: number;
|
||||
}
|
||||
|
||||
@ -122,6 +123,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
{...props}
|
||||
building={this.props.building}
|
||||
building_like={this.props.building_like}
|
||||
user_verified={this.props.user_verified}
|
||||
user={this.state.user}
|
||||
revisionId={this.props.revisionId}
|
||||
/>
|
||||
|
@ -24,6 +24,7 @@ interface BuildingViewProps {
|
||||
building_like?: boolean;
|
||||
user?: any;
|
||||
selectBuilding: (building: Building) => void;
|
||||
user_verified?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,7 +34,7 @@ const DataEntry: React.FC<DataEntryProps> = (props) => {
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
|
||||
|
||||
maxLength={props.maxLength}
|
||||
placeholder={props.placeholder}
|
||||
valueTransform={props.valueTransform}
|
||||
|
@ -33,7 +33,7 @@ const NumericDataEntry: React.FunctionComponent<NumericDataEntryProps> = (props)
|
||||
min={props.min}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
onChange={e =>
|
||||
onChange={e =>
|
||||
props.onChange(
|
||||
props.slug,
|
||||
e.target.value === '' ? null : parseFloat(e.target.value)
|
||||
|
16
app/src/frontend/building/data-components/verification.css
Normal file
16
app/src/frontend/building/data-components/verification.css
Normal file
@ -0,0 +1,16 @@
|
||||
.verification-container {
|
||||
font-size: 0.83333rem;
|
||||
}
|
||||
.verification-status {
|
||||
padding-right: 0.5em;
|
||||
cursor: default;
|
||||
}
|
||||
.verification-container .verification-status .fa-w-16 {
|
||||
width: 20px;
|
||||
}
|
||||
.verification-container .btn {
|
||||
margin: 0 0.5em;
|
||||
padding: 0.125em 0.75em;
|
||||
vertical-align: baseline;
|
||||
font-size: 0.83333rem;
|
||||
}
|
57
app/src/frontend/building/data-components/verification.tsx
Normal file
57
app/src/frontend/building/data-components/verification.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { VerifyIcon } from '../../components/icons';
|
||||
|
||||
import './verification.css';
|
||||
|
||||
interface VerificationProps {
|
||||
slug: string;
|
||||
onVerify: (slug: string, verify: boolean) => void;
|
||||
user_verified: boolean;
|
||||
user_verified_as: string;
|
||||
verified_count: number;
|
||||
allow_verify: boolean;
|
||||
}
|
||||
|
||||
const Verification: React.FunctionComponent<VerificationProps> = (props) => (
|
||||
<div className="verification-container">
|
||||
<span
|
||||
className="verification-status"
|
||||
title={`Verified by ${props.verified_count} ${(props.verified_count == 1)? "person": "people"}`}
|
||||
>
|
||||
<VerifyIcon />
|
||||
{props.verified_count}
|
||||
</span>
|
||||
{
|
||||
props.allow_verify?
|
||||
props.user_verified?
|
||||
<Fragment>
|
||||
Verified as
|
||||
"<span>{props.user_verified_as}</span>"
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
title="Remove my verification"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onVerify(props.slug, false)
|
||||
}}>
|
||||
Remove
|
||||
</button>
|
||||
</Fragment>
|
||||
:
|
||||
<Fragment>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
title="Confirm that the current value is correct"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onVerify(props.slug, true)
|
||||
}}>
|
||||
Verify
|
||||
</button>
|
||||
</Fragment>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Verification;
|
@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { NavLink, Redirect } from 'react-router-dom';
|
||||
import Confetti from 'canvas-confetti';
|
||||
|
||||
import { apiPost } from '../apiHelpers';
|
||||
import ErrorBox from '../components/error-box';
|
||||
@ -24,6 +25,7 @@ interface DataContainerProps {
|
||||
mode: 'view' | 'edit';
|
||||
building?: Building;
|
||||
building_like?: boolean;
|
||||
user_verified?: any;
|
||||
selectBuilding: (building: Building) => void;
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
this.handleReset = this.handleReset.bind(this);
|
||||
this.handleLike = this.handleLike.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleVerify = this.handleVerify.bind(this);
|
||||
|
||||
this.toggleCopying = this.toggleCopying.bind(this);
|
||||
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||
@ -167,7 +170,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
`/api/buildings/${this.props.building.building_id}/like.json`,
|
||||
{like: like}
|
||||
);
|
||||
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
@ -188,7 +191,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
`/api/buildings/${this.props.building.building_id}.json`,
|
||||
this.state.buildingEdits
|
||||
);
|
||||
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
@ -199,6 +202,33 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
}
|
||||
}
|
||||
|
||||
async handleVerify(slug: string, verify: boolean) {
|
||||
const verifyPatch = {};
|
||||
if (verify) {
|
||||
verifyPatch[slug] = this.props.building[slug];
|
||||
} else {
|
||||
verifyPatch[slug] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiPost(
|
||||
`/api/buildings/${this.props.building.building_id}/verify.json`,
|
||||
verifyPatch
|
||||
);
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
if (verify) {
|
||||
Confetti({zIndex: 2000});
|
||||
}
|
||||
this.props.selectBuilding(this.props.building);
|
||||
}
|
||||
} catch(err) {
|
||||
this.setState({error: err});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.mode === 'edit' && !this.props.user){
|
||||
return <Redirect to="/sign-up.html" />;
|
||||
@ -284,6 +314,8 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onLike={this.handleLike}
|
||||
onVerify={this.handleVerify}
|
||||
user_verified={[]}
|
||||
/>
|
||||
</Fragment> :
|
||||
this.props.building != undefined ?
|
||||
@ -330,6 +362,9 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onLike={this.handleLike}
|
||||
onVerify={this.handleVerify}
|
||||
user_verified={this.props.user_verified}
|
||||
user={this.props.user}
|
||||
/>
|
||||
</form> :
|
||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
||||
|
@ -5,6 +5,7 @@ import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import TextboxDataEntry from '../data-components/textbox-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import YearDataEntry from '../data-components/year-data-entry';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
@ -15,7 +16,7 @@ import { CategoryViewProps } from './category-view-props';
|
||||
*/
|
||||
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<YearDataEntry
|
||||
@ -38,6 +39,15 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
max={currentYear}
|
||||
tooltip={dataFields.facade_year.tooltip}
|
||||
/>
|
||||
<Verification
|
||||
allow_verify={props.user !== undefined}
|
||||
slug="facade_year"
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("facade_year")}
|
||||
user_verified_as={props.user_verified.facade_year}
|
||||
verified_count={props.building.verified.facade_year}
|
||||
/>
|
||||
|
||||
<SelectDataEntry
|
||||
title={dataFields.date_source.title}
|
||||
slug="date_source"
|
||||
|
@ -13,6 +13,9 @@ interface CategoryViewProps {
|
||||
copy: CopyProps;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onLike: (like: boolean) => void;
|
||||
onVerify: (slug: string, verify: boolean) => void;
|
||||
user_verified: any;
|
||||
user?: any;
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -2,8 +2,22 @@
|
||||
* Mini-library of icons
|
||||
*/
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faAngleLeft, faAngleRight, faCaretDown, faCaretRight, faCaretUp, faCheck, faCheckDouble,
|
||||
faEye, faInfoCircle, faPaintBrush, faQuestionCircle, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCaretDown,
|
||||
faCaretRight,
|
||||
faCaretUp,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckDouble,
|
||||
faEye,
|
||||
faInfoCircle,
|
||||
faPaintBrush,
|
||||
faQuestionCircle,
|
||||
faSearch,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
|
||||
@ -13,6 +27,7 @@ library.add(
|
||||
faPaintBrush,
|
||||
faTimes,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckDouble,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
@ -51,6 +66,10 @@ const SaveDoneIcon = () => (
|
||||
<FontAwesomeIcon icon="check-double" />
|
||||
);
|
||||
|
||||
const VerifyIcon = () => (
|
||||
<FontAwesomeIcon icon="check-circle" />
|
||||
)
|
||||
|
||||
const BackIcon = () => (
|
||||
<FontAwesomeIcon icon="angle-left" />
|
||||
);
|
||||
@ -88,5 +107,6 @@ export {
|
||||
DownIcon,
|
||||
UpIcon,
|
||||
RightIcon,
|
||||
SearchIcon
|
||||
SearchIcon,
|
||||
VerifyIcon
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||
building_like?: boolean;
|
||||
user?: any;
|
||||
revisionId?: number;
|
||||
user_verified?: object;
|
||||
}
|
||||
|
||||
interface MapAppState {
|
||||
@ -33,6 +34,7 @@ interface MapAppState {
|
||||
revision_id: number;
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
user_verified: object;
|
||||
}
|
||||
|
||||
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
@ -43,7 +45,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
category: this.getCategory(props.match.params.category),
|
||||
revision_id: props.revisionId || 0,
|
||||
building: props.building,
|
||||
building_like: props.building_like
|
||||
building_like: props.building_like,
|
||||
user_verified: props.user_verified || {}
|
||||
};
|
||||
|
||||
this.selectBuilding = this.selectBuilding.bind(this);
|
||||
@ -60,7 +63,10 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchLatestRevision();
|
||||
this.fetchBuildingData();
|
||||
|
||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
||||
this.fetchBuildingData(strictParseInt(this.props.match.params.building));
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLatestRevision() {
|
||||
@ -77,27 +83,29 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
* Fetches building data if a building is selected but no data provided through
|
||||
* props (from server-side rendering)
|
||||
*/
|
||||
async fetchBuildingData() {
|
||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
||||
try {
|
||||
// TODO: simplify API calls, create helpers for fetching data
|
||||
const buildingId = strictParseInt(this.props.match.params.building);
|
||||
let [building, building_uprns, building_like] = await Promise.all([
|
||||
apiGet(`/api/buildings/${buildingId}.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/like.json`)
|
||||
]);
|
||||
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;
|
||||
building.uprns = building_uprns.uprns;
|
||||
|
||||
this.setState({
|
||||
building: building,
|
||||
building_like: building_like.like
|
||||
});
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
// TODO: add UI for API errors
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,34 +140,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.increaseRevision(building.revision_id);
|
||||
// get UPRNs and update
|
||||
apiGet(`/api/buildings/${building.building_id}/uprns.json`)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
console.error(res);
|
||||
} else {
|
||||
building.uprns = res.uprns;
|
||||
this.setState({ building: building });
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.setState({ building: building });
|
||||
});
|
||||
this.fetchBuildingData(building.building_id);
|
||||
|
||||
// get if liked and update
|
||||
apiGet(`/api/buildings/${building.building_id}/like.json`)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
console.error(res);
|
||||
} else {
|
||||
this.setState({ building_like: res.like });
|
||||
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.setState({ building_like: false });
|
||||
});
|
||||
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,6 +231,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
cat={category}
|
||||
building={this.state.building}
|
||||
building_like={this.state.building_like}
|
||||
user_verified={this.state.user_verified}
|
||||
selectBuilding={this.selectBuilding}
|
||||
user={this.props.user}
|
||||
/>
|
||||
|
@ -76,6 +76,10 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-legend li {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-legend .key {
|
||||
display: inline-block;
|
||||
width: 1.3rem;
|
||||
|
@ -116,10 +116,10 @@ const LEGEND_CONFIG = {
|
||||
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: '#c72e08', text: 'Grade I listed'},
|
||||
{ color: '#e75b42', text: 'Grade II* listed'},
|
||||
{ color: '#ffbea1', text: 'Grade II listed'},
|
||||
{ color: '#858ed4', text: 'Locally listed'},
|
||||
{ color: '#858ed4', text: 'Locally listed'},
|
||||
]
|
||||
},
|
||||
dynamics: {
|
||||
@ -228,14 +228,8 @@ class Legend extends React.Component<LegendProps, LegendState> {
|
||||
return (
|
||||
|
||||
<li key={item.color} >
|
||||
<tr>
|
||||
<td>
|
||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
||||
</td>
|
||||
<td>
|
||||
{ item.text }
|
||||
</td>
|
||||
</tr>
|
||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
||||
{ item.text }
|
||||
</li>
|
||||
|
||||
);
|
||||
|
@ -8,18 +8,20 @@ import {
|
||||
getBuildingById,
|
||||
getBuildingLikeById,
|
||||
getBuildingUPRNsById,
|
||||
getLatestRevisionId
|
||||
getLatestRevisionId,
|
||||
getUserVerifiedAttributes
|
||||
} from './api/services/building';
|
||||
import { getUserById } from './api/services/user';
|
||||
import App from './frontend/app';
|
||||
import { parseBuildingURL } from './parse';
|
||||
import asyncController from './api/routes/asyncController';
|
||||
|
||||
|
||||
// reference packed assets
|
||||
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
||||
|
||||
|
||||
function frontendRoute(req: express.Request, res: express.Response) {
|
||||
const frontendRoute = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const context: any = {}; // TODO: remove any
|
||||
const data: any = {}; // TODO: remove any
|
||||
context.status = 200;
|
||||
@ -31,34 +33,39 @@ function frontendRoute(req: express.Request, res: express.Response) {
|
||||
context.status = 404;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
userId ? getUserById(userId) : undefined,
|
||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
||||
getLatestRevisionId()
|
||||
]).then(function ([user, building, uprns, buildingLike, latestRevisionId]) {
|
||||
try {
|
||||
let [user, building, uprns, buildingLike, userVerified, latestRevisionId] = await Promise.all([
|
||||
userId ? getUserById(userId) : undefined,
|
||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
||||
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||
getLatestRevisionId()
|
||||
]);
|
||||
|
||||
if (isBuilding && typeof (building) === 'undefined') {
|
||||
context.status = 404;
|
||||
}
|
||||
data.user = user;
|
||||
data.building = building;
|
||||
data.building_like = buildingLike;
|
||||
data.user_verified = userVerified;
|
||||
if (data.building != null) {
|
||||
data.building.uprns = uprns;
|
||||
}
|
||||
data.latestRevisionId = latestRevisionId;
|
||||
renderHTML(context, data, req, res);
|
||||
}).catch(error => {
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
data.user = undefined;
|
||||
data.building = undefined;
|
||||
data.building_like = undefined;
|
||||
data.user_verified = {}
|
||||
data.latestRevisionId = 0;
|
||||
context.status = 500;
|
||||
renderHTML(context, data, req, res);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderHTML(context, data, req, res) {
|
||||
const markup = renderToString(
|
||||
@ -93,7 +100,7 @@ function renderHTML(context, data, req, res) {
|
||||
<meta property="og:image" content="https://colouring.london/images/logo-cl-square.png" />
|
||||
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Colouring London">
|
||||
|
Loading…
Reference in New Issue
Block a user