Merge frontend/api into universal react app
12
app/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
|
||||
coverage
|
||||
node_modules
|
||||
build
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
12898
app/package-lock.json
generated
Normal file
31
app/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "colouring-london-app",
|
||||
"version": "1.0.0",
|
||||
"description": "Render frontend (server- and client-side)",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "razzle start",
|
||||
"build": "razzle build",
|
||||
"test": "razzle test --env=jsdom",
|
||||
"start:prod": "NODE_ENV=production node build/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.3",
|
||||
"bootstrap": "^4.1.3",
|
||||
"connect-pg-simple": "^5.0.0",
|
||||
"express": "4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"leaflet": "^1.3.4",
|
||||
"razzle": "2.4.0",
|
||||
"react": "16.4.2",
|
||||
"react-dom": "16.4.2",
|
||||
"react-leaflet": "^2.0.1",
|
||||
"react-leaflet-universal": "^1.2.0",
|
||||
"react-router-dom": "4.3.1",
|
||||
"serialize-javascript": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.2.3"
|
||||
}
|
||||
}
|
23
app/public/404.html
Executable file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Not found | Colouring London</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="h2">Page not found…</h1>
|
||||
<p>
|
||||
This is a public prototype (beta) site for the Colouring London project.
|
||||
</p>
|
||||
<p>
|
||||
If you're interested to follow the project, you can
|
||||
sign up for updates or read more about the project.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<a href="/about#sign-up" class="btn btn-outline-dark btn-half">Sign up for updates</a>
|
||||
<a href="/about" class="btn btn-outline-dark btn-half">Read more about the project</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
23
app/public/500.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Error | Colouring London</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="h2">Server error…</h1>
|
||||
<p>
|
||||
This is a public prototype (beta) site for the Colouring London project.
|
||||
</p>
|
||||
<p>
|
||||
If you're interested to follow the project, you can
|
||||
sign up for updates or read more about the project.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<a href="/about#sign-up" class="btn btn-outline-dark btn-half">Sign up for updates</a>
|
||||
<a href="/about" class="btn btn-outline-dark btn-half">Read more about the project</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
app/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
app/public/favicon-96x96.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
app/public/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/public/files/colouring-london-online-exhibition.pdf
Normal file
BIN
app/public/fonts/gidole-regular-webfont.woff
Executable file
BIN
app/public/fonts/gidole-regular-webfont.woff2
Executable file
BIN
app/public/fonts/glacialindifference-regular-webfont.ttf
Executable file
BIN
app/public/fonts/glacialindifference-regular-webfont.woff
Executable file
BIN
app/public/fonts/glacialindifference-regular-webfont.woff2
Executable file
BIN
app/public/images/data-categories.png
Executable file
After Width: | Height: | Size: 69 KiB |
BIN
app/public/images/edit.png
Executable file
After Width: | Height: | Size: 254 KiB |
BIN
app/public/images/housing-outline.jpg
Executable file
After Width: | Height: | Size: 143 KiB |
BIN
app/public/images/logo-casa.png
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
app/public/images/logo-gla.png
Executable file
After Width: | Height: | Size: 8.7 KiB |
BIN
app/public/images/logo-he.png
Executable file
After Width: | Height: | Size: 6.2 KiB |
BIN
app/public/images/logo-os.png
Executable file
After Width: | Height: | Size: 25 KiB |
BIN
app/public/images/logo-ucl.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
app/public/images/london-evolution-3d-square.jpg
Executable file
After Width: | Height: | Size: 67 KiB |
BIN
app/public/images/london-evolution-3d.jpg
Executable file
After Width: | Height: | Size: 67 KiB |
BIN
app/public/images/showcase.png
Executable file
After Width: | Height: | Size: 544 KiB |
BIN
app/public/images/slide-1-welcome.png
Executable file
After Width: | Height: | Size: 136 KiB |
BIN
app/public/images/slide-2-categories.png
Executable file
After Width: | Height: | Size: 31 KiB |
BIN
app/public/images/slide-3-edit.png
Executable file
After Width: | Height: | Size: 123 KiB |
BIN
app/public/images/slide-4-view.png
Executable file
After Width: | Height: | Size: 188 KiB |
BIN
app/public/images/slide-5-download.png
Executable file
After Width: | Height: | Size: 41 KiB |
BIN
app/public/images/slide-6-showcase.png
Executable file
After Width: | Height: | Size: 126 KiB |
BIN
app/public/images/slide-7-partners.png
Executable file
After Width: | Height: | Size: 41 KiB |
BIN
app/public/images/view.png
Executable file
After Width: | Height: | Size: 680 KiB |
2
app/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
|
13
app/razzle.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
modify: (config, { target, dev }, webpack) => {
|
||||
// load webfonts
|
||||
rules = config.module.rules || [];
|
||||
rules.push({
|
||||
test: /\.(eot|svg|ttf|woff|woff2)$/,
|
||||
loader: 'file-loader?name=public/fonts/[name].[ext]'
|
||||
})
|
||||
config.module.rules = rules;
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
41
app/src/building.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { query } from './db';
|
||||
|
||||
function queryBuildingAtPoint(lng, lat) {
|
||||
return query(
|
||||
`SELECT
|
||||
b.building_id as id,
|
||||
b.building_doc as doc,
|
||||
g.geometry_id as geometry_id
|
||||
FROM buildings as b, geometries as g
|
||||
WHERE
|
||||
b.geometry_id = g.geometry_id
|
||||
AND
|
||||
ST_Intersects(
|
||||
ST_Transform(
|
||||
ST_SetSRID(ST_Point($1, $2), 4326),
|
||||
3857
|
||||
),
|
||||
g.geometry_geom
|
||||
)
|
||||
LIMIT 1
|
||||
`,
|
||||
[lng, lat]
|
||||
).then(function(data){
|
||||
const rows = data.rows
|
||||
if (rows.length){
|
||||
const id = rows[0].id
|
||||
const doc = rows[0].doc
|
||||
const geometry_id = rows[0].geometry_id
|
||||
|
||||
doc.id = id
|
||||
doc.geometry_id = geometry_id
|
||||
return doc
|
||||
}
|
||||
return null;
|
||||
}).catch(function(error){
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export { queryBuildingAtPoint };
|
18
app/src/client.js
Normal file
@ -0,0 +1,18 @@
|
||||
import BrowserRouter from 'react-router-dom/BrowserRouter';
|
||||
import React from 'react';
|
||||
import { hydrate } from 'react-dom';
|
||||
|
||||
import App from './frontend/app';
|
||||
|
||||
const data = window.__PRELOADED_STATE__;
|
||||
|
||||
hydrate(
|
||||
<BrowserRouter>
|
||||
<App user={data.user} />
|
||||
</BrowserRouter>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
17
app/src/db.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Expose query interface to database pool
|
||||
*
|
||||
* - connection details must be set in environment variables, default to:
|
||||
* PGHOST='localhost'
|
||||
* PGUSER=process.env.USER
|
||||
* PGDATABASE=process.env.USER
|
||||
* PGPASSWORD=null
|
||||
* PGPORT=5432
|
||||
*/
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool()
|
||||
|
||||
const query = (text, params) => pool.query(text, params);
|
||||
|
||||
export { pool, query }
|
245
app/src/frontend/about.js
Normal file
@ -0,0 +1,245 @@
|
||||
import React from 'react';
|
||||
|
||||
const AboutPage = () => (
|
||||
<article>
|
||||
<div className="main-col">
|
||||
<h1 className="h2">
|
||||
Can you help us capture information on every building in London?
|
||||
</h1>
|
||||
<p>
|
||||
|
||||
How many buildings are there in London? What are their characteristics? Where
|
||||
are they located and how do they contribute to the city? How adaptable are
|
||||
they? How long will they last, and what are the environmental and
|
||||
socio-economic implications of demolition?
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
Colouring London is being designed to address these questions by crowdsourcing
|
||||
and visualising information on London’s buildings. We’re releasing the
|
||||
prototype for testing in the late summer. See the slideshow below for what it
|
||||
will look like.
|
||||
|
||||
</p>
|
||||
<div className="buttons-container btn-center">
|
||||
<a className="btn btn-outline-dark btn-lg" href="#sign-up">Sign up for updates</a>
|
||||
</div>
|
||||
<div className="carousel">
|
||||
<button className="carousel-control offscreen-text back">Back</button>
|
||||
<ul className="carousel-content">
|
||||
<li><img src="images/slide-1-welcome.png" alt="Welcome" /></li>
|
||||
<li><img src="images/slide-2-categories.png" alt="Categories" /></li>
|
||||
<li><img src="images/slide-3-edit.png" alt="Add/edit data" /></li>
|
||||
<li><img src="images/slide-4-view.png" alt="View maps" /></li>
|
||||
<li><img src="images/slide-5-download.png" alt="Download data" /></li>
|
||||
<li><img src="images/slide-6-showcase.png" alt="Showcase" /></li>
|
||||
<li><img src="images/slide-7-partners.png" alt="Partners" /></li>
|
||||
</ul>
|
||||
<button className="carousel-control offscreen-text next">Next</button>
|
||||
</div>
|
||||
<div className="buttons-container btn-center">
|
||||
<a className="btn btn-outline-dark btn-lg"
|
||||
href="files/colouring-london-online-exhibition.pdf">
|
||||
View online exhibition</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div className="main-col">
|
||||
<p>
|
||||
|
||||
Colouring London is being designed and built by the Centre for Advanced Spatial
|
||||
Analysis (CASA), University College London and funded by Historic England.
|
||||
Ordnance Survey is providing building footprints required to collect the data,
|
||||
facilitated by the GLA, and giving access to its API and technical support. It
|
||||
will launch in 2019.
|
||||
|
||||
</p>
|
||||
<ul className="logo-list">
|
||||
<li>
|
||||
<a href="https://www.ucl.ac.uk/bartlett/casa/">
|
||||
<img src="images/logo-casa.png"
|
||||
alt="Centre for Advanced Spatial Analysis (CASA)" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.ucl.ac.uk/">
|
||||
<img src="images/logo-ucl.png"
|
||||
alt="University College London" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.historicengland.org.uk/">
|
||||
<img src="images/logo-he.png"
|
||||
alt="Historic England" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.ordnancesurvey.co.uk/">
|
||||
<img src="images/logo-os.png"
|
||||
alt="Ordnance Survey" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.london.gov.uk/">
|
||||
<img src="images/logo-gla.png"
|
||||
alt="Supported by the Mayor of London" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr/>
|
||||
<div className="main-col">
|
||||
<h2 className="h1">Data Categories</h2>
|
||||
<p>
|
||||
|
||||
12 categories have been chosen in consultation with specialists working in a
|
||||
range of areas, from energy analysis and sustainable urban planning and design
|
||||
to building conservation, community planning, architecture and historical
|
||||
research.
|
||||
|
||||
</p>
|
||||
<ol className="data-category-list">
|
||||
<li className="bold-yellow">
|
||||
<h3 className="category">Location</h3>
|
||||
<p className="description">Where is it?</p>
|
||||
</li>
|
||||
<li className="bright-yellow">
|
||||
<h3 className="category">Use</h3>
|
||||
<p className="description">How is it used?</p>
|
||||
</li>
|
||||
<li className="bold-orange">
|
||||
<h3 className="category">Type</h3>
|
||||
<p className="description">How was it first used?</p>
|
||||
</li>
|
||||
<li className="red">
|
||||
<h3 className="category">Age</h3>
|
||||
<p className="description">When was it built?</p>
|
||||
</li>
|
||||
<li className="pastel-pink">
|
||||
<h3 className="category">Size</h3>
|
||||
<p className="description">How big is it?</p>
|
||||
</li>
|
||||
<li className="pastel-purple">
|
||||
<h3 className="category">Construction</h3>
|
||||
<p className="description">How is it built?</p>
|
||||
</li>
|
||||
<li className="blue-grey">
|
||||
<h3 className="category">Design/Build</h3>
|
||||
<p className="description">Who built it?</p>
|
||||
</li>
|
||||
<li className="bright-green">
|
||||
<h3 className="category">Street Front</h3>
|
||||
<p className="description">How does it relate to the street?</p>
|
||||
</li>
|
||||
<li className="pastel-green">
|
||||
<h3 className="category">Greenery</h3>
|
||||
<p className="description">Is it near a tree or park?</p>
|
||||
</li>
|
||||
<li className="bright-blue">
|
||||
<h3 className="category">Protection</h3>
|
||||
<p className="description">Is it designated?</p>
|
||||
</li>
|
||||
<li className="pale-grey">
|
||||
<h3 className="category">Demolitions</h3>
|
||||
<p className="description">How many rebuilds on the site?</p>
|
||||
</li>
|
||||
<li className="pale-brown">
|
||||
<h3 className="category">Like Me?</h3>
|
||||
<p className="description">Do you like it?</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<hr/>
|
||||
<div className="main-col">
|
||||
<h2 className="h1">Once built, our platform will allow you to:</h2>
|
||||
</div>
|
||||
<section className="pale-pink">
|
||||
<div className="main-col">
|
||||
<h3 className="h2">View maps</h3>
|
||||
<p>
|
||||
|
||||
To view the data, navigate to the ‘View Maps’ page and find the category that
|
||||
interests you.
|
||||
|
||||
</p>
|
||||
<img className="border-image" src="images/slide-4-view.png"
|
||||
alt="Preview of view maps page" />
|
||||
</div>
|
||||
</section>
|
||||
<section className="pale-yellow">
|
||||
<div className="main-col">
|
||||
<h3 className="h2">Add and edit data</h3>
|
||||
<p>
|
||||
|
||||
Find a building and add or edit data for any of the 12 core categories.
|
||||
|
||||
</p>
|
||||
<img className="border-image" src="images/slide-3-edit.png"
|
||||
alt="Preview of add/edit data page" />
|
||||
</div>
|
||||
</section>
|
||||
<section className="pale-orange">
|
||||
<div className="main-col">
|
||||
<h3 className="h2">See how people are using our data</h3>
|
||||
<p>
|
||||
|
||||
Find links to visualisations and analysis, art projects and applications
|
||||
relating to the evolution of London, housing, energy, planning, heritage and
|
||||
history—or something we haven’t imagined yet.
|
||||
|
||||
</p>
|
||||
<img className="border-image" src="images/slide-6-showcase.png"
|
||||
alt="Preview of data showcase page" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pale-green">
|
||||
<div className="main-col">
|
||||
<h3 className="h2">Download, remix and reuse</h3>
|
||||
<p>
|
||||
|
||||
Access bulk downloads of data created through the project to use and reuse
|
||||
under a liberal open data license. Let us know and we’ll feature showcase
|
||||
projects on the Colouring London site.
|
||||
|
||||
</p>
|
||||
<img className="border-image" src="images/slide-5-download.png"
|
||||
alt="Preview of download page" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="main-col">
|
||||
|
||||
<form id="sign-up" action="https://tinyletter.com/colouringlondon" method="post"
|
||||
target="popupwindow"
|
||||
onsubmit="window.open(
|
||||
'https://tinyletter.com/colouringlondon',
|
||||
'popupwindow',
|
||||
'scrollbars=yes,width=800,height=600'); return true">
|
||||
<h3 className="h1">Keep in touch</h3>
|
||||
<p>
|
||||
|
||||
Receive occasional newsletters about the project as it develops. You can
|
||||
read previous newsletters in our <a
|
||||
href="https://tinyletter.com/colouringlondon/archive"
|
||||
target="_blank" rel="noopener noreferrer">newsletter archive</a>.
|
||||
|
||||
</p>
|
||||
<label for="tlemail">Enter your email address:</label>
|
||||
<input className="form-control" type="email" name="email" id="tlemail" placeholder="name@example.com" />
|
||||
<input type="hidden" value="1" name="embed"/>
|
||||
<small className="form-text text-muted">
|
||||
<a href="https://tinyletter.com" target="_blank" rel="noopener noreferrer">
|
||||
powered by TinyLetter</a>.
|
||||
We'll never share your email address.
|
||||
</small>
|
||||
<div className="buttons-container">
|
||||
<input className="btn btn-outline-dark btn-block" type="submit" value="Sign up for updates" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
export default AboutPage;
|
68
app/src/frontend/app.js
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import AboutPage from './about';
|
||||
import BetaBanner from './beta-banner';
|
||||
import Header from './header';
|
||||
import Login from './login';
|
||||
import ColouringMap from './map';
|
||||
import SignUp from './signup';
|
||||
import Welcome from './welcome';
|
||||
|
||||
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
||||
import './main.css'
|
||||
import MyAccountPage from './my-account';
|
||||
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { user: props.user };
|
||||
this.login = this.login.bind(this);
|
||||
this.logout = this.logout.bind(this);
|
||||
}
|
||||
|
||||
login(user) {
|
||||
console.log(user)
|
||||
this.setState({user: user});
|
||||
}
|
||||
|
||||
logout(user) {
|
||||
this.setState({user: undefined});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<BetaBanner />
|
||||
<Header user={this.state.user} />
|
||||
<main className="beta">
|
||||
<Switch>
|
||||
<Route exact path="/" component={Welcome} />
|
||||
<Route exact path="/about.html" component={AboutPage} />
|
||||
<Route exact path="/maps.html" component={ColouringMap} />
|
||||
<Route exact path="/login.html">
|
||||
<Login user={this.state.user} login={this.login} />
|
||||
</Route>
|
||||
<Route exact path="/sign-up.html">
|
||||
<SignUp user={this.state.user} login={this.login} />
|
||||
</Route>
|
||||
<Route exact path="/my-account.html">
|
||||
<MyAccountPage user={this.state.user} logout={this.logout} />
|
||||
</Route>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</main>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
const NotFound = () => (
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1>Not found…</h1>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
|
||||
export default App;
|
17
app/src/frontend/app.test.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import App from './app';
|
||||
|
||||
describe('<App />', () => {
|
||||
test('renders without exploding', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<MemoryRouter>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
div
|
||||
);
|
||||
});
|
||||
});
|
22
app/src/frontend/beta-banner.css
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Banner
|
||||
*/
|
||||
.beta-banner {
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
font-size: 0.8333rem;
|
||||
padding: 0.25em 1em;
|
||||
background: #505050;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
.beta-banner a:hover,
|
||||
.beta-banner a:focus,
|
||||
.beta-banner a {
|
||||
color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.beta-banner a:hover,
|
||||
.beta-banner a:focus {
|
||||
border-bottom-width: 2px;
|
||||
}
|
16
app/src/frontend/beta-banner.js
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import './beta-banner.css';
|
||||
|
||||
/**
|
||||
* Banner for beta site
|
||||
*/
|
||||
const BetaBanner = () => (
|
||||
<p className="beta-banner" role="alert" >
|
||||
|
||||
Welcome to the prototype (beta) site for Colouring London. <a
|
||||
href="http://colouringlondon.org">Find out more about the project.</a>
|
||||
|
||||
</p>
|
||||
);
|
||||
|
||||
export default BetaBanner;
|
79
app/src/frontend/building-edit.js
Normal file
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
const BuildingEdit = () => (
|
||||
<div id="legend" className="info-container">
|
||||
<h2 className="h2">Edit building data</h2>
|
||||
<form action="building-view.html" method="GET">
|
||||
<fieldset className="data-section">
|
||||
<legend
|
||||
className="h3 bullet-prefix location toggled-on"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-location">Location</legend>
|
||||
<div id="data-list-location" className="data-list collapse show">
|
||||
<label for="">Building name</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
<label for="">Building number</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
<label for="">Street</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
<label for="">Address line 2</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
<label for="">Town</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
<label for="">Postcode</label>
|
||||
<input className="form-control" type="text" value="" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="data-section">
|
||||
<legend
|
||||
className="h3 bullet-prefix age"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-age">Age</legend>
|
||||
<div id="data-list-age" className="data-list collapse">
|
||||
<label for="">Year built (best estimate)</label>
|
||||
<input className="form-control" type="number" step="1" value="2018" />
|
||||
<label for="">Year built (upper estimate)</label>
|
||||
<input className="form-control" type="number" step="1" value="2018" />
|
||||
<label for="">Year built (lower estimate)</label>
|
||||
<input className="form-control" type="number" step="1" value="2018" />
|
||||
<label for="">Facade date</label>
|
||||
<input className="form-control" type="number" step="1" value="" />
|
||||
<label for="">Source</label>
|
||||
<input className="form-control" type="text" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="data-section">
|
||||
<legend
|
||||
className="h3 bullet-prefix size"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-size">Size</legend>
|
||||
<div id="data-list-size" className="data-list collapse">
|
||||
<label for="">Attic storeys</label>
|
||||
<input className="form-control" type="number" step="1" value="0" />
|
||||
<label for="">Core storeys</label>
|
||||
<input className="form-control" type="number" step="1" value="3" />
|
||||
<label for="">Basement storeys</label>
|
||||
<input className="form-control" type="number" step="1" value="1" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="data-section">
|
||||
<legend
|
||||
className="h3 bullet-prefix like"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-like">Like Me!</legend>
|
||||
<div id="data-list-like" className="data-list collapse">
|
||||
<label for="">Like this building?</label>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input position-static" type="checkbox" checked />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div className="buttons-container">
|
||||
<a href="/building/id" className="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" className="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BuildingEdit;
|
94
app/src/frontend/building-view.js
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
|
||||
const BuildingView = () => (
|
||||
<div id="legend" class="info-container">
|
||||
<h2 class="h2">Building data</h2>
|
||||
<section class="data-section">
|
||||
<h3 class="h3 bullet-prefix location toggled-on"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-location">
|
||||
Location
|
||||
</h3>
|
||||
<p class="data-intro">
|
||||
|
||||
Section introduction of up to roughly 100 characters will take
|
||||
approx­imately this much space.
|
||||
|
||||
<a href="#">Read more</a>.
|
||||
</p>
|
||||
<dl id="data-list-location" class="data-list collapse show">
|
||||
<dt>
|
||||
Building Name
|
||||
|
||||
<span class="tooltip-hook" data-toggle="tooltip">
|
||||
?
|
||||
<div class="tooltip bs-tooltip-bottom">
|
||||
<div class="arrow"></div>
|
||||
<div class="tooltip-inner">
|
||||
|
||||
Hint tooltip content should be ~40 chars.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
<dt>Building Number</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
<dt>Street</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
<dt>Address line 2</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
<dt>Town</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
<dt>Postcode</dt>
|
||||
<dd><span class="no-data">no data</span></dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="data-section">
|
||||
<h3 class="h3 bullet-prefix age"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-age">Age</h3>
|
||||
<dl id="data-list-age" class="data-list collapse">
|
||||
<dt>Year built (best estimate)</dt>
|
||||
<dd>2018</dd>
|
||||
<dt>Year built (lower estimate)</dt>
|
||||
<dd>2018</dd>
|
||||
<dt>Year built (upper estimate)</dt>
|
||||
<dd>2018</dd>
|
||||
<dt>Date Source</dt>
|
||||
<dd>Pevsner</dd>
|
||||
<dt>Facade date</dt>
|
||||
<dd>2018</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="data-section">
|
||||
<h3 class="h3 bullet-prefix size"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-size">Size</h3>
|
||||
<dl id="data-list-size" class="data-list collapse">
|
||||
<dt>Attic storeys</dt>
|
||||
<dd>0</dd>
|
||||
<dt>Core storeys</dt>
|
||||
<dd>3</dd>
|
||||
<dt>Basement storeys</dt>
|
||||
<dd>1</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="data-section">
|
||||
<h3 class="h3 bullet-prefix like"
|
||||
data-toggle="collapse"
|
||||
data-target="#data-list-like">Like Me!</h3>
|
||||
<dl id="data-list-like" class="data-list collapse">
|
||||
<dt>Likes</dt>
|
||||
<dd> 25</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<div class="buttons-container">
|
||||
<a href="/maps" class="btn btn-secondary">Back to maps</a>
|
||||
<a href="/building/id/edit" class="btn btn-primary">Edit data</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BuildingView;
|
15
app/src/frontend/header.css
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Main header
|
||||
*/
|
||||
.main-header {
|
||||
display: block;
|
||||
min-height: 82px;
|
||||
text-decoration: none;
|
||||
border-bottom: 3px solid #222;
|
||||
}
|
||||
.main-header .navbar {
|
||||
padding: 0.75em 0.75em;
|
||||
}
|
||||
.main-header .navbar-brand {
|
||||
margin: 0 1em 0 0;
|
||||
}
|
54
app/src/frontend/header.js
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import Logo from './logo';
|
||||
import './header.css';
|
||||
|
||||
/**
|
||||
* Main Header
|
||||
*/
|
||||
function Header(props) {
|
||||
if (props.user) {
|
||||
return (
|
||||
<header className="main-header">
|
||||
<nav className="navbar navbar-light navbar-expand-md">
|
||||
<Logo />
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<NavLink to="/maps.html" className="nav-link">View Maps</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/about.html" className="nav-link">About</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/my-account.html" className="nav-link">My account (Logged in as {props.user.username})</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<header className="main-header">
|
||||
<nav className="navbar navbar-light navbar-expand-md">
|
||||
<Logo />
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<NavLink to="/maps.html" className="nav-link">View Maps</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/about.html" className="nav-link">About</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/login.html" className="nav-link">Log in</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/sign-up.html" className="nav-link">Sign up</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Header;
|
BIN
app/src/frontend/images/arrow-back.png
Executable file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/frontend/images/arrow-next.png
Executable file
After Width: | Height: | Size: 1.3 KiB |
99
app/src/frontend/login.js
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Link } from 'react-router-dom';
|
||||
|
||||
class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
show_password: ''
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const target = event.target;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const login = this.props.login
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.state),
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(
|
||||
res => res.json()
|
||||
).then(function(res){
|
||||
if (res.error) {
|
||||
console.error(res.error); // tell user
|
||||
} else {
|
||||
console.log(res); // redirect back
|
||||
fetch('/users/me').then(
|
||||
(res) => res.json()
|
||||
).then(function(user){
|
||||
console.log(user)
|
||||
login(user);
|
||||
}).catch(function(err){
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
}).catch(
|
||||
err => console.error(err)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.user) {
|
||||
return <Redirect to="/my-account.html" />
|
||||
}
|
||||
return (
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1 className="h2">Log in</h1>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<label htmlFor="username">Username*</label>
|
||||
<input name="username" id="username"
|
||||
className="form-control" type="text"
|
||||
value={this.state.username} onChange={this.handleChange}
|
||||
placeholder="not-your-real-name" required
|
||||
/>
|
||||
|
||||
<label htmlFor="password">Password</label>
|
||||
<input name="password" id="password"
|
||||
className="form-control"
|
||||
type={(this.state.show_password)? 'text': 'password'}
|
||||
value={this.state.password} onChange={this.handleChange}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="form-check">
|
||||
<input id="show_password" name="show_password"
|
||||
className="position-static" type="checkbox"
|
||||
checked={this.state.show_password}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<label htmlFor="show_password">Show password?</label>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Log In" className="btn btn-primary" />
|
||||
<Link to="sign-up.html">Sign Up</Link>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Login;
|
167
app/src/frontend/logo.css
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Logo
|
||||
*/
|
||||
.logo {
|
||||
display: block;
|
||||
width: 6em;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.logo .logotype {
|
||||
font-family: 'glacial_cl', sans-serif;
|
||||
font-size: 2rem;
|
||||
margin: 0 0 0 3px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.logo .logotype span {
|
||||
display: block;
|
||||
font-size: 0.75em;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
.logo .logotype span:first-child {
|
||||
font-size: 0.625em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.logo .grid {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
font-size: 0;
|
||||
}
|
||||
.logo .row {
|
||||
display: block;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.logo .row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.logo .cell {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0 3px 0 0;
|
||||
}
|
||||
.logo .row:nth-child(1) .cell:nth-child(1) {
|
||||
animation: pulse 47s infinite;
|
||||
animation-delay: -1.5s;
|
||||
}
|
||||
.logo .row:nth-child(1) .cell:nth-child(2) {
|
||||
animation: pulse 32s infinite;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.logo .row:nth-child(1) .cell:nth-child(3) {
|
||||
animation: pulse 49s infinite;
|
||||
animation-delay: -6s;
|
||||
}
|
||||
.logo .row:nth-child(1) .cell:nth-child(4) {
|
||||
animation: pulse 35s infinite;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
.logo .row:nth-child(2) .cell:nth-child(1) {
|
||||
animation: pulse 34s infinite;
|
||||
animation-delay: -7.2s;
|
||||
}
|
||||
.logo .row:nth-child(2) .cell:nth-child(2) {
|
||||
animation: pulse 58s infinite;
|
||||
animation-delay: -15s;
|
||||
}
|
||||
.logo .row:nth-child(2) .cell:nth-child(3) {
|
||||
animation: pulse 31s infinite;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
.logo .row:nth-child(2) .cell:nth-child(4) {
|
||||
animation: pulse 46s infinite;
|
||||
animation-delay: -4.5s;
|
||||
}
|
||||
.logo .row:nth-child(3) .cell:nth-child(1) {
|
||||
animation: pulse 32s infinite;
|
||||
animation-delay: -3.5s;
|
||||
}
|
||||
.logo .row:nth-child(3) .cell:nth-child(2) {
|
||||
animation: pulse 49s infinite;
|
||||
animation-delay: -8.5s;
|
||||
}
|
||||
.logo .row:nth-child(3) .cell:nth-child(3) {
|
||||
animation: pulse 35s infinite;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
.logo .row:nth-child(3) .cell:nth-child(4) {
|
||||
animation: pulse 34s infinite;
|
||||
animation-delay: -17s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
background-color: #ffad00;
|
||||
}
|
||||
8% {
|
||||
background-color: #ffad00;
|
||||
}
|
||||
10% {
|
||||
background-color: #72b2fe;
|
||||
}
|
||||
16% {
|
||||
background-color: #72b2fe;
|
||||
}
|
||||
18% {
|
||||
background-color: #5ec233;
|
||||
}
|
||||
24% {
|
||||
background-color: #5ec233;
|
||||
}
|
||||
26% {
|
||||
background-color: #e96762;
|
||||
}
|
||||
32% {
|
||||
background-color: #e96762;
|
||||
}
|
||||
34% {
|
||||
background-color: #e099c1;
|
||||
}
|
||||
40% {
|
||||
background-color: #e099c1;
|
||||
}
|
||||
42% {
|
||||
background-color: #7d6f94;
|
||||
}
|
||||
48% {
|
||||
background-color: #7d6f94;
|
||||
}
|
||||
50% {
|
||||
background-color: #eb7905;
|
||||
}
|
||||
56% {
|
||||
background-color: #eb7905;
|
||||
}
|
||||
58% {
|
||||
background-color: #72b889;
|
||||
}
|
||||
64% {
|
||||
background-color: #72b889;
|
||||
}
|
||||
66% {
|
||||
background-color: #f0d106;
|
||||
}
|
||||
72% {
|
||||
background-color: #f0d106;
|
||||
}
|
||||
74% {
|
||||
background-color: #a6a6a7;
|
||||
}
|
||||
80% {
|
||||
background-color: #a6a6a7;
|
||||
}
|
||||
82% {
|
||||
background-color: #918e6e;
|
||||
}
|
||||
88% {
|
||||
background-color: #918e6e;
|
||||
}
|
||||
90% {
|
||||
background-color: #ffad00;
|
||||
}
|
||||
}
|
37
app/src/frontend/logo.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './logo.css';
|
||||
|
||||
/**
|
||||
* Logo
|
||||
*/
|
||||
const Logo = () => (
|
||||
<Link to="/" className="logo navbar-brand" id="top">
|
||||
<div className="grid">
|
||||
<div className="row">
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
<div className="cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="logotype">
|
||||
<span>Colouring</span>
|
||||
<span>London</span>
|
||||
</h1>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default Logo;
|
344
app/src/frontend/main.css
Executable file
@ -0,0 +1,344 @@
|
||||
@import 'styles/typography.css';
|
||||
@import 'styles/colours.css';
|
||||
@import 'styles/content.css';
|
||||
|
||||
/**
|
||||
* Main Layout
|
||||
*/
|
||||
main.beta {
|
||||
position: relative;
|
||||
min-height: 35rem;
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
main.beta {
|
||||
position: absolute;
|
||||
top: 117px; /* 32px banner + 82px header + 3px border */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
.info-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 3rem;
|
||||
padding: 0.25em 0.75em;
|
||||
background: #fff;
|
||||
background-color: rgba(255,255,255,0.95);
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
.info-container.offscreen {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
.leaflet-right{
|
||||
left: 0;
|
||||
}
|
||||
@media (min-width: 380px){
|
||||
.info-container {
|
||||
bottom: 2rem;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
.info-container {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20rem;
|
||||
bottom: 0;
|
||||
}
|
||||
.info-container.offscreen {
|
||||
transform: translateX(-20rem);
|
||||
}
|
||||
.leaflet-right{
|
||||
left: 20rem;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text pages
|
||||
*/
|
||||
article section {
|
||||
overflow: hidden;
|
||||
margin: 2.25em 0 4em;
|
||||
padding: 2em 0 4em;
|
||||
}
|
||||
.main-col {
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #000;
|
||||
width: 100%;
|
||||
margin: 2em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* View/edit, maps legend
|
||||
*/
|
||||
.maps-list {
|
||||
list-style: none;
|
||||
padding-left: 17px;
|
||||
}
|
||||
.bullet-prefix {
|
||||
position: relative;
|
||||
padding: 0rem 0.5rem 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bullet-prefix::before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #7d7d7d;
|
||||
content: ' ';
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.bullet-prefix:hover::before,
|
||||
.bullet-prefix.toggled-on::before {
|
||||
background-color: #222;
|
||||
}
|
||||
.tooltip-hook {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: #222;
|
||||
background: #fff;
|
||||
border: 1px solid #222;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
padding: 0 0 0 1px;
|
||||
font-size: 0.8rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
.tooltip {
|
||||
display: none;
|
||||
opacity: 1;
|
||||
min-width: 11em;
|
||||
left: -3px;
|
||||
top: 25px;
|
||||
}
|
||||
.tooltip .arrow {
|
||||
left: 5px;
|
||||
}
|
||||
.tooltip-hook:hover .tooltip,
|
||||
.tooltip-hook:hover + .tooltip {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
.data-section .h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.data-intro {
|
||||
padding-left: 1.5rem;
|
||||
font-size: 0.8333rem;
|
||||
}
|
||||
.data-list {
|
||||
margin: 0rem 0 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.data-list a {
|
||||
color: #555;
|
||||
}
|
||||
.data-list a:focus,
|
||||
.data-list a:active,
|
||||
.data-list a:hover {
|
||||
color: #222;
|
||||
}
|
||||
.data-list dt,
|
||||
.data-section label {
|
||||
margin: 0;
|
||||
font-size: 0.8333rem;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
color: #555;
|
||||
}
|
||||
.data-list dd,
|
||||
.data-list input {
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.data-list .no-data {
|
||||
color: #999;
|
||||
}
|
||||
.data-legend {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.data-legend .key {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms
|
||||
*/
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="email"] {
|
||||
border-color: #999;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
input[type="number"] {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
.form-check {
|
||||
padding-left: 0;
|
||||
}
|
||||
label {
|
||||
margin: 0.5em 0 0;
|
||||
}
|
||||
.buttons-container {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
form .btn {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.buttons-container.btn-center {
|
||||
text-align: center;
|
||||
}
|
||||
.btn.btn-half {
|
||||
width: 100%;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.btn.btn-half {
|
||||
width: 49%;
|
||||
margin-left: 0;
|
||||
margin-right: 2%;
|
||||
}
|
||||
.btn.btn-half:nth-child(2n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carousel
|
||||
*/
|
||||
.carousel {
|
||||
position: relative;
|
||||
}
|
||||
.carousel-control {
|
||||
display: none;
|
||||
}
|
||||
.carousel.active .carousel-control {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1.75em;
|
||||
border: 0;
|
||||
background-color: #fff;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
.carousel.active .carousel-control:hover,
|
||||
.carousel.active .carousel-control:active,
|
||||
.carousel.active .carousel-control:focus {
|
||||
border: 0;
|
||||
outline: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.carousel button::-moz-focus-inner {
|
||||
border:0;
|
||||
}
|
||||
.carousel-control.next {
|
||||
right: -1em;
|
||||
background-image: url('images/arrow-next.png');
|
||||
}
|
||||
.carousel-control.back {
|
||||
left: -1em;
|
||||
background-image: url('images/arrow-back.png');
|
||||
}
|
||||
.carousel-content {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.carousel-content li {
|
||||
text-align: center;
|
||||
}
|
||||
.carousel.active .carousel-content li {
|
||||
display: none;
|
||||
}
|
||||
.carousel.active .carousel-content li.current {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logos
|
||||
*/
|
||||
.logo-list {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.logo-list li {
|
||||
display: inline-block;
|
||||
width: 8em;
|
||||
padding: 0 0.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logo-list li:first-child {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data categories
|
||||
*/
|
||||
.data-category-list {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
margin: 0 -0.75em;
|
||||
}
|
||||
.data-category-list li {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
width: 9em;
|
||||
height: 9em;
|
||||
margin: 0.375em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
.data-category-list .category {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin: 1.4em 0 0.5em;
|
||||
}
|
||||
.data-category-list .description {
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
}
|
8
app/src/frontend/map.css
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
.leaflet-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
43
app/src/frontend/map.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal';
|
||||
|
||||
import '../../node_modules/leaflet/dist/leaflet.css'
|
||||
import './map.css'
|
||||
|
||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||
|
||||
/**
|
||||
* Map area
|
||||
*/
|
||||
class ColouringMap extends Component {
|
||||
state = {
|
||||
lat: 51.5245255,
|
||||
lng: -0.1338422,
|
||||
zoom: 16,
|
||||
}
|
||||
|
||||
render() {
|
||||
const position = [this.state.lat, this.state.lng];
|
||||
const key = OS_API_KEY
|
||||
const tilematrixSet = 'EPSG:3857'
|
||||
const layer = 'Night 3857' // alternatively 'Light 3857'
|
||||
const url = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`
|
||||
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines.'
|
||||
return (
|
||||
<Map
|
||||
center={position}
|
||||
zoom={this.state.zoom}
|
||||
minZoom={12}
|
||||
maxZoom={18}
|
||||
doubleClickZoom={false}
|
||||
zoomControl={false}
|
||||
attributionControl={false}>
|
||||
<TileLayer url={url} attribution={attribution}/>
|
||||
<ZoomControl position="topright" />
|
||||
<AttributionControl prefix="" />
|
||||
</Map>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ColouringMap;
|
49
app/src/frontend/my-account.js
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
class MyAccountPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
const logout = this.props.logout;
|
||||
event.preventDefault();
|
||||
fetch('/logout', {
|
||||
method: 'POST'
|
||||
}).then(
|
||||
res => res.json()
|
||||
).then(function(res){
|
||||
if (res.error) {
|
||||
console.error(res.error); // tell user
|
||||
} else {
|
||||
logout();
|
||||
console.log(res); // redirect back
|
||||
}
|
||||
}).catch(
|
||||
err => console.error(err)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props && this.props.user) {
|
||||
return (
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1 className="h3">Welcome, {this.props.user.username}</h1>
|
||||
<form method="POST" action="/logout" onSubmit={this.handleSubmit}>
|
||||
<input className="btn btn-secondary" type="submit" value="Log out"/>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Redirect to="/login.html" />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MyAccountPage;
|
127
app/src/frontend/signup.js
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Link } from 'react-router-dom';
|
||||
|
||||
class SignUp extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
email: '',
|
||||
confirm_email: '',
|
||||
password: '',
|
||||
show_password: '',
|
||||
confirm_conditions: false
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const target = event.target;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
fetch('/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.state),
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(
|
||||
res => res.json()
|
||||
).then(function(res){
|
||||
if (res.error) {
|
||||
console.error(res.error); // tell user
|
||||
} else {
|
||||
console.log(res); // redirect back
|
||||
fetch('/users/me').then(function(user){
|
||||
this.props.login(user);
|
||||
}).catch(function(err){
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
}).catch(
|
||||
err => console.error(err)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.user) {
|
||||
return <Redirect to="/my-account.html" />
|
||||
}
|
||||
return (
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1 className="h2">Sign up</h1>
|
||||
<p>
|
||||
Create an account to start colouring in.
|
||||
</p>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<label htmlFor="username">Username*</label>
|
||||
<input name="username" id="username"
|
||||
className="form-control" type="text"
|
||||
value={this.state.username} onChange={this.handleChange}
|
||||
placeholder="not-your-real-name" required
|
||||
/>
|
||||
|
||||
<label htmlFor="email">Email (optional)</label>
|
||||
<input name="email" id="email"
|
||||
className="form-control" type="email"
|
||||
value={this.state.email} onChange={this.handleChange}
|
||||
placeholder="someone@example.com"
|
||||
/>
|
||||
|
||||
<label htmlFor="confirm_email">Confirm email (optional)</label>
|
||||
<input name="confirm_email" id="confirm_email"
|
||||
className="form-control" type="email"
|
||||
value={this.state.confirm_email} onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<label htmlFor="password">Password</label>
|
||||
<input name="password" id="password"
|
||||
className="form-control"
|
||||
type={(this.state.show_password)? 'text': 'password'}
|
||||
value={this.state.password} onChange={this.handleChange}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="form-check">
|
||||
<input id="show_password" name="show_password"
|
||||
className="position-static" type="checkbox"
|
||||
checked={this.state.show_password}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<label htmlFor="show_password">Show password?</label>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input id="confirm_conditions" name="confirm_conditions"
|
||||
className="position-static" type="checkbox"
|
||||
checked={this.state.confirm_conditions}
|
||||
onChange={this.handleChange}
|
||||
required />
|
||||
<label htmlFor="confirm_conditions">
|
||||
I confirm that I have read and agree to the <a
|
||||
href="/privacy-policy">privacy policy</a> and <a
|
||||
href="/user-agreement">contributor agreement</a>.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Sign Up" className="btn btn-primary" />
|
||||
<Link to="login.html">Log in</Link>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SignUp;
|
54
app/src/frontend/styles/colours.css
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Colours
|
||||
*/
|
||||
.white {
|
||||
background-color: #fff;
|
||||
}
|
||||
.bold-yellow {
|
||||
background-color: #ffad00;
|
||||
}
|
||||
.bright-yellow {
|
||||
background-color: #f0d106;
|
||||
}
|
||||
.pale-yellow {
|
||||
background-color: #fff021;
|
||||
}
|
||||
.bold-orange {
|
||||
background-color: #eb7905;
|
||||
}
|
||||
.pale-orange {
|
||||
background-color: #ffc04e;
|
||||
}
|
||||
.red {
|
||||
background-color: #e96762;
|
||||
}
|
||||
.pastel-pink {
|
||||
background-color: #e099c1;
|
||||
}
|
||||
.pale-pink {
|
||||
background-color: #ffcde5;
|
||||
}
|
||||
.pastel-purple {
|
||||
background-color: #7d6f94;
|
||||
}
|
||||
.blue-grey {
|
||||
background-color: #6f879c;
|
||||
}
|
||||
.bright-green {
|
||||
background-color: #5ec233;
|
||||
}
|
||||
.pastel-green {
|
||||
background-color: #72b889;
|
||||
}
|
||||
.pale-green {
|
||||
background-color: #73ebaf;
|
||||
}
|
||||
.bright-blue {
|
||||
background-color: #72b2fe;
|
||||
}
|
||||
.pale-grey {
|
||||
background-color: #a6a6a7;
|
||||
}
|
||||
.pale-brown {
|
||||
background-color: #918e6e;
|
||||
}
|
80
app/src/frontend/styles/content.css
Normal file
@ -0,0 +1,80 @@
|
||||
|
||||
/**
|
||||
* Text content
|
||||
*/
|
||||
img {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
.offscreen-text {
|
||||
overflow: hidden;
|
||||
text-indent: -999px;
|
||||
}
|
||||
figure img {
|
||||
max-width: 100%;
|
||||
}
|
||||
figure a:hover img,
|
||||
figure a:focus img,
|
||||
figure a:active img {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.icon-pad {
|
||||
padding: 0 4em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.icon-pad svg {
|
||||
max-width: 10em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p a,
|
||||
small a {
|
||||
color: #52a5f8;
|
||||
border-bottom: 1px solid #52a5f8;
|
||||
}
|
||||
p a:hover,
|
||||
small a:hover,
|
||||
p a:focus,
|
||||
small a:focus,
|
||||
p a:active,
|
||||
small a:active {
|
||||
color: #006fdf;
|
||||
border-bottom-color: #006fdf;
|
||||
text-decoration: none;
|
||||
}
|
||||
p a.btn:hover,
|
||||
p a.btn:focus,
|
||||
p a.btn:active {
|
||||
color: #fff;
|
||||
}
|
||||
.text-muted {
|
||||
color: #878d96 !important;
|
||||
}
|
||||
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.h2 {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em 0 0.5em;
|
||||
}
|
||||
p, li, dd {
|
||||
line-height: 1.3;
|
||||
}
|
||||
dd {
|
||||
margin: 0 0 1.5em;
|
||||
}
|
||||
small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.text-muted {
|
||||
color: #878d96 !important;
|
||||
}
|
||||
.border-image {
|
||||
border: 1px solid #000;
|
||||
}
|
26
app/src/frontend/styles/typography.css
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Typography
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
.h1, .h2, .h3, .h4, .h5 {
|
||||
font-family: 'glacial_cl', sans-serif;
|
||||
}
|
||||
.h2 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
.h3, .h4, .h5 {
|
||||
font-family: 'glacial_cl', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
28
app/src/frontend/welcome.css
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Welcome jumbotron
|
||||
*/
|
||||
.welcome-float {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
background-color: rgba(255,255,255,0.95);
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
.welcome-float.offscreen {
|
||||
opacity: 0;
|
||||
}
|
||||
.welcome-float.remove {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
.welcome-float {
|
||||
left: 50%;
|
||||
margin-left: -20em;
|
||||
width: 40em;
|
||||
top: 1em;
|
||||
}
|
||||
}
|
14
app/src/frontend/welcome.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
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">Colour in, view and download data on London's buildings</p>
|
||||
<Link to="/maps.html" className="btn btn-outline-dark btn-lg btn-block">Get Started</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Welcome;
|
26
app/src/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import app from './server';
|
||||
import http from 'http';
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
let currentApp = app;
|
||||
|
||||
server.listen(process.env.PORT || 3000, error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log('🚀 started');
|
||||
});
|
||||
|
||||
if (module.hot) {
|
||||
console.log('✅ Server-side HMR Enabled!');
|
||||
|
||||
module.hot.accept('./server', () => {
|
||||
console.log('🔁 HMR Reloading x`./server`...');
|
||||
server.removeListener('request', currentApp);
|
||||
const newApp = require('./server').default;
|
||||
server.on('request', newApp);
|
||||
currentApp = newApp;
|
||||
});
|
||||
}
|
203
app/src/server.js
Normal file
@ -0,0 +1,203 @@
|
||||
import React from 'react';
|
||||
import { StaticRouter } from 'react-router-dom';
|
||||
import express from 'express';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
const bodyParser = require('body-parser')
|
||||
const session = require('express-session')
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
|
||||
import App from './frontend/app';
|
||||
import { pool } from './db';
|
||||
import { authUser, createUser, getUserById } from './user';
|
||||
import { queryBuildingAtPoint } from './building';
|
||||
|
||||
// create server
|
||||
const server = express();
|
||||
|
||||
// reference packed assets
|
||||
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
||||
|
||||
// disable header
|
||||
server.disable('x-powered-by');
|
||||
|
||||
// serve static files
|
||||
server.use(express.static(process.env.RAZZLE_PUBLIC_DIR));
|
||||
|
||||
// parse POSTed json body
|
||||
server.use(bodyParser.json());
|
||||
|
||||
// handle user sessions
|
||||
const sess = {
|
||||
name: 'cl.session',
|
||||
store: new pgSession({
|
||||
pool: pool,
|
||||
tableName : 'user_sessions'
|
||||
}),
|
||||
secret: process.env.APP_COOKIE_SECRET,
|
||||
resave: false,
|
||||
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
|
||||
};
|
||||
if (server.get('env') === 'production') {
|
||||
// trust first proxy
|
||||
server.set('trust proxy', 1)
|
||||
// serve secure cookies
|
||||
sess.cookie.secure = true
|
||||
}
|
||||
server.use(session(sess));
|
||||
|
||||
// handle HTML routes (server-side rendered React)
|
||||
server.get('/*.html', frontendRoute);
|
||||
server.get('/', frontendRoute);
|
||||
|
||||
function frontendRoute(req, res) {
|
||||
const context = {};
|
||||
const data = {};
|
||||
if (req.session.user_id) {
|
||||
getUserById(req.session.user_id).then(function(user){
|
||||
data.user = user;
|
||||
console.log(user);
|
||||
renderHTML(context, data, req, res)
|
||||
}).catch(function(){
|
||||
renderHTML(context, data, req, res);
|
||||
});
|
||||
} else {
|
||||
renderHTML(context, data, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
function renderHTML(context, data, req, res){
|
||||
const markup = renderToString(
|
||||
<StaticRouter context={context} location={req.url}>
|
||||
<App user={data.user} />
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
if (context.url) {
|
||||
res.redirect(context.url);
|
||||
} else {
|
||||
res.status(200).send(
|
||||
`<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta charset="utf-8" />
|
||||
<title>Colouring London</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'glacial_cl';
|
||||
src: url('/fonts/glacialindifference-regular-webfont.woff2') format('woff2'),
|
||||
url('/fonts/glacialindifference-regular-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
${
|
||||
assets.client.css
|
||||
? `<link rel="stylesheet" href="${assets.client.css}">`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
process.env.NODE_ENV === 'production'
|
||||
? `<script src="${assets.client.js}" defer></script>`
|
||||
: `<script src="${assets.client.js}" defer crossorigin></script>`
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">${markup}</div>
|
||||
<script>
|
||||
window.__PRELOADED_STATE__ = ${serialize(data)}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GET building at point
|
||||
server.get('/buildings', function(req, res){
|
||||
const { lng, lat } = req.query
|
||||
queryBuildingAtPoint(lng, lat).then(function(result){
|
||||
if (result) {
|
||||
res.send(result)
|
||||
} else {
|
||||
res.status(404).send({error:'Not Found'})
|
||||
}
|
||||
}).catch(function(error){
|
||||
res.send({error:'Database error'})
|
||||
})
|
||||
})
|
||||
|
||||
// POST new user
|
||||
server.post('/users', function(req, res){
|
||||
const user = req.body;
|
||||
if (req.session.user_id) {
|
||||
res.send({error: 'Already signed in'});
|
||||
}
|
||||
|
||||
if (user.email){
|
||||
if (user.email != user.confirm_email) {
|
||||
res.send({error: "Email did not match confirmation."});
|
||||
}
|
||||
} else {
|
||||
user.email = null;
|
||||
}
|
||||
|
||||
createUser(user).then(function(result){
|
||||
if (result.user_id) {
|
||||
req.session.user_id = result.user_id;
|
||||
res.send({user_id: result.user_id});
|
||||
} else {
|
||||
req.session.user_id = undefined;
|
||||
res.send({error: result.error});
|
||||
}
|
||||
}).catch(function(err){
|
||||
console.error(err);
|
||||
res.send({error: 'Server error'})
|
||||
});
|
||||
});
|
||||
|
||||
// POST user auth
|
||||
server.post('/login', function(req, res){
|
||||
authUser(req.body.username, req.body.password).then(function(user) {
|
||||
if (user.user_id) {
|
||||
req.session.user_id = user.user_id;
|
||||
} else {
|
||||
req.session.user_id = undefined;
|
||||
}
|
||||
res.send(user);
|
||||
}).catch(function(error){
|
||||
res.send(error);
|
||||
})
|
||||
});
|
||||
|
||||
// POST user logout
|
||||
server.post('/logout', function(req, res){
|
||||
req.session.destroy(function(err){
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.send({error: 'Failed to end session'})
|
||||
}
|
||||
res.send({success: true});
|
||||
});
|
||||
});
|
||||
|
||||
// GET own user info
|
||||
server.get('/users/me', function(req, res){
|
||||
if (!req.session.user_id) {
|
||||
res.send({error: 'Must be logged in'});
|
||||
return
|
||||
}
|
||||
|
||||
getUserById(req.session.user_id).then(function(user){
|
||||
res.send(user);
|
||||
}).catch(function(error){
|
||||
res.send(error);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
export default server;
|
84
app/src/user.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { query } from './db';
|
||||
|
||||
|
||||
function createUser(user) {
|
||||
return query(
|
||||
`INSERT
|
||||
INTO users (
|
||||
user_id,
|
||||
username,
|
||||
email,
|
||||
pass
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
$1,
|
||||
$2,
|
||||
crypt($3, gen_salt('bf'))
|
||||
) RETURNING user_id
|
||||
`, [
|
||||
user.username,
|
||||
user.email,
|
||||
user.password
|
||||
]
|
||||
).then(function(data){
|
||||
return data.rows[0];
|
||||
}).catch(function(error){
|
||||
console.error('Error:', error)
|
||||
|
||||
if (error.detail.indexOf('already exists') !== -1){
|
||||
if (error.detail.indexOf('username') !== -1){
|
||||
return {error:'Username already registered'};
|
||||
} else if (error.detail.indexOf('email') !== -1) {
|
||||
return {error: 'Email already registered'};
|
||||
}
|
||||
}
|
||||
return {error: 'Database error'}
|
||||
});
|
||||
}
|
||||
|
||||
function authUser(username, password) {
|
||||
return query(
|
||||
`SELECT
|
||||
user_id,
|
||||
(
|
||||
pass = crypt($2, pass)
|
||||
) AS auth_ok
|
||||
FROM users
|
||||
WHERE
|
||||
username = $1
|
||||
`, [
|
||||
username,
|
||||
password
|
||||
]
|
||||
).then(function(data){
|
||||
const user = data.rows[0];
|
||||
if (user.auth_ok) {
|
||||
return {user_id: user.user_id}
|
||||
} else {
|
||||
return {error: 'Authentication failed'}
|
||||
}
|
||||
}).catch(function(err){
|
||||
console.error(err);
|
||||
return {error: 'Database error'};
|
||||
})
|
||||
}
|
||||
|
||||
function getUserById(user_id) {
|
||||
return query(
|
||||
`SELECT
|
||||
username, email, registered
|
||||
FROM users
|
||||
WHERE
|
||||
user_id = $1
|
||||
`, [
|
||||
user_id
|
||||
]
|
||||
).then(function(data){
|
||||
return data.rows[0];
|
||||
}).catch(function(error){
|
||||
console.error('Error:', error)
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export { getUserById, createUser, authUser }
|