Merge pull request #564 from colouring-london/feature/menu-sidebar

Feature: menu sidebar
This commit is contained in:
Tom Russell 2020-05-04 17:52:15 +01:00 committed by GitHub
commit 57a43176d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3191 additions and 1674 deletions

View File

@ -1,5 +1,15 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"jest",
"react"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"eslint:recommended",
"plugin:jest/recommended",
"plugin:react/recommended"
@ -11,7 +21,6 @@
"es6": true,
"jest": true
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
@ -32,10 +41,6 @@
"no-multiple-empty-lines": ["warn", {"max": 1}],
"prefer-const": "warn"
},
"plugins": [
"jest",
"react"
],
"settings": {
"react": {
"version": "detect"

3346
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,28 +9,28 @@
"start": "razzle start",
"build": "razzle build",
"test": "razzle test --env=jsdom",
"lint": "eslint .",
"lint": "eslint --ext .tsx --ext .ts .",
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.21",
"@fortawesome/free-solid-svg-icons": "^5.10.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@mapbox/sphericalmercator": "^1.1.0",
"body-parser": "^1.19.0",
"bootstrap": "^4.4.1",
"connect-pg-simple": "^6.0.1",
"connect-pg-simple": "^6.1.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"leaflet": "^1.6.0",
"lodash.isequal": "^4.5.0",
"mapnik": "^4.2.1",
"mapnik": "^4.4.0",
"node-fs": "^0.1.7",
"nodemailer": "^6.3.0",
"nodemailer": "^6.4.6",
"pg-promise": "^8.7.5",
"query-string": "^6.8.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"query-string": "^6.12.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-leaflet": "^1.0.1",
"react-leaflet-universal": "^1.2.0",
"react-router-dom": "^5.0.1",
@ -39,30 +39,30 @@
"use-throttle": "0.0.3"
},
"devDependencies": {
"@types/express": "^4.17.2",
"@types/express-session": "^1.15.16",
"@types/jest": "^24.0.23",
"@types/express": "^4.17.5",
"@types/express-session": "^1.17.0",
"@types/jest": "^24.9.1",
"@types/lodash": "^4.14.149",
"@types/lodash.isequal": "^4.5.5",
"@types/mapbox__sphericalmercator": "^1.1.3",
"@types/node": "^12.12.25",
"@types/nodemailer": "^6.2.2",
"@types/react": "^16.9.16",
"@types/react-dom": "^16.9.4",
"@types/react-leaflet": "^2.5.0",
"@types/node": "^12.12.35",
"@types/nodemailer": "^6.4.0",
"@types/react": "^16.9.33",
"@types/react-dom": "^16.9.6",
"@types/react-leaflet": "^2.5.1",
"@types/react-router-dom": "^4.3.5",
"@types/sharp": "^0.22.3",
"@types/webpack-env": "^1.14.1",
"babel-eslint": "^10.0.3",
"@types/webpack-env": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"babel-eslint": "^10.1.0",
"eslint": "^5.16.0",
"eslint-plugin-jest": "^22.21.0",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react": "^7.19.0",
"razzle": "^3.0.0",
"razzle-plugin-typescript": "^3.0.0",
"ts-jest": "^24.2.0",
"tslint": "^5.20.1",
"tslint-react": "^4.1.0",
"typescript": "^3.7.3"
"ts-jest": "^24.3.0",
"typescript": "^3.8.3"
},
"jest": {
"transform": {

View File

@ -1,5 +1,19 @@
module.exports = {
plugins: ['typescript'],
plugins: [
{
name: "typescript",
options: {
useBabel: true,
useEslint: true,
forkTsChecker: {
tsconfig: "./tsconfig.json",
tslint: undefined,
watch: "./src",
typeCheck: true,
},
},
},
],
modify: (config, { target, dev }, webpack) => {
// load webfonts
rules = config.module.rules || [];

View File

@ -15,7 +15,7 @@ import * as userService from '../services/user';
const getBuildingsByLocation = asyncController(async (req: express.Request, res: express.Response) => {
const { lng, lat } = req.query;
try {
const result = await buildingService.queryBuildingsAtPoint(lng, lat);
const result = await buildingService.queryBuildingsAtPoint(Number(lng), Number(lat));
res.send(result);
} catch (error) {
console.error(error);
@ -27,7 +27,7 @@ const getBuildingsByLocation = asyncController(async (req: express.Request, res:
const getBuildingsByReference = asyncController(async (req: express.Request, res: express.Response) => {
const { key, id } = req.query;
try {
const result = await buildingService.queryBuildingsByReference(key, id);
const result = await buildingService.queryBuildingsByReference(String(key), String(id));
res.send(result);
} catch (error) {
console.error(error);
@ -53,7 +53,7 @@ const updateBuildingById = asyncController(async (req: express.Request, res: exp
await updateBuilding(req, res, req.session.user_id);
} else if (req.query.api_key) {
try {
const user = await userService.authAPIUser(req.query.api_key);
const user = await userService.authAPIUser(String(req.query.api_key));
await updateBuilding(req, res, user.user_id);
} catch(err) {
console.error(err);
@ -106,7 +106,7 @@ const getBuildingLikeById = asyncController(async (req: express.Request, res: ex
}
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
try {
const like = await buildingService.getBuildingLikeById(buildingId, req.session.user_id);
@ -146,7 +146,7 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
if(error instanceof UserError) {
throw new ApiUserError(error.message, error);
}
throw error;
}

View File

@ -5,8 +5,8 @@ import * as leadersService from '../services/leaderboard';
const getLeaders = asyncController(async (req: express.Request, res: express.Response) => {
try {
const number_limit = req.query.number_limit;
const time_limit = req.query.time_limit;
const number_limit = Number(req.query.number_limit);
const time_limit = Number(req.query.time_limit);
const result = await leadersService.getLeaders(number_limit, time_limit);
res.send({
leaders: result

View File

@ -1,35 +1,42 @@
import db from '../../db';
async function getLeaders(number_limit: number, time_limit: number) {
// Hard constraint on number of users returned
const max_limit = 100;
number_limit = Math.min(number_limit, max_limit);
try {
let leaders;
if(time_limit > 0){
return await db.manyOrNone(
`SELECT count(log_id) as number_edits, username
FROM logs, users
WHERE logs.user_id=users.user_id
AND CURRENT_TIMESTAMP::DATE - log_timestamp::DATE <= $1
AND NOT (users.username = 'casa_friendly_robot')
AND NOT (users.username = 'colouringlondon')
GROUP by users.username
ORDER BY number_edits DESC
LIMIT $2`, [time_limit, number_limit]
);
}else{
return await db.manyOrNone(
`SELECT count(log_id) as number_edits, username
FROM logs, users
WHERE logs.user_id=users.user_id
AND NOT (users.username = 'casa_friendly_robot')
AND NOT (users.username = 'colouringlondon')
GROUP by users.username
ORDER BY number_edits DESC
LIMIT $1`, [number_limit]
leaders = await db.manyOrNone(
`SELECT count(log_id) as number_edits, username
FROM logs, users
WHERE logs.user_id = users.user_id
AND CURRENT_TIMESTAMP::DATE - log_timestamp::DATE <= $1
AND NOT (users.username = 'casa_friendly_robot')
AND NOT (users.username = 'colouringlondon')
GROUP by users.username
ORDER BY number_edits DESC
LIMIT $2`, [time_limit, number_limit]
);
}
} else {
leaders = await db.manyOrNone(
`SELECT count(log_id) as number_edits, username
FROM logs, users
WHERE logs.user_id = users.user_id
AND NOT (users.username = 'casa_friendly_robot')
AND NOT (users.username = 'colouringlondon')
GROUP by users.username
ORDER BY number_edits DESC
LIMIT $1`, [number_limit]
);
}
return leaders.map(d => {
return {username: d.username, number_edits: Number(d.number_edits)};
})
} catch(error) {
console.error(error);
return [];
console.error(error);
return [];
}
}

View File

@ -10,6 +10,7 @@ import { Building } from './models/building';
import { User } from './models/user';
import AboutPage from './pages/about';
import ChangesPage from './pages/changes';
import CodeOfConductPage from './pages/code-of-conduct';
import ContactPage from './pages/contact';
import ContributorAgreementPage from './pages/contributor-agreement';
import DataAccuracyPage from './pages/data-accuracy';
@ -53,7 +54,7 @@ class App extends React.Component<AppProps, AppState> {
constructor(props: Readonly<AppProps>) {
super(props);
this.state = {
user: props.user
};
@ -89,7 +90,6 @@ class App extends React.Component<AppProps, AppState> {
<Header user={this.state.user} animateLogo={true} />
</Route>
</Switch>
<main>
<Switch>
<Route exact path="/about.html" component={AboutPage} />
<Route exact path="/login.html">
@ -114,6 +114,7 @@ class App extends React.Component<AppProps, AppState> {
<Route exact path="/data-accuracy.html" component={DataAccuracyPage} />
<Route exact path="/data-extracts.html" component={DataExtracts} />
<Route exact path="/contact.html" component={ContactPage} />
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
<Route exact path="/history.html" component={ChangesPage} />
<Route exact path={App.mapAppPaths} render={(props) => (
@ -127,7 +128,6 @@ class App extends React.Component<AppProps, AppState> {
)} />
<Route component={NotFound} />
</Switch>
</main>
</Fragment>
);
}

View File

@ -6,7 +6,7 @@ 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 LikeContainer from './data-containers/like';
import DynamicsContainer from './data-containers/dynamics';
import LocationContainer from './data-containers/location';
import PlanningContainer from './data-containers/planning';
import SizeContainer from './data-containers/size';
@ -44,7 +44,7 @@ const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
return <UseContainer
{...props}
inactive={false}
title="Land Use"
title="Current Use"
intro="How are buildings used, and how does use change over time? Coming soon…"
help="https://pages.colouring.london/use"
/>;
@ -52,7 +52,7 @@ const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
return <TypeContainer
{...props}
inactive={false}
title="Type"
title="Original Use"
intro="How were buildings previously used?"
help="https://www.pages.colouring.london/buildingtypology"
/>;
@ -107,7 +107,6 @@ const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
title="Community"
intro="How does this building work for the local community?"
help="https://pages.colouring.london/community"
inactive={true}
/>;
case 'planning':
return <PlanningContainer
@ -116,12 +115,13 @@ const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
intro="Planning controls relating to protection and reuse."
help="https://pages.colouring.london/planning"
/>;
case 'like':
return <LikeContainer
case 'dynamics':
return <DynamicsContainer
{...props}
title="Like Me!"
intro="Do you like the building and think it contributes to the city?"
help="https://pages.colouring.london/likeme"
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" />;

View File

@ -1,25 +1,36 @@
/**
* Data categories
*/
.data-category-list {
padding: 0 0 0.75rem;
text-align: center;
.data-category-list {
padding: 0px 0 10px 9px;
list-style: none;
margin: 0 0 0 0.2rem;
text-align: center;
margin: 0;
text-align: left;
max-width: 480px;
}
.navbar .data-category-list {
padding: 0px 0 0px 15px;
}
.data-category-list li {
position: relative;
display: inline-block;
vertical-align: bottom;
width: 10rem;
height: 10rem;
margin: 0.375rem;
box-shadow: 0 0 2px 5px #ffffff;
width: 110px;
height: 110px;
margin: 2px;
box-shadow: 0 0 2px 3px #ffffff;
transition: box-shadow 0.2s;
}
.navbar .data-category-list li {
width: 105px;
height: 105px;
}
.data-category-list li:nth-child(4n) {
margin-right: 0;
}
.data-category-list li:hover {
box-shadow: 0 0 2px 5px #00ffff;
box-shadow: 0 0 2px 3px #00ffff;
z-index: 1;
}
.data-category-list a {
color: #222;
@ -41,11 +52,6 @@
.data-category-list .category {
text-align: center;
font-size: 1.4em;
font-size: 1em;
margin: 0 0 0.5em;
}
.data-category-list .description {
text-align: center;
font-size: 0.9em;
margin: 0;
}

View File

@ -12,7 +12,6 @@ const Categories: React.FC<CategoriesProps> = (props) => (
<ol className="data-category-list">
<Category
title="Location"
desc="Where's the building?"
slug="location"
help="https://pages.colouring.london/location"
inactive={false}
@ -20,8 +19,7 @@ const Categories: React.FC<CategoriesProps> = (props) => (
building_id={props.building_id}
/>
<Category
title="Land Use"
desc="What's it used for?"
title="Current Use"
slug="use"
help="https://pages.colouring.london/use"
inactive={true}
@ -29,8 +27,7 @@ const Categories: React.FC<CategoriesProps> = (props) => (
building_id={props.building_id}
/>
<Category
title="Type"
desc="Building type"
title="Original Use"
slug="type"
help="https://pages.colouring.london/buildingtypology"
inactive={false}
@ -39,7 +36,6 @@ const Categories: React.FC<CategoriesProps> = (props) => (
/>
<Category
title="Age"
desc="Age & history"
slug="age"
help="https://pages.colouring.london/age"
inactive={false}
@ -48,7 +44,6 @@ const Categories: React.FC<CategoriesProps> = (props) => (
/>
<Category
title="Size &amp; Shape"
desc="Form & scale"
slug="size"
help="https://pages.colouring.london/shapeandsize"
inactive={false}
@ -57,7 +52,6 @@ const Categories: React.FC<CategoriesProps> = (props) => (
/>
<Category
title="Construction"
desc="Methods & materials"
slug="construction"
help="https://pages.colouring.london/construction"
inactive={false}
@ -66,7 +60,6 @@ const Categories: React.FC<CategoriesProps> = (props) => (
/>
<Category
title="Streetscape"
desc="Environment"
slug="streetscape"
help="https://pages.colouring.london/greenery"
inactive={true}
@ -75,34 +68,14 @@ const Categories: React.FC<CategoriesProps> = (props) => (
/>
<Category
title="Team"
desc="Builder & designer"
slug="team"
help="https://pages.colouring.london/team"
inactive={true}
mode={props.mode}
building_id={props.building_id}
/>
<Category
title="Sustainability"
desc="Performance"
slug="sustainability"
help="https://pages.colouring.london/sustainability"
inactive={false}
mode={props.mode}
building_id={props.building_id}
/>
<Category
title="Community"
desc="Public asset?"
slug="community"
help="https://pages.colouring.london/community"
inactive={false}
mode={props.mode}
building_id={props.building_id}
/>
<Category
title="Planning"
desc="Special controls?"
slug="planning"
help="https://pages.colouring.london/planning"
inactive={true}
@ -110,10 +83,25 @@ const Categories: React.FC<CategoriesProps> = (props) => (
building_id={props.building_id}
/>
<Category
title="Like Me?"
desc="Adds to the city?"
slug="like"
help="https://pages.colouring.london/likeme"
title="Sustainability"
slug="sustainability"
help="https://pages.colouring.london/sustainability"
inactive={false}
mode={props.mode}
building_id={props.building_id}
/>
<Category
title="Dynamics"
slug="dynamics"
help="https://pages.colouring.london/dynamics"
inactive={true}
mode={props.mode}
building_id={props.building_id}
/>
<Category
title="Community"
slug="community"
help="https://pages.colouring.london/community"
inactive={false}
mode={props.mode}
building_id={props.building_id}
@ -126,7 +114,6 @@ interface CategoryProps {
building_id?: number;
slug: string;
title: string;
desc: string;
help: string;
inactive: boolean;
}
@ -147,7 +134,6 @@ const Category: React.FC<CategoryProps> = (props) => {
}>
<div className="category-title-container">
<h3 className="category">{props.title}</h3>
<p className="description">{props.desc}</p>
</div>
</NavLink>
</li>

View File

@ -1,7 +1,4 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { BackIcon }from '../components/icons';
interface ContainerHeaderProps {
cat?: string;
@ -11,9 +8,6 @@ interface ContainerHeaderProps {
const ContainerHeader: React.FunctionComponent<ContainerHeaderProps> = (props) => (
<header className={`section-header view ${props.cat ? props.cat : ''} ${props.cat ? `background-${props.cat}` : ''}`}>
<Link className="icon-button back" to={props.backLink}>
<BackIcon />
</Link>
<h2 className="h2">{props.title}</h2>
<nav className="icon-buttons">
{props.children}

View File

@ -1,6 +1,7 @@
import React, { Fragment } from 'react';
import withCopyEdit from '../data-container';
import LikeDataEntry from '../data-components/like-data-entry';
import { CategoryViewProps } from './category-view-props';
@ -9,6 +10,12 @@ import { CategoryViewProps } from './category-view-props';
*/
const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => (
<Fragment>
<LikeDataEntry
userLike={props.building_like}
totalLikes={props.building.likes_total}
mode={props.mode}
onLike={props.onLike}
/>
<p className="data-intro">{props.intro}</p>
<ul className="data-list">
<li>Is this a publicly owned building?</li>

View File

@ -1,7 +1,6 @@
import React, { Fragment } from 'react';
import { dataFields } from '../../data_fields';
import DataEntry from '../data-components/data-entry';
import SelectDataEntry from '../data-components/select-data-entry';
import withCopyEdit from '../data-container';
@ -38,7 +37,7 @@ const ConstructionView: React.FunctionComponent<CategoryViewProps> = (props) =>
<SelectDataEntry
title={dataFields.construction_core_material.title}
slug="construction_core_material"
value={props.building.construction_core_material} // check
value={props.building.construction_core_material}
tooltip={dataFields.construction_core_material.tooltip}
options={ConstructionMaterialsOptions}
mode={props.mode}
@ -47,8 +46,9 @@ const ConstructionView: React.FunctionComponent<CategoryViewProps> = (props) =>
/>
<SelectDataEntry
title={dataFields.construction_secondary_materials.title}
disabled={true}
slug="construction_secondary_materials"
value={props.building.construction_secondary_materials} // check
value={props.building.construction_secondary_materials}
tooltip={dataFields.construction_secondary_materials.tooltip}
options={ConstructionMaterialsOptions}
mode={props.mode}
@ -58,7 +58,7 @@ const ConstructionView: React.FunctionComponent<CategoryViewProps> = (props) =>
<SelectDataEntry
title={dataFields.construction_roof_covering.title}
slug="construction_roof_covering"
value={props.building.construction_roof_covering} // check
value={props.building.construction_roof_covering}
tooltip={dataFields.construction_roof_covering.tooltip}
options={RoofCoveringOptions}
mode={props.mode}
@ -67,7 +67,7 @@ const ConstructionView: React.FunctionComponent<CategoryViewProps> = (props) =>
/>
</Fragment>
);
};
};
const ConstructionContainer = withCopyEdit(ConstructionView);

View File

@ -0,0 +1,24 @@
import React, { Fragment } from 'react';
import withCopyEdit from '../data-container';
import { CategoryViewProps } from './category-view-props';
/**
* Dynamics view/edit section
*/
const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => (
<Fragment>
<p className="data-intro">{props.intro}</p>
<ul className="data-list">
<li>Under threat of demolition (partial/complete?</li>
<li>Demolition permit no. issued </li>
<li>Whole building demolitions for current year</li>
<li>Whole building demolitions since 2000</li>
<li>Pairs of construction and demolition dates for previous buildings built on any part of the site</li>
</ul>
</Fragment>
);
const DynamicsContainer = withCopyEdit(DynamicsView);
export default DynamicsContainer;

View File

@ -1,23 +0,0 @@
import React, { Fragment } from 'react';
import LikeDataEntry from '../data-components/like-data-entry';
import withCopyEdit from '../data-container';
import { CategoryViewProps } from './category-view-props';
/**
* Like view/edit section
*/
const LikeView: React.FunctionComponent<CategoryViewProps> = (props) => (
<Fragment>
<LikeDataEntry
userLike={props.building_like}
totalLikes={props.building.likes_total}
mode={props.mode}
onLike={props.onLike}
/>
</Fragment>
);
const LikeContainer = withCopyEdit(LikeView);
export default LikeContainer;

View File

@ -1,16 +1,15 @@
import { parse } from 'query-string';
import React from 'react';
import { Link, Redirect, RouteComponentProps } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import { parseJsonOrDefault } from '../../helpers';
import ErrorBox from '../components/error-box';
import { BackIcon } from '../components/icons';
import InfoBox from '../components/info-box';
import { dataFields } from '../data_fields';
import { User } from '../models/user';
import DataEntry from './data-components/data-entry';
import Sidebar from './sidebar';
import Categories from './categories';
interface MultiEditProps {
user?: User;
@ -26,6 +25,7 @@ const MultiEdit: React.FC<MultiEditProps> = (props) => {
// 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>
@ -54,39 +54,37 @@ const MultiEdit: React.FC<MultiEditProps> = (props) => {
return (
<Sidebar>
<Categories mode={'view'} />
<section className='data-section'>
<header className={`section-header view ${props.category} background-${props.category}`}>
<Link
className="icon-button back"
to={`/edit/${props.category}`}>
<BackIcon />
</Link>
<h2 className="h2">Copy {props.category} data</h2>
</header>
<form>
<div className="section-body">
<form>
{
error ?
<ErrorBox msg={error} /> :
<InfoBox msg='Click buildings one at a time to colour using the data below' />
}
{
Object.keys(data).map((key => {
const info = dataFields[key] || {};
return (
<DataEntry
title={info.title || `Unknown field (${key})`}
slug={key}
disabled={true}
value={data[key]}
/>
);
}))
}
</form>
<form className='buttons-container'>
<Link to={`/view/${props.category}`} className='btn btn-secondary'>Back to view</Link>
<Link to={`/edit/${props.category}`} className='btn btn-secondary'>Back to edit</Link>
</form>
{
Object.keys(data).map((key => {
const info = dataFields[key] || {};
return (
<DataEntry
title={info.title || `Unknown field (${key})`}
slug={key}
disabled={true}
value={data[key]}
/>
);
}))
}
</form>
<form className='buttons-container'>
<Link to={`/view/${props.category}`} className='btn btn-secondary'>Back to view</Link>
<Link to={`/edit/${props.category}`} className='btn btn-secondary'>Back to edit</Link>
</form>
</div>
</section>
</Sidebar>
);

View File

@ -2,19 +2,43 @@
* Sidebar layout
*/
.info-container {
order: 1;
position: absolute;
top: 75px;
left: 0;
bottom: 0;
width: 95%;
width: calc(100% - 40px);
border-right: 1px solid #000;
z-index: 1001;
transition: transform 0.3s;
transform: translateX(0);
}
.info-container.offscreen {
transform: translateX(-100%);
}
.info-container-collapse {
position: absolute;
right: -32px;
top: 2rem;
padding: 2.5rem 0rem;
border-radius: 0 .25rem .25rem 0;
}
@media (min-width: 990px){
.info-container {
width: 480px; /* to match .main-header menu width */
}
.info-container.offscreen {
transform: translateX(0);
}
.info-container-collapse {
display: none;
}
}
.info-container-inner {
overflow-y: auto;
height: 100%;
padding: 0 0 2em;
background: #fff;
overflow-y: auto;
height: 40%;
}
@media (min-width: 768px){
.info-container {
order: 0;
height: unset;
width: 23rem;
}
}
/**
@ -26,20 +50,19 @@
clear: both;
text-decoration: none;
color: #222;
padding: 0.75rem 0.25rem 0.5rem 0;
padding: 0.75rem 0.25rem 0.5rem 0.75rem;
z-index: 1000;
}
@media (min-width: 768px) {
.section-header {
position: sticky;
top: 0;
}
position: sticky;
top: 0;
}
.section-header h2,
.section-header .icon-buttons {
display: inline-block;
}
.section-header .h2 {
font-size: 1.5rem;
margin: 0.25rem 0;
}
.section-header .icon-buttons {
position: absolute;
right: 0;
@ -141,16 +164,16 @@
/**
* Data list sections
*/
.section-body {
margin-top: 0.75em;
padding: 0 0.75em;
}
.section-body {
margin-top: 0.75em;
padding: 0 0.75em 5em 0.75em;
min-height: 80vh;
}
.data-section .h3 {
margin: 0;
}
.data-intro {
padding: 0 0.5rem 0 2.25rem;
padding: 0 0.5rem 0 0;
margin-top: 0.5rem;
}
.data-section p {
@ -158,7 +181,7 @@
margin: 0.5rem 0;
}
.data-section ul {
padding-left: 3.333rem;
padding-left: 1.333rem;
font-size: 1rem;
}
.data-section li {
@ -191,6 +214,10 @@
.data-section select {
margin: 0 0 0.5em 0;
}
.data-section input[type="checkbox"] {
position: static;
margin-right: 0.5em;
}
.data-list dd {
margin: 0 0 0.5rem;
line-height: 1.5;

View File

@ -1,11 +1,29 @@
import React from 'react';
import React, { useState, Fragment } from 'react';
import './sidebar.css';
import { BackIcon, ForwardIcon } from '../components/icons';
const Sidebar: React.FC<{}> = (props) => (
<div id="sidebar" className="info-container">
{ props.children }
</div>
);
const Sidebar: React.FC<{}> = (props) => {
const [collapsed, setCollapsed] = useState(true);
return (
<Fragment>
<div id="sidebar" className={"info-container " + (collapsed? "offscreen": "")}>
<button className="info-container-collapse btn btn-light"
onClick={() => setCollapsed(!collapsed)}
>
{
collapsed?
<ForwardIcon />
: <BackIcon />
}
</button>
<div className="info-container-inner">
{ props.children }
</div>
</div>
</Fragment>
);
}
export default Sidebar;

View File

@ -2,7 +2,7 @@
* Mini-library of icons
*/
import { library } from '@fortawesome/fontawesome-svg-core';
import { faAngleLeft, faCaretDown, faCaretRight, faCaretUp, faCheck, faCheckDouble,
import { faAngleLeft, faAngleRight, faCaretDown, faCaretRight, faCaretUp, faCheck, faCheckDouble,
faEye, faInfoCircle, faPaintBrush, faQuestionCircle, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
@ -15,6 +15,7 @@ library.add(
faCheck,
faCheckDouble,
faAngleLeft,
faAngleRight,
faCaretDown,
faCaretUp,
faCaretRight,
@ -54,6 +55,10 @@ const BackIcon = () => (
<FontAwesomeIcon icon="angle-left" />
);
const ForwardIcon = () => (
<FontAwesomeIcon icon="angle-right" />
);
const DownIcon = () => (
<FontAwesomeIcon icon="caret-down" />
);
@ -79,6 +84,7 @@ export {
SaveIcon,
SaveDoneIcon,
BackIcon,
ForwardIcon,
DownIcon,
UpIcon,
RightIcon,

View File

@ -40,9 +40,9 @@ const LogoGrid: React.FunctionComponent = () => (
</div>
<div className="row">
<div className="cell background-sustainability"></div>
<div className="cell background-community"></div>
<div className="cell background-planning"></div>
<div className="cell background-like"></div>
<div className="cell background-dynamics"></div>
<div className="cell background-community"></div>
</div>
</div>
);

View File

@ -2,27 +2,68 @@
* Main header
*/
.main-header {
z-index: 3000;
position: absolute;
top: 0;
left: 0;
height: 76px;
width: 100%;
text-decoration: none;
border-bottom: 2px solid #222;
}
.main-header .navbar {
padding: 0.5em 0.5em 0.5em;
}
.main-header .navbar-brand {
margin: 0 1em 0 0;
}
.main-header .shorten-username {
text-overflow: '…)';
white-space: nowrap;
overflow: hidden;
display: inline-block;
vertical-align: bottom;
max-width: 70vw;
}
@media (min-width: 768px) {
.main-header .shorten-username {
max-width: 5vw;
@media (min-width: 990px){
.main-header {
width: 480px; /* to match .info-container menu width */
}
}
}
.main-header.navbar {
padding: 0;
}
.nav-header {
z-index: 1;
display: flex;
justify-content: space-between;
width: 100%;
padding: 12px;
background: #fff;
border-right: 1px solid #000;
}
.nav-header a {
padding: 0;
margin: 0;
}
.navbar .navbar-toggler {
padding: .5rem .75rem .5rem .75rem;
}
.navbar .navbar-toggler-icon {
width: 1em;
height: 1em;
}
.navbar .close {
display: inline-block;
vertical-align: middle;
height: 1em;
padding: 0 3px;
}
.main-header .navbar-collapse {
height: calc(100vh - 76px);
display: block;
overflow-y: auto;
transition: transform 0.3s;
transform: translateY(0);
background: #fff;
border-right: 1px solid #000;
}
.main-header .navbar-collapse > ul:last-child {
padding-bottom: 5rem;
}
.navbar-collapse.collapse {
transform: translateY(-100vh);
}
.navbar .nav-link {
padding: .5rem 1rem;
}
.navbar hr {
height: 2px;
width: auto;
margin: 1em;
}

View File

@ -5,6 +5,7 @@ import './header.css';
import { Logo } from './components/logo';
import { User } from './models/user';
import Categories from './building/categories';
interface HeaderProps {
@ -41,90 +42,133 @@ class Header extends React.Component<HeaderProps, HeaderState> {
render() {
return (
<header className="main-header">
<nav className="navbar navbar-light navbar-expand-lg">
<span className="navbar-brand align-self-start">
<NavLink to="/">
<Logo variant={this.props.animateLogo ? 'animated' : 'default'}/>
</NavLink>
</span>
<button className="navbar-toggler navbar-toggler-right" type="button"
<header className="main-header navbar navbar-light">
<div className="nav-header">
<NavLink to="/">
<Logo variant={this.props.animateLogo ? 'animated' : 'default'}/>
</NavLink>
<button className="navbar-toggler" type="button"
onClick={this.handleClick} aria-expanded={!this.state.collapseMenu} aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
Menu&nbsp;
{
this.state.collapseMenu ?
<span className="navbar-toggler-icon"></span>
: <span className="close">&times;</span>
}
</button>
<div className={this.state.collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<NavLink to="/view/categories" className="nav-link" onClick={this.handleNavigate}>
View/Edit Maps
</NavLink>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london">
About
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/buildingcategories">
Data Categories
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/whoisinvolved">
Who&rsquo;s Involved?
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/data-ethics">
Data Ethics
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://discuss.colouring.london">
Discuss
</a>
</li>
<li className="nav-item">
<NavLink to="/data-extracts.html" className="nav-link" onClick={this.handleNavigate}>
Downloads
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/leaderboard.html" className="nav-link" onClick={this.handleNavigate}>
Leaderboard
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/contact.html" className="nav-link" onClick={this.handleNavigate}>
Contact
</NavLink>
</li>
{
this.props.user?
(
<li className="nav-item">
<NavLink to="/my-account.html" className="nav-link" onClick={this.handleNavigate}>
Account <span className="shorten-username">({this.props.user.username})</span>
</NavLink>
</li>
):
(
<Fragment>
<li className="nav-item">
<NavLink to="/login.html" className="nav-link" onClick={this.handleNavigate}>
Log in
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/sign-up.html" className="nav-link" onClick={this.handleNavigate}>
Sign up
</NavLink>
</li>
</Fragment>
)
}
</ul>
</div>
</div>
<nav className={this.state.collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
<Categories mode='view' />
<hr />
<ul className="navbar-nav flex-column">
<li className="nav-item">
<NavLink to="/view/categories" className="nav-link" onClick={this.handleNavigate}>
View/Edit Maps
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/data-extracts.html" className="nav-link" onClick={this.handleNavigate}>
Downloads
</NavLink>
</li>
{/*
<li className="nav-item">
<NavLink to="/showcase.html" className="nav-link" onClick={this.handleNavigate}>
Showcase
</NavLink>
</li>
*/}
<li className="nav-item">
<NavLink to="/leaderboard.html" className="nav-link" onClick={this.handleNavigate}>
Leaderboard
</NavLink>
</li>
<li className="nav-item">
<a className="nav-link" href="https://discuss.colouring.london">
Discuss
</a>
</li>
{
this.props.user?
(
<li className="nav-item">
<NavLink to="/my-account.html" className="nav-link" onClick={this.handleNavigate}>
Account <span className="shorten-username">({this.props.user.username})</span>
</NavLink>
</li>
):
(
<Fragment>
<li className="nav-item">
<NavLink to="/login.html" className="nav-link" onClick={this.handleNavigate}>
Log in
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/sign-up.html" className="nav-link" onClick={this.handleNavigate}>
Sign up
</NavLink>
</li>
</Fragment>
)
}
</ul>
<hr />
<ul className="navbar-nav flex-column">
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london">
About
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/buildingcategories">
Data Categories
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/whoisinvolved">
Who&rsquo;s Involved?
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="https://pages.colouring.london/data-ethics">
Data Ethics
</a>
</li>
</ul>
<hr />
<ul className="navbar-nav flex-column">
<li className="nav-item">
<NavLink to="/contact.html" className="nav-link" onClick={this.handleNavigate}>
Contact
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/privacy-policy.html" className="nav-link" onClick={this.handleNavigate}>
Privacy Policy
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/contributor-agreement.html" className="nav-link" onClick={this.handleNavigate}>
Contributor Agreement
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/code-of-conduct.html" className="nav-link" onClick={this.handleNavigate}>
Code of Conduct
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/data-accuracy.html" className="nav-link" onClick={this.handleNavigate}>
Data Accuracy Agreement
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/ordnance-survey-uprn.html" className="nav-link" onClick={this.handleNavigate}>
Ordnance Survey terms of UPRN usage
</NavLink>
</li>
</ul>
</nav>
</header>
);

View File

@ -66,7 +66,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
async fetchLatestRevision() {
try {
const {latestRevisionId} = await apiGet(`/api/buildings/revision`);
this.increaseRevision(latestRevisionId);
} catch(error) {
console.error(error);
@ -74,7 +74,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
}
/**
* Fetches building data if a building is selected but no data provided through
* Fetches building data if a building is selected but no data provided through
* props (from server-side rendering)
*/
async fetchBuildingData() {
@ -173,13 +173,13 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
*/
colourBuilding(building: Building) {
const cat = this.props.match.params.category;
if (cat === 'like') {
this.likeBuilding(building.building_id);
} else {
const data = parseJsonOrDefault(this.getMultiEditDataString());
if (data != undefined && !Object.values(data).some(x => x == undefined)) {
this.updateBuilding(building.building_id, data);
}
@ -224,7 +224,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
<Fragment>
<Switch>
<Route exact path="/">
<Welcome />
<Sidebar>
<Welcome />
</Sidebar>
</Route>
<Route exact path="/:mode/categories/:building?">
<Sidebar>
@ -240,6 +242,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
)} />
<Route exact path="/:mode/:cat/:building?">
<Sidebar>
<Categories mode={mode || 'view'} building_id={building_id} />
<BuildingView
mode={viewEditMode}
cat={category}
@ -252,11 +255,12 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
</Route>
<Route exact path="/:mode/:cat/:building/history">
<Sidebar>
<Categories mode={mode || 'view'} building_id={building_id} />
<EditHistory building={this.state.building} />
</Sidebar>
</Route>
<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>
<ColouringMap

View File

@ -10,7 +10,7 @@
min-width: 14rem;
max-width: 14rem;
max-height: 60%;
display: flex;
flex-direction: column;
@ -28,7 +28,7 @@
.map-legend .logo {
display: none;
}
@media (min-width: 768px) {
@media (min-width: 990px) {
.map-legend .logo {
display: block;
}
@ -45,7 +45,7 @@
bottom: 1.5rem;
}
}
@media (min-width: 768px) {
@media (min-width: 990px) {
.map-legend {
bottom: 2.5rem;
}
@ -94,7 +94,7 @@
position: absolute;
bottom: 1rem;
right: 0.5rem;
height: 1rem;
width: 1rem;
line-height: 0.5;
@ -121,4 +121,4 @@
.map-legend .logo .cell {
height: 12px;
width: 12px;
}
}

View File

@ -118,11 +118,11 @@ const LEGEND_CONFIG = {
{ color: '#858ed4', text: 'Locally listed'},
]
},
community: {
title: 'Community',
dynamics: {
title: 'Dynamics',
elements: []
},
like: {
community: {
title: 'Like Me',
elements: [
{ color: '#bd0026', text: '👍👍👍👍 100+' },
@ -179,7 +179,6 @@ class Legend extends React.Component<LegendProps, LegendState> {
this.setState({collapseList: (e.target.outerHeight < 670 || e.target.outerWidth < 768)}); // magic number needs to be consistent with CSS expander-button media query
}
render() {
const details = LEGEND_CONFIG[this.props.slug] || {};
const title = details.title || "";
@ -187,7 +186,7 @@ class Legend extends React.Component<LegendProps, LegendState> {
return (
<div className="map-legend">
<Logo variant='gray' />
<Logo variant="default" />
<h4 className="h4">
{ title }
</h4>

View File

@ -1,17 +1,14 @@
.map-notice {
padding: 0.5rem 0.75rem;
background: #fff;
border: 1px solid #fff;
border-radius: 4px;
z-index: 1000;
position: absolute;
box-shadow: 0px 0px 1px 1px #222;
visibility: hidden;
}
.map-container {
flex: 1;
position: relative;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
@media (min-width: 990px) {
.map-container {
left: 480px;
}
}
.leaflet-container {
height: 100%;
@ -21,21 +18,29 @@
border: 1px solid #fff;
box-shadow: 0 0 1px 1px #222;
}
.leaflet-container .leaflet-control-attribution {
width: 100%;
background: #fff;
background: rgba(255, 255, 255, 0.7);
}
.leaflet-grab {
cursor: crosshair;
}
@media (min-width: 768px){
.map-notice {
position: absolute;
top: 3.5rem;
left: 0.5rem;
z-index: 1000;
padding: 0.5rem 0.75rem;
background: #fff;
border: 1px solid #fff;
border-radius: 4px;
box-shadow: 0px 0px 1px 1px #222;
display: none;
}
@media (min-width: 990px){
/* Only show the "Click a building ..." notice for larger screens */
.map-notice {
left: 0.5rem;
top: 3.5rem;
visibility: visible;
display: block;
}
}

View File

@ -123,7 +123,7 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
const boundaryStyleFn = () => ({color: '#bbb', fill: false});
const boundaryLayer = this.state.boundary &&
const boundaryLayer = this.state.boundary &&
<GeoJSON data={this.state.boundary} style={boundaryStyleFn}/>;
// colour-data tiles
@ -133,7 +133,7 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
size: 'size_height',
construction: 'construction_core_material',
location: 'location',
like: 'likes',
community: 'likes',
planning: 'planning_combined',
sustainability: 'sust_dec',
type: 'building_attachment_form',

View File

@ -42,7 +42,7 @@
padding: 0.25rem 0.2rem;
margin-right: 0.1rem;
}
@media (min-width: 768px) {
@media (min-width: 990px) {
/* The following is a fix (?) for the truncation of the "Search for postcode" text */
.form-inline .form-control {
width: 200px;

View File

@ -21,9 +21,8 @@
background-image: none;
border-color: #343a40;
}
@media (max-width: 768px){
@media (max-width: 990px){
.theme-switcher {
visibility: hidden;
}
}

View File

@ -33,7 +33,7 @@ const ChangesPage = (props: RouteComponentProps) => {
if(after_id) {
url = `${url}&after_id=${after_id}`;
}
if (before_id) {
url = `${url}&before_id=${before_id}`;
}
@ -47,14 +47,15 @@ const ChangesPage = (props: RouteComponentProps) => {
setPaging(paging);
}
} catch (err) {
setError('Connection problem. Please try again later...');
console.error('Connection problem. Please try again later...');
setError(err);
}
};
fetchData();
}, [props.location.search]);
return (
<article>
<section className="main-col">
@ -81,7 +82,7 @@ const ChangesPage = (props: RouteComponentProps) => {
(history?.length === 0) &&
<InfoBox msg="No changes so far"></InfoBox>
}
{
{
(history != undefined && history.length > 0) &&
history.map(entry => (
<li key={`${entry.revision_id}`} className="edit-history-list-element">

View File

@ -0,0 +1,107 @@
import React from 'react';
import InfoBox from '../components/info-box';
const CodeOfConductPage = () => (
<article>
<section className="main-col">
<h1 className="h2">Code of Conduct</h1>
<InfoBox msg="Draft code of conduct for discussion" />
<h2 className="h3">Our Pledge</h2>
<p>
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
</p>
<h2 className="h3">Our Standards</h2>
<p>
Examples of behavior that contributes to creating a positive environment
include:
</p>
<ul>
<li>Using welcoming and inclusive language</li>
<li>Being respectful of differing viewpoints and experiences</li>
<li>Gracefully accepting constructive criticism</li>
<li>Focusing on what is best for the community</li>
<li>Showing empathy towards other community members</li>
</ul>
<p>
Examples of unacceptable behavior by participants include:
</p>
<ul>
<li>The use of sexualized language or imagery and unwelcome sexual attention or</li>
advances
<li>Trolling, insulting/derogatory comments, and personal or political attacks</li>
<li>Public or private harassment</li>
<li>Publishing others' private information, such as a physical or electronic address, without explicit permission</li>
<li>Other conduct which could reasonably be considered inappropriate in a professional setting</li>
</ul>
<h2 className="h3">Our Responsibilities</h2>
<p>
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
</p>
<p>
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
</p>
<h2 className="h3">Scope</h2>
<p>
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
</p>
<h2 className="h3">Enforcement</h2>
<p>
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the project team at <a
href="mailto:team@colouring.london">team@colouring.london</a>. All complaints will
be reviewed and investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
</p>
<p>
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
</p>
<h2 className="h3">Attribution</h2>
<p>
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at <a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct.html">
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html</a>
</p>
<p>
For answers to common questions about this code of conduct, see <a
href="https://www.contributor-covenant.org/faq">https://www.contributor-covenant.org/faq</a>
</p>
</section>
</article>
);
export default CodeOfConductPage;

View File

@ -1,34 +0,0 @@
table {
table-layout: fixed;
width: 60%;
margin-left: 20%;
margin-right: 20%;
border: 1px solid black;
}
table th, td {
border: 1px solid black;
text-align: left;
padding-left: 1%;
}
table tr:nth-child(odd) {
background: #f6f8fa;
}
table tr:nth-child(1) {
background: #fff;
}
#title {
text-align: center;
padding-bottom: 1%;
}
#radiogroup {
padding: 1%;
}
input[type="radio"] {
margin: 0 2px 0 10px;
}

View File

@ -1,25 +1,23 @@
import React, { Component } from 'react';
import './leaderboard.css';
interface Leader {
number_edits: string;
username: string;
}
interface Leader {
number_edits: number;
username: string;
}
interface LeaderboardProps {
}
interface LeaderboardProps {}
interface LeaderboardState {
leaders: Leader[];
fetching: boolean;
interface LeaderboardState {
leaders: Leader[];
fetching: boolean;
//We need to track the state of the radio buttons to ensure their current state is shown correctly when the view is (re)rendered
number_limit: number;
time_limit: number;
}
//We need to track the state of the radio buttons to ensure their current state is shown correctly when the view is (re)rendered
number_limit: number;
time_limit: number;
}
class LeaderboardPage extends Component<LeaderboardProps, LeaderboardState> {
@ -27,44 +25,38 @@ class LeaderboardPage extends Component<LeaderboardProps, LeaderboardState> {
constructor(props) {
super(props);
this.state = {
leaders: [],
fetching: false,
leaders: [],
fetching: false,
number_limit: 10,
time_limit: -1
};
this.getLeaders = this.getLeaders.bind(this);
this.renderTableData = this.renderTableData.bind(this);
this.handleChange = this.handleChange.bind(this);
this.getLeaders = this.getLeaders.bind(this);
this.renderTableData = this.renderTableData.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
if(e.target.name == 'number_limit'){
this.getLeaders(e.target.value, this.state.time_limit);
this.setState({number_limit: e.target.value});
}else {
} else {
this.getLeaders(this.state.number_limit, e.target.value);
this.setState({time_limit: e.target.value});
this.setState({time_limit: e.target.value});
}
}
componentDidMount() {
this.getLeaders(this.state.number_limit, this.state.time_limit);
componentDidMount() {
this.getLeaders(this.state.number_limit, this.state.time_limit);
}
componentWillUnmount() {}
getLeaders(number_limit, time_limit) {
getLeaders(number_limit: number, time_limit: number) {
this.setState({
fetching: true
});
fetch(
'/api/leaderboard/leaders?number_limit=' + number_limit + '&time_limit='+time_limit
`/api/leaderboard/leaders?number_limit=${number_limit}&time_limit=${time_limit}`
).then(
(res) => res.json()
).then((data) => {
@ -73,7 +65,7 @@ class LeaderboardPage extends Component<LeaderboardProps, LeaderboardState> {
leaders: data.leaders,
fetching: false
});
} else {
console.error(data);
@ -91,59 +83,115 @@ class LeaderboardPage extends Component<LeaderboardProps, LeaderboardState> {
});
});
}
renderTableData() {
return this.state.leaders.map((u, i) => {
const username = u.username;
const username = u.username;
const number_edits = u.number_edits;
return (
<tr key={username}>
<td>{i+1}</td>
<td>{username}</td>
<td>{number_edits}</td>
</tr>
);
});
}
return (
<tr key={username}>
<th scope="row">{i+1}</th>
<td>{username}</td>
<td>{number_edits.toLocaleString()}</td>
</tr>
);
});
}
render() {
return(
<div>
<form id="radiogroup">
<div id="number-radiogroup" >
<p>Select number of users to be displayed: <br/>
<input type="radio" name="number_limit" value="10" onChange={this.handleChange} checked={10 == this.state.number_limit} />10
<input type="radio" name="number_limit" value="100" onChange={this.handleChange} checked={100 == this.state.number_limit} />100
</p>
</div>
<div id="time-radiogroup" >
<p>Select time period: <br/>
<input type="radio" name="time_limit" value="-1" onChange={this.handleChange} checked={-1 == this.state.time_limit} /> All time
<input type="radio" name="time_limit" value="7" onChange={this.handleChange} checked={7 == this.state.time_limit} /> Last 7 days
<input type="radio" name="time_limit" value="30" onChange={this.handleChange} checked={30 == this.state.time_limit} /> Last 30 days
</p>
</div>
</form>
<h1 id='title'>Leader Board</h1>
<table id='leaderboard'>
<tbody>
<tr>
<th>Rank</th>
<th>Username</th>
<th>Contributions</th>
</tr>
{this.renderTableData()}
</tbody>
</table>
</div>
);
return (
<article>
<section className="main-col">
<h1 className="h2">Leaderboard</h1>
<form>
<label>Select number of users to be displayed</label>
<div className="form-group">
<div className="form-check-inline">
<input
type="radio"
name="number_limit"
id="number_10"
className="form-check-input"
value="10"
onChange={this.handleChange}
checked={10 == this.state.number_limit}
/>
<label className="form-check-label" htmlFor="number_10">10</label>
</div>
<div className="form-check-inline">
<input
type="radio"
name="number_limit"
id="number_100"
className="form-check-input"
value="100"
onChange={this.handleChange}
checked={100 == this.state.number_limit}
/>
<label className="form-check-label" htmlFor="number_100">100</label>
</div>
</div>
<label>Select time period</label>
<div className="form-group">
<div className="form-check-inline">
<input
type="radio"
name="time_limit"
id="time_all"
className="form-check-input"
value="-1"
onChange={this.handleChange}
checked={-1 == this.state.time_limit}
/>
<label className="form-check-label" htmlFor="time_all">All time</label>
</div>
<div className="form-check-inline">
<input
type="radio"
name="time_limit"
id="time_7"
className="form-check-input"
value="7"
onChange={this.handleChange}
checked={7 == this.state.time_limit}
/>
<label className="form-check-label" htmlFor="time_7">Last 7 days</label>
</div>
<div className="form-check-inline">
<input
type="radio"
name="time_limit"
id="time_30"
className="form-check-input"
value="30"
onChange={this.handleChange}
checked={30 == this.state.time_limit}
/>
<label className="form-check-label" htmlFor="time_30">Last 30 days</label>
</div>
</div>
</form>
<table className="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Contributions</th>
</tr>
</thead>
<tbody>
{this.renderTableData()}
</tbody>
</table>
</section>
</article>
);
}
}
export default LeaderboardPage;

View File

@ -1,36 +1,6 @@
/**
* Welcome jumbotron
*/
.welcome-float {
position: absolute;
z-index: 10000;
top: 0;
width: 100%;
max-height: 95%;
max-height: calc(100%-2em);
border-radius: 0;
overflow-y: auto;
.welcome.section-body {
padding-bottom: 0;
}
.welcome-float.jumbotron {
padding: 1em 2.5em 1.5em;
background: #fff;
background-color: rgba(255,255,255,0.95);
}
@media (min-width: 768px){
.welcome-float {
left: 50%;
margin-left: -22.5em;
width: 45em;
top: 1em;
}
}
.welcome-float .lead {
font-size: 1.2em;
}
.welcome-float .lead a {
color: #333;
border-bottom-color: #333;
}

View File

@ -4,23 +4,45 @@ import { Link } from 'react-router-dom';
import './welcome.css';
const Welcome = () => (
<div className="jumbotron welcome-float">
<h1 className="h1">Welcome to Colouring London</h1>
<p className="lead">
Colouring London is a knowledge exchange platform set up by University College London to help make the city more sustainable. It provides open statistical data on the characteristics of the city's buildings and on the dynamic behaviour of the stock. We're working to collate, collect, generate, verify over fifty types of data and to visualise many of these datasets.
<div className="section-body welcome">
<h1 className="h2">Welcome to Colouring London!</h1>
<p>
We collect and provide open data about buildings.
</p>
<p className="lead">
Our information comes from many different sources. As we are unable to vouch for data accuracy, we are currently experimenting with a range of features including 'data source', 'edit history', and 'entry verification', to assist you in checking reliability and judging how suitable the data are for your intended use. Your help in checking and adding data is very much appreciated.
<p>
Colouring London is a knowledge exchange platform set up by University College
London. It provides open statistical data about the city's buildings and the
dynamic behaviour of the stock. We're working to collate, collect, generate,
verify over fifty types of data and to visualise many of these datasets.
</p>
<p className="lead">
All data we collect are made <Link to="/data-extracts.html">openly available</Link>. We just ask you to credit Colouring London and read our <a href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> when using or sharing our data, maps or <a href="https://github.com/tomalrussell/colouring-london">code</a>.
<p>
Our information comes from many different sources. As we are unable to vouch for
data accuracy, we are experimenting with how to present data sources, how data
are edited over time, and how to ask for data verification, to help you to check
reliability and judge how suitable the data are for your intended use. Your help
in checking and adding data is very much appreciated.
</p>
<p>
All data we collect are made <Link to="/data-extracts.html">openly
available</Link>. We ask you to credit Colouring London and read our <a
href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> when
using or sharing our data, maps or <a
href="https://github.com/colouring-london/colouring-london">code</a>.
</p>
<Link to="/view/categories"
className="btn btn-outline-dark btn-lg btn-block">
Start Colouring Here!
</Link>
<p>
<img src="images/supporter-logos.png" alt="Colouring London collaborating organisations: The Bartlett UCL, Ordnance Survey, Historic England, Greater London Authority" />
<img src="images/supporter-logos.png" alt="Colouring London collaborating organisations: The Bartlett UCL, Ordnance Survey, Historic England, Greater London Authority" />
</p>
</div>
);

View File

@ -80,15 +80,15 @@
.background-team {
background-color: #7cbf39;
}
.background-sustainability {
.background-planning {
background-color: #57c28e;
}
.background-community {
.background-sustainability {
background-color: #6bb1e3;
}
.background-planning {
.background-dynamics {
background-color: #aaaaaa;
}
.background-like {
.background-community {
background-color: #a3916f;
}

View File

@ -55,11 +55,11 @@ p a.btn:active {
h1, h2, h3, h4 {
font-weight: normal;
}
main .h1 {
article .h1 {
font-size: 2em;
margin: 0.5em 0;
}
main .h2 {
article .h2 {
font-size: 1.5em;
margin: 0.25em 0 0.5em;
}

View File

@ -1,38 +1,31 @@
/**
* Main Layout
*/
html, body, #root {
height: 100%;
}
body {
margin: 0;
}
#root {
display: flex;
flex-direction: column;
overflow: hidden;
}
main {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
}
@media(min-width: 768px) {
main {
flex-direction: row;
justify-content: center;
}
}
/**
* Text pages
*/
article {
position: absolute;
top: 76px;
left: 0;
right: 0;
bottom: 0;
padding: 0 1rem;
max-height: 100vh;
overflow-y: auto;
}
@media (min-width: 990px) {
article {
top: 0px;
left: 480px;
}
}
article section {
overflow: hidden;
margin: 2.25em 0 4em;

View File

@ -8,7 +8,6 @@ body {
}
.h2 {
font-weight: normal;
margin: 0;
}
.h3, .h4, .h5 {
font-weight: normal;

View File

@ -57,13 +57,13 @@ const BUILDING_LAYER_DEFINITIONS = {
) as size_height`,
construction_core_material: `(
SELECT
b.core_materials,
b.construction_core_material::text as construction_core_material,
g.geometry_geom
FROM
geometries as g,
buildings as b
WHERE g.geometry_id = b.geometry_id
) as core_materials`,
) as construction_core_material`,
location: `(
SELECT
(
@ -116,7 +116,7 @@ const BUILDING_LAYER_DEFINITIONS = {
b.planning_in_conservation_area
OR b.planning_in_local_list
OR b.planning_list_cat is not null
) as planning_combined`,
) as planning_combined`,
conservation_area: `(
SELECT
g.geometry_geom

View File

@ -1,19 +0,0 @@
{
"defaultSeverity": "warning",
"rules": {
"eofline": true,
"ordered-imports": [
true,
{
"grouped-imports": true,
"groups": [
{ "name": "css", "match": "\\.css$", "order": 15 },
{ "name": "parent directories", "match": "^\\.\\.", "order": 20 },
{ "name": "current directory", "match": "^\\.", "order": 30 },
{ "name": "libraries", "match": ".*", "order": 10 }
]
}
],
"semicolon": true
}
}

View File

@ -0,0 +1,12 @@
-- Primary material (brick/clay tile, slate, steel/metal, timber, stone, glass, concrete,
-- natural-green or thatch, asphalt or other)
ALTER TABLE buildings DROP COLUMN IF EXISTS construction_core_material;
--Secondary material
ALTER TABLE buildings DROP COLUMN IF EXISTS construction_secondary_materials;
--Primary roof material
ALTER TABLE buildings DROP COLUMN IF EXISTS construction_roof_covering;
DROP TYPE IF EXISTS construction_materials;
DROP TYPE IF EXISTS roof_covering;

View File

@ -0,0 +1,42 @@
-- Launch of Constuction category
-- Fields: Core construction material, Secondary construction materials, Roof covering materials
-- Construction materials: Wood, Stone, Brick, Steel, Reinforced Concrete, Other Metal
-- Other Natural Material, Other Man-Made Material
CREATE TYPE construction_materials
AS ENUM (
'Wood',
'Stone',
'Brick',
'Steel',
'Reinforced Concrete',
'Other Metal',
'Other Natural Material',
'Other Man-Made Material'
);
-- Roof covering materials: Slate, Clay Tile, Wood, Asphalt, Iron or Steel, Other Metal
-- Other Natural Material, Other Man-Made Material
CREATE TYPE roof_covering
AS ENUM (
'Slate',
'Clay Tile',
'Wood',
'Asphalt',
'Iron or Steel',
'Other Metal',
'Other Natural Material',
'Other Man-Made Material'
);
-- Core Construction Material
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS construction_core_material construction_materials;
-- Secondary Construction Materials
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS construction_secondary_materials construction_materials;
-- Main Roof Covering
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS construction_roof_covering roof_covering;

View File

@ -1,8 +0,0 @@
ALTER TABLE buildings DROP COLUMN IF EXISTS core_materials;
ALTER TABLE buildings DROP COLUMN IF EXISTS secondary_materials;
ALTER TABLE buildings DROP COLUMN IF EXISTS roof_covering;
DROP TYPE IF EXISTS construction_materials;
DROP TYPE IF EXISTS roof_covering;

View File

@ -1,38 +0,0 @@
-- Launch of Constuction category
-- Fields: Core construction material, Secondary construction materials, Roof covering materials
-- Construction materials: Wood, Stone, Brick, Steel, Reinforced Concrete, Other Metal
-- Other Natural Material, Other Man-Made Material
CREATE TYPE construction_materials
AS ENUM ('Wood',
'Stone',
'Brick',
'Steel',
'Reinforced Concrete',
'Other Metal',
'Other Natural Material',
'Other Man-Made Material');
-- Roof covering materials: Slate, Clay Tile, Wood, Asphalt, Iron or Steel, Other Metal
-- Other Natural Material, Other Man-Made Material
CREATE TYPE roof_covering
AS ENUM ('Slate',
'Clay Tile',
'Wood',
'Asphalt',
'Iron or Steel',
'Other Metal',
'Other Natural Material',
'Other Man-Made Material');
-- Core Construction Material
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS core_materials construction_materials;
-- Secondary Construction Materials
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS secondary_materials construction_materials;
-- Main Roof Covering
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS roof_covering roof_covering;

View File

@ -1,23 +0,0 @@
--NOTE Some construction category fields (insulation and glazing) are migrated in sustainability.up.1-extra see #405
--Primary material (brick/clay tile, slate, steel/metal, timber, stone, glass, concrete, natural-green or thatch, asphalt or other)
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_primary_mat;
--Secondary material
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_secondary_mat;
--Primary roof material
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_primary_roof_mat;
--Secondary roof material
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_secondary_roof_mat;
--Has building been extended y/n
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_extension;
--What kind of extensions (side, rear, loft, basement)
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_extension_type;
--Glazing type, what is most common glazing type. (unless I add into sustainability for now)
ALTER TABLE buildings DROP COLUMN IF EXISTS constrctn_glazing_type;
--Also pick up roof in this for