commit
da4f998fb0
133
app/package-lock.json
generated
133
app/package-lock.json
generated
@ -855,7 +855,6 @@
|
|||||||
"version": "7.4.5",
|
"version": "7.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
|
||||||
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
|
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.2"
|
"regenerator-runtime": "^0.13.2"
|
||||||
},
|
},
|
||||||
@ -863,8 +862,7 @@
|
|||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.2",
|
"version": "0.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
|
||||||
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==",
|
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1037,6 +1035,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/geojson": {
|
||||||
|
"version": "7946.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
|
||||||
|
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/glob": {
|
"@types/glob": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
|
||||||
@ -1075,6 +1079,15 @@
|
|||||||
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
|
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/leaflet": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-E5k+vyE2Tv9wQsO6ZsEy08Pjd8RjHPkCzz3Ubt7feMc+5+VkbXtcZMcciczRWuMN5rFIsVywLxRhvTp7fAbbzg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/mime": {
|
"@types/mime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||||
@ -1139,6 +1152,16 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-leaflet": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-leaflet/-/react-leaflet-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-kDZ2Ky6FQxXRODBEFlq25Lu80Nc7UsDSHCmHTa22UQn2RIJxe3O443K0vzOrFyzWPpVEOmqBpfDkX9QSTBoFxg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/leaflet": "*",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-router": {
|
"@types/react-router": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.3.tgz",
|
||||||
@ -7062,6 +7085,11 @@
|
|||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"gud": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
|
||||||
|
},
|
||||||
"gzip-size": {
|
"gzip-size": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz",
|
||||||
@ -7244,25 +7272,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "4.7.2",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz",
|
||||||
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
|
"integrity": "sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"invariant": "^2.2.1",
|
"@babel/runtime": "^7.1.2",
|
||||||
"loose-envify": "^1.2.0",
|
"loose-envify": "^1.2.0",
|
||||||
"resolve-pathname": "^2.2.0",
|
"resolve-pathname": "^2.2.0",
|
||||||
"value-equal": "^0.4.0",
|
"tiny-invariant": "^1.0.2",
|
||||||
"warning": "^3.0.0"
|
"tiny-warning": "^1.0.0",
|
||||||
},
|
"value-equal": "^0.4.0"
|
||||||
"dependencies": {
|
|
||||||
"warning": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
|
|
||||||
"requires": {
|
|
||||||
"loose-envify": "^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hmac-drbg": {
|
"hmac-drbg": {
|
||||||
@ -7277,9 +7296,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hoist-non-react-statics": {
|
"hoist-non-react-statics": {
|
||||||
"version": "2.5.5",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
|
||||||
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
|
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
|
||||||
|
"requires": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"home-or-tmp": {
|
"home-or-tmp": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -7901,6 +7923,7 @@
|
|||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -9451,6 +9474,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
|
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
|
||||||
},
|
},
|
||||||
|
"mini-create-react-context": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.4.0",
|
||||||
|
"gud": "^1.0.0",
|
||||||
|
"tiny-warning": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"mini-css-extract-plugin": {
|
"mini-css-extract-plugin": {
|
||||||
"version": "0.4.5",
|
"version": "0.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.5.tgz",
|
||||||
@ -10515,7 +10548,7 @@
|
|||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "http://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
|
||||||
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c="
|
"integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13840,17 +13873,20 @@
|
|||||||
"integrity": "sha512-Wl0p9HonxGnyTly+gfiotj5f0Su/ZD7omZ5ko0ji3lEuJe4nsokbz9UcLt9CaR/C33Ha4OmzPn8I/XqwKJUs7g=="
|
"integrity": "sha512-Wl0p9HonxGnyTly+gfiotj5f0Su/ZD7omZ5ko0ji3lEuJe4nsokbz9UcLt9CaR/C33Ha4OmzPn8I/XqwKJUs7g=="
|
||||||
},
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "4.3.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
|
||||||
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==",
|
"integrity": "sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"history": "^4.7.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"hoist-non-react-statics": "^2.5.0",
|
"history": "^4.9.0",
|
||||||
"invariant": "^2.2.4",
|
"hoist-non-react-statics": "^3.1.0",
|
||||||
"loose-envify": "^1.3.1",
|
"loose-envify": "^1.3.1",
|
||||||
|
"mini-create-react-context": "^0.3.0",
|
||||||
"path-to-regexp": "^1.7.0",
|
"path-to-regexp": "^1.7.0",
|
||||||
"prop-types": "^15.6.1",
|
"prop-types": "^15.6.2",
|
||||||
"warning": "^4.0.1"
|
"react-is": "^16.6.0",
|
||||||
|
"tiny-invariant": "^1.0.2",
|
||||||
|
"tiny-warning": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isarray": {
|
"isarray": {
|
||||||
@ -13869,16 +13905,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router-dom": {
|
||||||
"version": "4.3.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.1.tgz",
|
||||||
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
|
"integrity": "sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"history": "^4.7.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"invariant": "^2.2.4",
|
"history": "^4.9.0",
|
||||||
"loose-envify": "^1.3.1",
|
"loose-envify": "^1.3.1",
|
||||||
"prop-types": "^15.6.1",
|
"prop-types": "^15.6.2",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "5.0.1",
|
||||||
"warning": "^4.0.1"
|
"tiny-invariant": "^1.0.2",
|
||||||
|
"tiny-warning": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"read-pkg": {
|
"read-pkg": {
|
||||||
@ -16114,6 +16151,16 @@
|
|||||||
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
|
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tiny-invariant": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
|
||||||
|
},
|
||||||
|
"tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
@ -17002,14 +17049,6 @@
|
|||||||
"makeerror": "1.0.x"
|
"makeerror": "1.0.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"warning": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
|
||||||
"requires": {
|
|
||||||
"loose-envify": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"watch": {
|
"watch": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz",
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
"react-leaflet": "^1.0.1",
|
"react-leaflet": "^1.0.1",
|
||||||
"react-leaflet-universal": "^1.2.0",
|
"react-leaflet-universal": "^1.2.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^5.0.1",
|
||||||
"serialize-javascript": "^1.7.0",
|
"serialize-javascript": "^1.7.0",
|
||||||
"sharp": "^0.21.3"
|
"sharp": "^0.21.3"
|
||||||
},
|
},
|
||||||
@ -45,6 +45,7 @@
|
|||||||
"@types/prop-types": "^15.7.1",
|
"@types/prop-types": "^15.7.1",
|
||||||
"@types/react": "^16.9.1",
|
"@types/react": "^16.9.1",
|
||||||
"@types/react-dom": "^16.8.5",
|
"@types/react-dom": "^16.8.5",
|
||||||
|
"@types/react-leaflet": "^2.4.0",
|
||||||
"@types/react-router-dom": "^4.3.4",
|
"@types/react-router-dom": "^4.3.4",
|
||||||
"@types/webpack-env": "^1.14.0",
|
"@types/webpack-env": "^1.14.0",
|
||||||
"babel-eslint": "^10.0.2",
|
"babel-eslint": "^10.0.2",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Client-side entry point to shared frontend React App
|
* Client-side entry point to shared frontend React App
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import BrowserRouter from 'react-router-dom/BrowserRouter';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { hydrate } from 'react-dom';
|
import { hydrate } from 'react-dom';
|
||||||
|
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Route, Switch, Link } from 'react-router-dom';
|
import { Route, Switch, Link } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { parse } from 'query-string';
|
|
||||||
|
|
||||||
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import BuildingView from './building/building-view';
|
|
||||||
import ColouringMap from './map/map';
|
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import MultiEdit from './building/multi-edit';
|
|
||||||
import Categories from './building/categories';
|
|
||||||
import Sidebar from './building/sidebar';
|
|
||||||
|
|
||||||
import AboutPage from './pages/about';
|
import AboutPage from './pages/about';
|
||||||
import ContributorAgreementPage from './pages/contributor-agreement';
|
import ContributorAgreementPage from './pages/contributor-agreement';
|
||||||
import PrivacyPolicyPage from './pages/privacy-policy';
|
import PrivacyPolicyPage from './pages/privacy-policy';
|
||||||
import Welcome from './pages/welcome';
|
|
||||||
|
|
||||||
import Login from './user/login';
|
import Login from './user/login';
|
||||||
import MyAccountPage from './user/my-account';
|
import MyAccountPage from './user/my-account';
|
||||||
import SignUp from './user/signup';
|
import SignUp from './user/signup';
|
||||||
|
|
||||||
import ForgottenPassword from './user/forgotten-password';
|
import ForgottenPassword from './user/forgotten-password';
|
||||||
import PasswordReset from './user/password-reset';
|
import PasswordReset from './user/password-reset';
|
||||||
|
|
||||||
import { parseCategoryURL } from '../parse';
|
import MapApp from './map-app';
|
||||||
|
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
user?: any;
|
||||||
|
building?: any;
|
||||||
|
building_like?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App component
|
* App component
|
||||||
@ -39,35 +38,28 @@ import { parseCategoryURL } from '../parse';
|
|||||||
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
||||||
* child components to navigate without a full page reload.
|
* child components to navigate without a full page reload.
|
||||||
*/
|
*/
|
||||||
class App extends React.Component<any, any> { // TODO: add proper types
|
class App extends React.Component<AppProps, any> { // TODO: add proper types
|
||||||
static propTypes = { // TODO: generate propTypes from TS
|
static propTypes = { // TODO: generate propTypes from TS
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
building: PropTypes.object,
|
building: PropTypes.object,
|
||||||
building_like: PropTypes.bool
|
building_like: PropTypes.bool
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Readonly<AppProps>) {
|
||||||
super(props);
|
super(props);
|
||||||
// set building revision id, default 0
|
|
||||||
const rev = (props.building)? +props.building.revision_id : 0;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
user: props.user,
|
user: props.user
|
||||||
building: props.building,
|
|
||||||
building_like: props.building_like,
|
|
||||||
revision_id: rev
|
|
||||||
};
|
};
|
||||||
this.login = this.login.bind(this);
|
this.login = this.login.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
this.logout = this.logout.bind(this);
|
this.logout = this.logout.bind(this);
|
||||||
this.selectBuilding = this.selectBuilding.bind(this);
|
|
||||||
this.colourBuilding = this.colourBuilding.bind(this);
|
|
||||||
this.increaseRevision = this.increaseRevision.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login(user) {
|
login(user) {
|
||||||
if (user.error) {
|
if (user.error) {
|
||||||
this.logout();
|
this.logout();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.setState({user: user});
|
this.setState({user: user});
|
||||||
}
|
}
|
||||||
@ -80,171 +72,12 @@ class App extends React.Component<any, any> { // TODO: add proper types
|
|||||||
this.setState({user: undefined});
|
this.setState({user: undefined});
|
||||||
}
|
}
|
||||||
|
|
||||||
increaseRevision(revisionId) {
|
|
||||||
revisionId = +revisionId;
|
|
||||||
// bump revision id, only ever increasing
|
|
||||||
if (revisionId > this.state.revision_id){
|
|
||||||
this.setState({revision_id: revisionId})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectBuilding(building) {
|
|
||||||
this.increaseRevision(building.revision_id);
|
|
||||||
// get UPRNs and update
|
|
||||||
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then((res) => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error(res);
|
|
||||||
} else {
|
|
||||||
building.uprns = res.uprns;
|
|
||||||
this.setState({building: building});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({building: building});
|
|
||||||
});
|
|
||||||
|
|
||||||
// get if liked and update
|
|
||||||
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then((res) => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error(res);
|
|
||||||
} else {
|
|
||||||
this.setState({building_like: res.like});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({building_like: false});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colour building
|
|
||||||
*
|
|
||||||
* Used in multi-edit mode to colour buildings on map click
|
|
||||||
*
|
|
||||||
* Pulls data from URL to form update
|
|
||||||
*
|
|
||||||
* @param {object} building
|
|
||||||
*/
|
|
||||||
colourBuilding(building) {
|
|
||||||
const cat = parseCategoryURL(window.location.pathname);
|
|
||||||
const q = parse(window.location.search);
|
|
||||||
const data = (cat === 'like')? {like: true}: JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
|
|
||||||
if (cat === 'like'){
|
|
||||||
this.likeBuilding(building.building_id)
|
|
||||||
} else {
|
|
||||||
this.updateBuilding(building.building_id, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
likeBuilding(buildingId) {
|
|
||||||
fetch(`/api/buildings/${buildingId}/like.json`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({like: true})
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then(function(res){
|
|
||||||
if (res.error) {
|
|
||||||
console.error({error: res.error})
|
|
||||||
} else {
|
|
||||||
this.increaseRevision(res.revision_id);
|
|
||||||
}
|
|
||||||
}.bind(this)).catch(
|
|
||||||
(err) => console.error({error: err})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBuilding(buildingId, data){
|
|
||||||
fetch(`/api/buildings/${buildingId}.json`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then(res => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error({error: res.error})
|
|
||||||
} else {
|
|
||||||
this.increaseRevision(res.revision_id);
|
|
||||||
}
|
|
||||||
}).catch(
|
|
||||||
(err) => console.error({error: err})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const building_id = (this.state.building)?
|
|
||||||
this.state.building.building_id
|
|
||||||
: 2503371 // Default to UCL main building. TODO use last selected if any
|
|
||||||
const building = this.state.building;
|
|
||||||
const building_like = this.state.building_like;
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Header user={this.state.user} />
|
<Header user={this.state.user} />
|
||||||
<main>
|
<main>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
|
||||||
<Welcome />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/view/categories.html">
|
|
||||||
<Sidebar>
|
|
||||||
<Categories mode="view" building_id={building_id} />
|
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/edit/categories.html">
|
|
||||||
<Sidebar>
|
|
||||||
<Categories mode="edit" building_id={building_id} />
|
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/multi-edit/:cat.html" render={(props) => (
|
|
||||||
<MultiEdit
|
|
||||||
{...props}
|
|
||||||
user={this.state.user}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
<Route exact path="/:mode/:cat/building/:building.html" render={(props) => (
|
|
||||||
<BuildingView
|
|
||||||
mode={props.match.params.mode}
|
|
||||||
cat={props.match.params.cat}
|
|
||||||
building={this.state.building}
|
|
||||||
building_like={this.state.building_like}
|
|
||||||
selectBuilding={this.selectBuilding}
|
|
||||||
user={this.state.user}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
</Switch>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/(multi-edit.*|edit.*|view.*)?" render={(props) => (
|
|
||||||
<ColouringMap
|
|
||||||
{...props}
|
|
||||||
building={this.state.building}
|
|
||||||
revision_id={this.state.revision_id}
|
|
||||||
selectBuilding={this.selectBuilding}
|
|
||||||
colourBuilding={this.colourBuilding}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
<Route exact path="/about.html" component={AboutPage} />
|
<Route exact path="/about.html" component={AboutPage} />
|
||||||
<Route exact path="/login.html">
|
<Route exact path="/login.html">
|
||||||
<Login user={this.state.user} login={this.login} />
|
<Login user={this.state.user} login={this.login} />
|
||||||
@ -263,6 +96,14 @@ class App extends React.Component<any, any> { // TODO: add proper types
|
|||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
||||||
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
||||||
|
<Route exact path={["/", "/:mode(view|edit|multi-edit)/:category/:building(\\d+)?"]} render={(props) => (
|
||||||
|
<MapApp
|
||||||
|
{...props}
|
||||||
|
building={this.props.building}
|
||||||
|
building_like={this.props.building_like}
|
||||||
|
user={this.state.user}
|
||||||
|
/>
|
||||||
|
)} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Sidebar from './sidebar';
|
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
|
|
||||||
|
|
||||||
@ -11,12 +10,12 @@ interface BuildingNotFoundProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BuildingNotFound: React.FunctionComponent<BuildingNotFoundProps> = (props) => (
|
const BuildingNotFound: React.FunctionComponent<BuildingNotFoundProps> = (props) => (
|
||||||
<Sidebar>
|
<Fragment>
|
||||||
<InfoBox msg="We can't find that one anywhere - try the map again?" />
|
<InfoBox msg="We can't find that one anywhere - try the map again?" />
|
||||||
<div className="buttons-container ml-3 mr-3">
|
<div className="buttons-container ml-3 mr-3">
|
||||||
<Link to={`/${props.mode}/categories.html`} className="btn btn-secondary">Back to categories</Link>
|
<Link to={`/${props.mode}/categories`} className="btn btn-secondary">Back to categories</Link>
|
||||||
</div>
|
</div>
|
||||||
</Sidebar>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
BuildingNotFound.propTypes = {
|
BuildingNotFound.propTypes = {
|
||||||
|
@ -21,15 +21,11 @@ import LikeContainer from './data-containers/like';
|
|||||||
* @param props
|
* @param props
|
||||||
*/
|
*/
|
||||||
const BuildingView = (props) => {
|
const BuildingView = (props) => {
|
||||||
if (typeof(props.building) === "undefined"){
|
|
||||||
return <BuildingNotFound mode="view" />
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (props.cat) {
|
switch (props.cat) {
|
||||||
case 'location':
|
case 'location':
|
||||||
return <LocationContainer
|
return <LocationContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Location"
|
title="Location"
|
||||||
help="https://pages.colouring.london/location"
|
help="https://pages.colouring.london/location"
|
||||||
intro="Where are the buildings? Address, location and cross-references."
|
intro="Where are the buildings? Address, location and cross-references."
|
||||||
@ -37,7 +33,7 @@ const BuildingView = (props) => {
|
|||||||
case 'use':
|
case 'use':
|
||||||
return <UseContainer
|
return <UseContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
inactive={true}
|
inactive={true}
|
||||||
title="Land Use"
|
title="Land Use"
|
||||||
intro="How are buildings used, and how does use change over time? Coming soon…"
|
intro="How are buildings used, and how does use change over time? Coming soon…"
|
||||||
@ -46,7 +42,7 @@ const BuildingView = (props) => {
|
|||||||
case 'type':
|
case 'type':
|
||||||
return <TypeContainer
|
return <TypeContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
inactive={true}
|
inactive={true}
|
||||||
title="Type"
|
title="Type"
|
||||||
intro="How were buildings previously used? Coming soon…"
|
intro="How were buildings previously used? Coming soon…"
|
||||||
@ -55,7 +51,7 @@ const BuildingView = (props) => {
|
|||||||
case 'age':
|
case 'age':
|
||||||
return <AgeContainer
|
return <AgeContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Age"
|
title="Age"
|
||||||
help="https://pages.colouring.london/age"
|
help="https://pages.colouring.london/age"
|
||||||
intro="Building age data can support energy analysis and help predict long-term change."
|
intro="Building age data can support energy analysis and help predict long-term change."
|
||||||
@ -63,7 +59,7 @@ const BuildingView = (props) => {
|
|||||||
case 'size':
|
case 'size':
|
||||||
return <SizeContainer
|
return <SizeContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Size & Shape"
|
title="Size & Shape"
|
||||||
intro="How big are buildings?"
|
intro="How big are buildings?"
|
||||||
help="https://pages.colouring.london/shapeandsize"
|
help="https://pages.colouring.london/shapeandsize"
|
||||||
@ -71,7 +67,7 @@ const BuildingView = (props) => {
|
|||||||
case 'construction':
|
case 'construction':
|
||||||
return <ConstructionContainer
|
return <ConstructionContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Construction"
|
title="Construction"
|
||||||
intro="How are buildings built? Coming soon…"
|
intro="How are buildings built? Coming soon…"
|
||||||
help="https://pages.colouring.london/construction"
|
help="https://pages.colouring.london/construction"
|
||||||
@ -80,7 +76,7 @@ const BuildingView = (props) => {
|
|||||||
case 'team':
|
case 'team':
|
||||||
return <TeamContainer
|
return <TeamContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Team"
|
title="Team"
|
||||||
intro="Who built the buildings? Coming soon…"
|
intro="Who built the buildings? Coming soon…"
|
||||||
help="https://pages.colouring.london/team"
|
help="https://pages.colouring.london/team"
|
||||||
@ -89,7 +85,7 @@ const BuildingView = (props) => {
|
|||||||
case 'sustainability':
|
case 'sustainability':
|
||||||
return <SustainabilityContainer
|
return <SustainabilityContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Sustainability"
|
title="Sustainability"
|
||||||
intro="Are buildings energy efficient? Coming soon…"
|
intro="Are buildings energy efficient? Coming soon…"
|
||||||
help="https://pages.colouring.london/sustainability"
|
help="https://pages.colouring.london/sustainability"
|
||||||
@ -98,7 +94,7 @@ const BuildingView = (props) => {
|
|||||||
case 'greenery':
|
case 'greenery':
|
||||||
return <GreeneryContainer
|
return <GreeneryContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Greenery"
|
title="Greenery"
|
||||||
intro="Is there greenery nearby? Coming soon…"
|
intro="Is there greenery nearby? Coming soon…"
|
||||||
help="https://pages.colouring.london/greenery"
|
help="https://pages.colouring.london/greenery"
|
||||||
@ -107,7 +103,7 @@ const BuildingView = (props) => {
|
|||||||
case 'community':
|
case 'community':
|
||||||
return <CommunityContainer
|
return <CommunityContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Community"
|
title="Community"
|
||||||
intro="How does this building work for the local community?"
|
intro="How does this building work for the local community?"
|
||||||
help="https://pages.colouring.london/community"
|
help="https://pages.colouring.london/community"
|
||||||
@ -116,7 +112,7 @@ const BuildingView = (props) => {
|
|||||||
case 'planning':
|
case 'planning':
|
||||||
return <PlanningContainer
|
return <PlanningContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Planning"
|
title="Planning"
|
||||||
intro="Planning controls relating to protection and reuse."
|
intro="Planning controls relating to protection and reuse."
|
||||||
help="https://pages.colouring.london/planning"
|
help="https://pages.colouring.london/planning"
|
||||||
@ -124,7 +120,7 @@ const BuildingView = (props) => {
|
|||||||
case 'like':
|
case 'like':
|
||||||
return <LikeContainer
|
return <LikeContainer
|
||||||
{...props}
|
{...props}
|
||||||
key={props.building.building_id}
|
key={props.building && props.building.building_id}
|
||||||
title="Like Me!"
|
title="Like Me!"
|
||||||
intro="Do you like the building and think it contributes to the city?"
|
intro="Do you like the building and think it contributes to the city?"
|
||||||
help="https://pages.colouring.london/likeme"
|
help="https://pages.colouring.london/likeme"
|
||||||
|
@ -9,10 +9,11 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.data-category-list li {
|
.data-category-list li {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
width: 11rem;
|
width: 10rem;
|
||||||
height: 11rem;
|
height: 10rem;
|
||||||
margin: 0.375rem;
|
margin: 0.375rem;
|
||||||
box-shadow: 0 0 2px 5px #ffffff;
|
box-shadow: 0 0 2px 5px #ffffff;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
@ -28,7 +29,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 7rem;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.data-category-list .category-link:hover,
|
.data-category-list .category-link:hover,
|
||||||
.data-category-list .category-link:active,
|
.data-category-list .category-link:active,
|
||||||
@ -36,18 +37,24 @@
|
|||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
.data-category-list .help {
|
.data-category-list .help {
|
||||||
height: 4rem;
|
position: absolute;
|
||||||
padding: 1.5em 0;
|
bottom: 0.75rem;
|
||||||
width: 100%;
|
right: 0.75rem;
|
||||||
background-color: rgba(255,255,255,0.2);
|
color: #222;
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
}
|
||||||
.data-category-list .help:hover,
|
.data-category-list .help:hover,
|
||||||
.data-category-list .help:active,
|
.data-category-list .help:active,
|
||||||
.data-category-list .help:focus {
|
.data-category-list .help:focus {
|
||||||
color: #222;
|
color: #000;
|
||||||
background-color: rgba(255,255,255,0.3);
|
background-color: rgba(37, 10, 10, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-category-list .help::before {
|
||||||
|
content: "\f05a";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.data-category-list .category {
|
.data-category-list .category {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
@ -2,8 +2,6 @@ import React from 'react';
|
|||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Sidebar from './sidebar';
|
|
||||||
|
|
||||||
import './categories.css'
|
import './categories.css'
|
||||||
|
|
||||||
const Categories = (props) => (
|
const Categories = (props) => (
|
||||||
@ -124,11 +122,15 @@ Categories.propTypes = {
|
|||||||
building_id: PropTypes.number
|
building_id: PropTypes.number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Category = (props) => (
|
const Category = (props) => {
|
||||||
|
let categoryLink = `/${props.mode}/${props.slug}`;
|
||||||
|
if (props.building_id != undefined) categoryLink += `/${props.building_id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
<li className={`category-block ${props.slug} background-${props.slug}`}>
|
<li className={`category-block ${props.slug} background-${props.slug}`}>
|
||||||
<NavLink
|
<NavLink
|
||||||
className="category-link"
|
className="category-link"
|
||||||
to={`/${props.mode}/${props.slug}/building/${props.building_id}.html`}
|
to={categoryLink}
|
||||||
title={
|
title={
|
||||||
(props.inactive)?
|
(props.inactive)?
|
||||||
'Coming soon… Click more info for details.'
|
'Coming soon… Click more info for details.'
|
||||||
@ -138,10 +140,11 @@ const Category = (props) => (
|
|||||||
<p className="description">{props.desc}</p>
|
<p className="description">{props.desc}</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<a className="icon-button help" href={props.help}>
|
<a className="icon-button help" href={props.help}>
|
||||||
More info
|
More
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Category.propTypes = {
|
Category.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
@ -6,17 +6,17 @@ import { BackIcon, EditIcon, ViewIcon }from '../components/icons';
|
|||||||
|
|
||||||
const ContainerHeader: React.FunctionComponent<any> = (props) => (
|
const ContainerHeader: React.FunctionComponent<any> = (props) => (
|
||||||
<header className={`section-header view ${props.cat} background-${props.cat}`}>
|
<header className={`section-header view ${props.cat} background-${props.cat}`}>
|
||||||
<Link className="icon-button back" to="/view/categories.html">
|
<Link className="icon-button back" to={`/${props.mode}/categories${props.building != undefined ? `/${props.building.building_id}` : ''}`}>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="h2">{props.title}</h2>
|
<h2 className="h2">{props.title}</h2>
|
||||||
<nav className="icon-buttons">
|
<nav className="icon-buttons">
|
||||||
{
|
{
|
||||||
(!props.inactive)?
|
props.building != undefined && !props.inactive ?
|
||||||
props.copy.copying?
|
props.copy.copying?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/multi-edit/${props.cat}.html?data=${props.data_string}`}
|
to={`/multi-edit/${props.cat}?data=${props.data_string}`}
|
||||||
className="icon-button copy">
|
className="icon-button copy">
|
||||||
Copy selected
|
Copy selected
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@ -45,19 +45,19 @@ const ContainerHeader: React.FunctionComponent<any> = (props) => (
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!props.inactive && !props.copy.copying?
|
props.building != undefined && !props.inactive && !props.copy.copying?
|
||||||
(props.mode === 'edit')?
|
(props.mode === 'edit')?
|
||||||
<NavLink
|
<NavLink
|
||||||
className="icon-button view"
|
className="icon-button view"
|
||||||
title="View data"
|
title="View data"
|
||||||
to={`/view/${props.cat}/building/${props.building.building_id}.html`}>
|
to={`/view/${props.cat}/${props.building.building_id}`}>
|
||||||
View
|
View
|
||||||
<ViewIcon />
|
<ViewIcon />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
: <NavLink
|
: <NavLink
|
||||||
className="icon-button edit"
|
className="icon-button edit"
|
||||||
title="Edit data"
|
title="Edit data"
|
||||||
to={`/edit/${props.cat}/building/${props.building.building_id}.html`}>
|
to={`/edit/${props.cat}/${props.building.building_id}`}>
|
||||||
Edit
|
Edit
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -12,7 +12,7 @@ const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove
|
|||||||
<Tooltip text="People who like the building and think it contributes to the city." />
|
<Tooltip text="People who like the building and think it contributes to the city." />
|
||||||
<div className="icon-buttons">
|
<div className="icon-buttons">
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/multi-edit/like.html?data=${data_string}`}
|
to={`/multi-edit/like?data=${data_string}`}
|
||||||
className="icon-button like">
|
className="icon-button like">
|
||||||
Like more
|
Like more
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -2,9 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import BuildingNotFound from './building-not-found';
|
|
||||||
import ContainerHeader from './container-header';
|
import ContainerHeader from './container-header';
|
||||||
import Sidebar from './sidebar';
|
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
|
|
||||||
@ -198,8 +196,7 @@ const withCopyEdit = (WrappedComponent) => {
|
|||||||
toggleCopyAttribute: this.toggleCopyAttribute,
|
toggleCopyAttribute: this.toggleCopyAttribute,
|
||||||
copyingKey: (key) => this.state.keys_to_copy[key]
|
copyingKey: (key) => this.state.keys_to_copy[key]
|
||||||
}
|
}
|
||||||
return this.props.building?
|
return (
|
||||||
<Sidebar>
|
|
||||||
<section
|
<section
|
||||||
id={this.props.slug}
|
id={this.props.slug}
|
||||||
className="data-section">
|
className="data-section">
|
||||||
@ -208,13 +205,15 @@ const withCopyEdit = (WrappedComponent) => {
|
|||||||
data_string={data_string}
|
data_string={data_string}
|
||||||
copy={copy}
|
copy={copy}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
this.props.building != undefined ?
|
||||||
<form
|
<form
|
||||||
action={`/edit/${this.props.slug}/building/${this.props.building.building_id}.html`}
|
action={`/edit/${this.props.slug}/${this.props.building.building_id}`}
|
||||||
method="POST"
|
method="POST"
|
||||||
onSubmit={this.handleSubmit}>
|
onSubmit={this.handleSubmit}>
|
||||||
<ErrorBox msg={this.state.error} />
|
<ErrorBox msg={this.state.error} />
|
||||||
{
|
{
|
||||||
(this.props.mode === 'edit' && this.props.inactive)?
|
(this.props.mode === 'edit' && this.props.inactive) ?
|
||||||
<InfoBox
|
<InfoBox
|
||||||
msg={`We're not collecting data on ${this.props.title.toLowerCase()} yet - check back soon.`}
|
msg={`We're not collecting data on ${this.props.title.toLowerCase()} yet - check back soon.`}
|
||||||
/>
|
/>
|
||||||
@ -230,12 +229,12 @@ const withCopyEdit = (WrappedComponent) => {
|
|||||||
onUpdate={this.handleUpdate}
|
onUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
(this.props.mode === 'edit' && !this.props.inactive)?
|
(this.props.mode === 'edit' && !this.props.inactive) ?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InfoBox
|
<InfoBox
|
||||||
msg="Colouring may take a few seconds - try zooming the map or hitting refresh after saving (we're working on making this smoother)." />
|
msg="Colouring may take a few seconds - try zooming the map or hitting refresh after saving (we're working on making this smoother)." />
|
||||||
{
|
{
|
||||||
this.props.slug === 'like'? // special-case for likes
|
this.props.slug === 'like' ? // special-case for likes
|
||||||
null :
|
null :
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
<button
|
<button
|
||||||
@ -249,9 +248,10 @@ const withCopyEdit = (WrappedComponent) => {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
: <InfoBox msg="Select a building to view data"></InfoBox>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
</Sidebar>
|
);
|
||||||
: <BuildingNotFound mode="view" />
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,8 @@ const MultiEdit = (props) => {
|
|||||||
<form className='buttons-container'>
|
<form className='buttons-container'>
|
||||||
<InfoBox msg='Click all the buildings that you like and think contribute to the city!' />
|
<InfoBox msg='Click all the buildings that you like and think contribute to the city!' />
|
||||||
|
|
||||||
<Link to='/view/like.html' className='btn btn-secondary'>Back to view</Link>
|
<Link to='/view/like' className='btn btn-secondary'>Back to view</Link>
|
||||||
<Link to='/edit/like.html' className='btn btn-secondary'>Back to edit</Link>
|
<Link to='/edit/like' className='btn btn-secondary'>Back to edit</Link>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
@ -53,8 +53,8 @@ const MultiEdit = (props) => {
|
|||||||
<form className='buttons-container'>
|
<form className='buttons-container'>
|
||||||
<InfoBox msg='Click buildings to colour using the data above' />
|
<InfoBox msg='Click buildings to colour using the data above' />
|
||||||
|
|
||||||
<Link to={`/view/${cat}.html`} className='btn btn-secondary'>Back to view</Link>
|
<Link to={`/view/${cat}`} className='btn btn-secondary'>Back to view</Link>
|
||||||
<Link to={`/edit/${cat}.html`} className='btn btn-secondary'>Back to edit</Link>
|
<Link to={`/edit/${cat}`} className='btn btn-secondary'>Back to edit</Link>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
@ -2,51 +2,25 @@
|
|||||||
* Sidebar layout
|
* Sidebar layout
|
||||||
*/
|
*/
|
||||||
.info-container {
|
.info-container {
|
||||||
position: absolute;
|
order: 1;
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 3rem;
|
|
||||||
padding: 0 0 2em;
|
padding: 0 0 2em;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
z-index: 1000;
|
overflow-y: scroll;
|
||||||
overflow-y: auto;
|
height: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-container h2:first-child {
|
.info-container h2:first-child {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-left: -0.1em;
|
margin-left: -0.1em;
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
}
|
}
|
||||||
#root .leaflet-container .leaflet-control-attribution {
|
|
||||||
width: 100%;
|
|
||||||
height: 3rem;
|
|
||||||
background: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
.leaflet-right{
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
@media (min-width: 380px){
|
|
||||||
.info-container {
|
|
||||||
bottom: 2rem;
|
|
||||||
}
|
|
||||||
#root .leaflet-container .leaflet-control-attribution {
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 768px){
|
@media (min-width: 768px){
|
||||||
.info-container {
|
.info-container {
|
||||||
top: 0;
|
order: 0;
|
||||||
left: 0;
|
height: unset;
|
||||||
width: 25rem;
|
width: 23rem;
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
.leaflet-right{
|
|
||||||
left: 25rem;
|
|
||||||
}
|
|
||||||
#root .leaflet-container .leaflet-control-attribution {
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import React from 'react'
|
|||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faQuestionCircle, faPaintBrush, faInfoCircle, faTimes, faCheck, faCheckDouble,
|
import { faQuestionCircle, faPaintBrush, faInfoCircle, faTimes, faCheck, faCheckDouble,
|
||||||
faAngleLeft, faCaretDown, faSearch, faEye } from '@fortawesome/free-solid-svg-icons'
|
faAngleLeft, faCaretDown, faSearch, faEye, faCaretUp } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
@ -16,6 +16,7 @@ library.add(
|
|||||||
faCheckDouble,
|
faCheckDouble,
|
||||||
faAngleLeft,
|
faAngleLeft,
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
|
faCaretUp,
|
||||||
faSearch,
|
faSearch,
|
||||||
faEye
|
faEye
|
||||||
);
|
);
|
||||||
@ -56,6 +57,10 @@ const DownIcon = () => (
|
|||||||
<FontAwesomeIcon icon="caret-down" />
|
<FontAwesomeIcon icon="caret-down" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const UpIcon = () => (
|
||||||
|
<FontAwesomeIcon icon="caret-up" />
|
||||||
|
);
|
||||||
|
|
||||||
const SearchIcon = () => (
|
const SearchIcon = () => (
|
||||||
<FontAwesomeIcon icon="search" />
|
<FontAwesomeIcon icon="search" />
|
||||||
);
|
);
|
||||||
@ -70,5 +75,6 @@ export {
|
|||||||
SaveDoneIcon,
|
SaveDoneIcon,
|
||||||
BackIcon,
|
BackIcon,
|
||||||
DownIcon,
|
DownIcon,
|
||||||
|
UpIcon,
|
||||||
SearchIcon
|
SearchIcon
|
||||||
};
|
};
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
font-family: 'glacial_cl', sans-serif;
|
font-family: 'glacial_cl', sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.logo,
|
.logo {
|
||||||
.logo.navbar-brand {
|
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #000;
|
color: #000;
|
||||||
@ -27,19 +26,7 @@
|
|||||||
font-size: 0.625em;
|
font-size: 0.625em;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
.map-legend .logo {
|
|
||||||
padding: 0 0.5rem 0.5rem;
|
|
||||||
}
|
|
||||||
.map-legend .logo .logotype {
|
|
||||||
font-size: 1.9rem;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
.map-legend .logo .row .cell {
|
|
||||||
background-color: #ccc;
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
.logo .grid {
|
.logo .grid {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
@ -60,51 +47,56 @@
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
margin: 0 3px 0 0;
|
margin: 0 3px 0 0;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(1) .cell:nth-child(1) {
|
|
||||||
|
.logo.gray .cell {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo.animated .row:nth-child(1) .cell:nth-child(1) {
|
||||||
animation: pulse 87s infinite;
|
animation: pulse 87s infinite;
|
||||||
animation-delay: -1.5s;
|
animation-delay: -1.5s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(1) .cell:nth-child(2) {
|
.logo.animated .row:nth-child(1) .cell:nth-child(2) {
|
||||||
animation: pulse 52s infinite;
|
animation: pulse 52s infinite;
|
||||||
animation-delay: -0.5s;
|
animation-delay: -0.5s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(1) .cell:nth-child(3) {
|
.logo.animated .row:nth-child(1) .cell:nth-child(3) {
|
||||||
animation: pulse 79s infinite;
|
animation: pulse 79s infinite;
|
||||||
animation-delay: -6s;
|
animation-delay: -6s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(1) .cell:nth-child(4) {
|
.logo.animated .row:nth-child(1) .cell:nth-child(4) {
|
||||||
animation: pulse 55s infinite;
|
animation: pulse 55s infinite;
|
||||||
animation-delay: -10s;
|
animation-delay: -10s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(2) .cell:nth-child(1) {
|
.logo.animated .row:nth-child(2) .cell:nth-child(1) {
|
||||||
animation: pulse 64s infinite;
|
animation: pulse 64s infinite;
|
||||||
animation-delay: -7.2s;
|
animation-delay: -7.2s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(2) .cell:nth-child(2) {
|
.logo.animated .row:nth-child(2) .cell:nth-child(2) {
|
||||||
animation: pulse 98s infinite;
|
animation: pulse 98s infinite;
|
||||||
animation-delay: -25s;
|
animation-delay: -25s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(2) .cell:nth-child(3) {
|
.logo.animated .row:nth-child(2) .cell:nth-child(3) {
|
||||||
animation: pulse 51s infinite;
|
animation: pulse 51s infinite;
|
||||||
animation-delay: -35s;
|
animation-delay: -35s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(2) .cell:nth-child(4) {
|
.logo.animated .row:nth-child(2) .cell:nth-child(4) {
|
||||||
animation: pulse 76s infinite;
|
animation: pulse 76s infinite;
|
||||||
animation-delay: -20s;
|
animation-delay: -20s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(3) .cell:nth-child(1) {
|
.logo.animated .row:nth-child(3) .cell:nth-child(1) {
|
||||||
animation: pulse 52s infinite;
|
animation: pulse 52s infinite;
|
||||||
animation-delay: -3.5s;
|
animation-delay: -3.5s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(3) .cell:nth-child(2) {
|
.logo.animated .row:nth-child(3) .cell:nth-child(2) {
|
||||||
animation: pulse 79s infinite;
|
animation: pulse 79s infinite;
|
||||||
animation-delay: -8.5s;
|
animation-delay: -8.5s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(3) .cell:nth-child(3) {
|
.logo.animated .row:nth-child(3) .cell:nth-child(3) {
|
||||||
animation: pulse 65s infinite;
|
animation: pulse 65s infinite;
|
||||||
animation-delay: -4s;
|
animation-delay: -4s;
|
||||||
}
|
}
|
||||||
.logo .row:nth-child(3) .cell:nth-child(4) {
|
.logo.animated .row:nth-child(3) .cell:nth-child(4) {
|
||||||
animation: pulse 54s infinite;
|
animation: pulse 54s infinite;
|
||||||
animation-delay: -17s;
|
animation-delay: -17s;
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import './logo.css';
|
import './logo.css';
|
||||||
|
|
||||||
|
interface LogoProps {
|
||||||
|
variant: 'default' | 'animated' | 'gray';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logo
|
* Logo
|
||||||
*
|
*
|
||||||
* As link to homepage, used in top header
|
* As link to homepage, used in top header
|
||||||
*/
|
*/
|
||||||
const Logo: React.FunctionComponent = () => (
|
const Logo: React.FunctionComponent<LogoProps> = (props) => {
|
||||||
<Link to="/" className="logo navbar-brand" id="top">
|
const variantClass = props.variant === 'default' ? '' : props.variant;
|
||||||
|
return (
|
||||||
|
<div className={`logo ${variantClass}`} >
|
||||||
<LogoGrid />
|
<LogoGrid />
|
||||||
<h1 className="logotype">
|
<h1 className="logotype">
|
||||||
<span>Colouring</span>
|
<span>Colouring</span>
|
||||||
<span>London</span>
|
<span>London</span>
|
||||||
</h1>
|
</h1>
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MinorLogo
|
|
||||||
*
|
|
||||||
* As plain logo, used in legend
|
|
||||||
*/
|
|
||||||
const MinorLogo: React.FunctionComponent = () => (
|
|
||||||
<div className="logo">
|
|
||||||
<LogoGrid />
|
|
||||||
<h3 className="h3 logotype">
|
|
||||||
<span>Colouring</span>
|
|
||||||
<span>London</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LogoGrid: React.FunctionComponent = () => (
|
const LogoGrid: React.FunctionComponent = () => (
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
@ -55,4 +46,4 @@ const LogoGrid: React.FunctionComponent = () => (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { Logo, MinorLogo };
|
export { Logo };
|
||||||
|
@ -2,14 +2,27 @@
|
|||||||
* Main header
|
* Main header
|
||||||
*/
|
*/
|
||||||
.main-header {
|
.main-header {
|
||||||
display: block;
|
|
||||||
min-height: 79px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 3px solid #222;
|
border-bottom: 2px solid #222;
|
||||||
}
|
}
|
||||||
.main-header .navbar {
|
.main-header .navbar {
|
||||||
padding: 0.75em 0.5em 0.75em;
|
padding: 0.5em 0.5em 0.5em;
|
||||||
}
|
}
|
||||||
.main-header .navbar-brand {
|
.main-header .navbar-brand {
|
||||||
margin: 0 1em 0 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ class Header extends React.Component<any, any> { // TODO: add proper types
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {collapseMenu: true};
|
this.state = {collapseMenu: true};
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.handleNavigate = this.handleNavigate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick() {
|
handleClick() {
|
||||||
@ -27,22 +28,35 @@ class Header extends React.Component<any, any> { // TODO: add proper types
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNavigate() {
|
||||||
|
this.setState({
|
||||||
|
collapseMenu: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<header className="main-header">
|
<header className="main-header">
|
||||||
<nav className="navbar navbar-light navbar-expand-md">
|
<nav className="navbar navbar-light navbar-expand-lg">
|
||||||
<span className="navbar-brand">
|
<span className="navbar-brand align-self-start">
|
||||||
<Logo/>
|
<NavLink to="/">
|
||||||
|
<Logo variant='animated'/>
|
||||||
|
</NavLink>
|
||||||
</span>
|
</span>
|
||||||
<button className="navbar-toggler navbar-toggler-right" type="button"
|
<button className="navbar-toggler navbar-toggler-right" type="button"
|
||||||
onClick={this.handleClick} aria-expanded="false" aria-label="Toggle navigation">
|
onClick={this.handleClick} aria-expanded={!this.state.collapseMenu} aria-label="Toggle navigation">
|
||||||
<span className="navbar-toggler-icon"></span>
|
<span className="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div className={this.state.collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
|
<div className={this.state.collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
|
||||||
<ul className="navbar-nav ml-auto">
|
<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">
|
<li className="nav-item">
|
||||||
<a className="nav-link" href="https://pages.colouring.london">
|
<a className="nav-link" href="https://pages.colouring.london">
|
||||||
Hello
|
About
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
@ -50,23 +64,13 @@ class Header extends React.Component<any, any> { // TODO: add proper types
|
|||||||
Data Categories
|
Data Categories
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink to="/view/categories.html" className="nav-link">
|
|
||||||
View/Edit Maps
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<a className="nav-link" href="https://pages.colouring.london/about">
|
|
||||||
More about
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<a className="nav-link" href="https://pages.colouring.london/whoisinvolved">
|
<a className="nav-link" href="https://pages.colouring.london/whoisinvolved">
|
||||||
Who’s Involved?
|
Who’s Involved?
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<a className="nav-link" href="https://www.pages.colouring.london/data-ethics">
|
<a className="nav-link" href="https://pages.colouring.london/data-ethics">
|
||||||
Data Ethics
|
Data Ethics
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -79,20 +83,20 @@ class Header extends React.Component<any, any> { // TODO: add proper types
|
|||||||
this.props.user?
|
this.props.user?
|
||||||
(
|
(
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<NavLink to="/my-account.html" className="nav-link">
|
<NavLink to="/my-account.html" className="nav-link" onClick={this.handleNavigate}>
|
||||||
My account (Logged in as {this.props.user.username})
|
Account <span className="shorten-username">({this.props.user.username})</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
):
|
):
|
||||||
(
|
(
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<NavLink to="/login.html" className="nav-link">
|
<NavLink to="/login.html" className="nav-link" onClick={this.handleNavigate}>
|
||||||
Log in
|
Log in
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<NavLink to="/sign-up.html" className="nav-link">
|
<NavLink to="/sign-up.html" className="nav-link" onClick={this.handleNavigate}>
|
||||||
Sign up
|
Sign up
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
250
app/src/frontend/map-app.tsx
Normal file
250
app/src/frontend/map-app.tsx
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { Switch, Route, RouteComponentProps, Redirect } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Welcome from './pages/welcome';
|
||||||
|
import Sidebar from './building/sidebar';
|
||||||
|
import Categories from './building/categories';
|
||||||
|
import MultiEdit from './building/multi-edit';
|
||||||
|
import BuildingView from './building/building-view';
|
||||||
|
import ColouringMap from './map/map';
|
||||||
|
import { parse } from 'query-string';
|
||||||
|
|
||||||
|
interface MapAppRouteParams {
|
||||||
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
|
category: string;
|
||||||
|
building?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||||
|
building: any;
|
||||||
|
building_like: boolean;
|
||||||
|
user: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapAppState {
|
||||||
|
category: string;
|
||||||
|
revision_id: number;
|
||||||
|
building: any;
|
||||||
|
building_like: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||||
|
static propTypes = {
|
||||||
|
category: PropTypes.string,
|
||||||
|
revision_id: PropTypes.number,
|
||||||
|
building: PropTypes.object,
|
||||||
|
building_like: PropTypes.bool,
|
||||||
|
user: PropTypes.object
|
||||||
|
};
|
||||||
|
constructor(props: Readonly<MapAppProps>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// set building revision id, default 0
|
||||||
|
const rev = props.building != undefined ? +props.building.revision_id : 0;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
category: this.getCategory(props.match.params.category),
|
||||||
|
revision_id: rev,
|
||||||
|
building: props.building,
|
||||||
|
building_like: props.building_like
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectBuilding = this.selectBuilding.bind(this);
|
||||||
|
this.colourBuilding = this.colourBuilding.bind(this);
|
||||||
|
this.increaseRevision = this.increaseRevision.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props: Readonly<MapAppProps>) {
|
||||||
|
const newCategory = this.getCategory(props.match.params.category);
|
||||||
|
if (newCategory != undefined) {
|
||||||
|
this.setState({ category: newCategory });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategory(category: string) {
|
||||||
|
if (category === 'categories') return undefined;
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseRevision(revisionId) {
|
||||||
|
revisionId = +revisionId;
|
||||||
|
// bump revision id, only ever increasing
|
||||||
|
if (revisionId > this.state.revision_id) {
|
||||||
|
this.setState({ revision_id: revisionId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectBuilding(building) {
|
||||||
|
const mode = this.props.match.params.mode || 'view';
|
||||||
|
const category = this.props.match.params.category || 'age';
|
||||||
|
|
||||||
|
if (building == undefined ||
|
||||||
|
(this.state.building != undefined &&
|
||||||
|
building.building_id === this.state.building.building_id)) {
|
||||||
|
this.setState({ building: undefined });
|
||||||
|
this.props.history.push(`/${mode}/${category}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.increaseRevision(building.revision_id);
|
||||||
|
// get UPRNs and update
|
||||||
|
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error(res);
|
||||||
|
} else {
|
||||||
|
building.uprns = res.uprns;
|
||||||
|
this.setState({ building: building });
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
this.setState({ building: building });
|
||||||
|
});
|
||||||
|
|
||||||
|
// get if liked and update
|
||||||
|
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error(res);
|
||||||
|
} else {
|
||||||
|
this.setState({ building_like: res.like });
|
||||||
|
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
this.setState({ building_like: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colour building
|
||||||
|
*
|
||||||
|
* Used in multi-edit mode to colour buildings on map click
|
||||||
|
*
|
||||||
|
* Pulls data from URL to form update
|
||||||
|
*
|
||||||
|
* @param {object} building
|
||||||
|
*/
|
||||||
|
colourBuilding(building) {
|
||||||
|
const cat = this.props.match.params.category;
|
||||||
|
const q = parse(window.location.search);
|
||||||
|
const data = (cat === 'like') ? { like: true } : JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
|
||||||
|
if (cat === 'like') {
|
||||||
|
this.likeBuilding(building.building_id)
|
||||||
|
} else {
|
||||||
|
this.updateBuilding(building.building_id, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
likeBuilding(buildingId) {
|
||||||
|
fetch(`/api/buildings/${buildingId}/like.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ like: true })
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then(function (res) {
|
||||||
|
if (res.error) {
|
||||||
|
console.error({ error: res.error })
|
||||||
|
} else {
|
||||||
|
this.increaseRevision(res.revision_id);
|
||||||
|
}
|
||||||
|
}.bind(this)).catch(
|
||||||
|
(err) => console.error({ error: err })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBuilding(buildingId, data) {
|
||||||
|
fetch(`/api/buildings/${buildingId}.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then(res => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error({ error: res.error })
|
||||||
|
} else {
|
||||||
|
this.increaseRevision(res.revision_id);
|
||||||
|
}
|
||||||
|
}).catch(
|
||||||
|
(err) => console.error({ error: err })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const mode = this.props.match.params.mode || 'basic';
|
||||||
|
|
||||||
|
let category = this.state.category || 'age';
|
||||||
|
|
||||||
|
const building_id = this.state.building && this.state.building.building_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/">
|
||||||
|
<Welcome />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/:mode/categories/:building?">
|
||||||
|
<Sidebar>
|
||||||
|
<Categories mode={mode} building_id={building_id} />
|
||||||
|
</Sidebar>
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/multi-edit/:cat" render={(props) => (
|
||||||
|
<MultiEdit
|
||||||
|
{...props}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
)} />
|
||||||
|
<Route exact path="/:mode/:cat/:building?">
|
||||||
|
<Sidebar>
|
||||||
|
<BuildingView
|
||||||
|
mode={mode}
|
||||||
|
cat={category}
|
||||||
|
building={this.state.building}
|
||||||
|
building_like={this.state.building_like}
|
||||||
|
selectBuilding={this.selectBuilding}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
</Sidebar>
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/(view|edit|multi-edit)">
|
||||||
|
<Redirect to="/view/categories" />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
<ColouringMap
|
||||||
|
building={this.state.building}
|
||||||
|
mode={mode}
|
||||||
|
category={category}
|
||||||
|
revision_id={this.state.revision_id}
|
||||||
|
selectBuilding={this.selectBuilding}
|
||||||
|
colourBuilding={this.colourBuilding}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapApp;
|
@ -2,28 +2,53 @@
|
|||||||
* Map legend
|
* Map legend
|
||||||
*/
|
*/
|
||||||
.map-legend {
|
.map-legend {
|
||||||
z-index: 1000;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 50%;
|
bottom: 2.5rem;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
float: right;
|
max-height: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.5rem 0rem 0.25rem;
|
padding: 0.5rem 0rem 0.25rem;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
box-shadow: 0px 0px 1px 1px #222222;
|
box-shadow: 0px 0px 1px 1px #222222;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px){
|
|
||||||
.map-legend {
|
.map-legend * {
|
||||||
bottom: 40px;
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-legend .logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.map-legend .logo {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1020px){
|
|
||||||
|
/* Prevent legend from overlapping with attribution */
|
||||||
|
@media (min-width: 706px){
|
||||||
.map-legend {
|
.map-legend {
|
||||||
bottom: 24px;
|
bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.map-legend {
|
||||||
|
bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1072px){
|
||||||
|
.map-legend {
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.map-legend .h4,
|
.map-legend .h4,
|
||||||
.map-legend p,
|
.map-legend p,
|
||||||
.data-legend {
|
.data-legend {
|
||||||
@ -33,30 +58,12 @@
|
|||||||
margin: 0.25rem 0 0.5rem;
|
margin: 0.25rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
.data-legend {
|
.data-legend {
|
||||||
max-height: 80px;
|
overflow: auto;
|
||||||
max-height: 20vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
@media (min-height: 470px) {
|
|
||||||
.data-legend {
|
|
||||||
max-height: 150px;
|
|
||||||
max-height: 30vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-height: 550px) {
|
|
||||||
.data-legend {
|
|
||||||
max-height: 220px;
|
|
||||||
max-height: 40vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-height: 670px) {
|
|
||||||
.data-legend {
|
|
||||||
max-height: 330px;
|
|
||||||
max-height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.data-legend .key {
|
.data-legend .key {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.3rem;
|
width: 1.3rem;
|
||||||
@ -71,10 +78,35 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.expander-button {
|
.expander-button {
|
||||||
float: right;
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
line-height: 0.5;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
@media (min-height: 670px) and (min-width: 768px) {
|
.expander-button:focus,
|
||||||
|
.expander-button:active,
|
||||||
|
.expander-button:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
@media (min-height: 670px) and (min-width: 880px) {
|
||||||
.expander-button {
|
.expander-button {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-legend .logo {
|
||||||
|
padding: 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
.map-legend .logo .logotype {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
.map-legend .logo .cell {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
@ -2,7 +2,8 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './legend.css';
|
import './legend.css';
|
||||||
import { MinorLogo } from '../components/logo';
|
import { Logo } from '../components/logo';
|
||||||
|
import { DownIcon, UpIcon, BackIcon } from '../components/icons';
|
||||||
|
|
||||||
const LEGEND_CONFIG = {
|
const LEGEND_CONFIG = {
|
||||||
location: {
|
location: {
|
||||||
@ -146,8 +147,21 @@ class Legend extends React.Component<any, any> { // TODO: add proper types
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-legend">
|
<div className="map-legend">
|
||||||
<MinorLogo />
|
<Logo variant='gray' />
|
||||||
<h4 className="h4">{ title } {elements.length?<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={this.handleClick} >^</button>:null}</h4>
|
<h4 className="h4">
|
||||||
|
{ title }
|
||||||
|
</h4>
|
||||||
|
{
|
||||||
|
elements.length > 0 ?
|
||||||
|
<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={this.handleClick} >
|
||||||
|
{
|
||||||
|
this.state.collapseList ?
|
||||||
|
<UpIcon /> :
|
||||||
|
<DownIcon />
|
||||||
|
}
|
||||||
|
</button> :
|
||||||
|
null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
details.description?
|
details.description?
|
||||||
<p>{details.description} </p>
|
<p>{details.description} </p>
|
||||||
|
@ -8,25 +8,34 @@
|
|||||||
box-shadow: 0px 0px 1px 1px #222;
|
box-shadow: 0px 0px 1px 1px #222;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
position: absolute;
|
height: 100%;
|
||||||
top: 0;
|
width: 100%;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
.leaflet-container .leaflet-control-zoom {
|
.leaflet-container .leaflet-control-zoom {
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
box-shadow: 0 0 1px 1px #222;
|
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 {
|
.leaflet-grab {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px){
|
@media (min-width: 768px){
|
||||||
/* Only show the "Click a building ..." notice for larger screens */
|
/* Only show the "Click a building ..." notice for larger screens */
|
||||||
.map-notice {
|
.map-notice {
|
||||||
left: 25.5rem;
|
left: 0.5rem;
|
||||||
top: 0.5rem;
|
top: 3.5rem;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import { LatLngExpression } from 'leaflet';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal';
|
import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal';
|
||||||
|
|
||||||
import '../../../node_modules/leaflet/dist/leaflet.css'
|
import '../../../node_modules/leaflet/dist/leaflet.css'
|
||||||
@ -13,17 +14,32 @@ import ThemeSwitcher from './theme-switcher';
|
|||||||
|
|
||||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||||
|
|
||||||
|
interface ColouringMapProps {
|
||||||
|
building: any;
|
||||||
|
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||||
|
category: string;
|
||||||
|
revision_id: number;
|
||||||
|
selectBuilding: any;
|
||||||
|
colourBuilding: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColouringMapState {
|
||||||
|
theme: 'light' | 'night';
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
zoom: number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Map area
|
* Map area
|
||||||
*/
|
*/
|
||||||
class ColouringMap extends Component<any, any> { // TODO: add proper types
|
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> { // TODO: add proper types
|
||||||
static propTypes = { // TODO: generate propTypes from TS
|
static propTypes = { // TODO: generate propTypes from TS
|
||||||
building: PropTypes.object,
|
building: PropTypes.object,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
category: PropTypes.string,
|
||||||
revision_id: PropTypes.number,
|
revision_id: PropTypes.number,
|
||||||
selectBuilding: PropTypes.func,
|
selectBuilding: PropTypes.func,
|
||||||
colourBuilding: PropTypes.func,
|
colourBuilding: PropTypes.func
|
||||||
match: PropTypes.object,
|
|
||||||
history: PropTypes.object
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -37,7 +53,6 @@ class ColouringMap extends Component<any, any> { // TODO: add proper types
|
|||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleLocate = this.handleLocate.bind(this);
|
this.handleLocate = this.handleLocate.bind(this);
|
||||||
this.themeSwitch = this.themeSwitch.bind(this);
|
this.themeSwitch = this.themeSwitch.bind(this);
|
||||||
this.getMode = this.getMode.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLocate(lat, lng, zoom){
|
handleLocate(lat, lng, zoom){
|
||||||
@ -45,21 +60,13 @@ class ColouringMap extends Component<any, any> { // TODO: add proper types
|
|||||||
lat: lat,
|
lat: lat,
|
||||||
lng: lng,
|
lng: lng,
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getMode() {
|
|
||||||
const isEdit = this.props.match.url.match('edit')
|
|
||||||
const isMulti = this.props.match.url.match('multi')
|
|
||||||
return isEdit? (isMulti? 'multi' : 'edit') : 'view';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
const mode = this.getMode()
|
const mode = this.props.mode;
|
||||||
const lat = e.latlng.lat
|
const lat = e.latlng.lat;
|
||||||
const lng = e.latlng.lng
|
const lng = e.latlng.lng;
|
||||||
const newCat = parseCategoryURL(this.props.match.url);
|
|
||||||
const mapCat = newCat || 'age';
|
|
||||||
fetch(
|
fetch(
|
||||||
'/api/buildings/locate?lat='+lat+'&lng='+lng
|
'/api/buildings/locate?lat='+lat+'&lng='+lng
|
||||||
).then(
|
).then(
|
||||||
@ -67,17 +74,15 @@ class ColouringMap extends Component<any, any> { // TODO: add proper types
|
|||||||
).then(function(data){
|
).then(function(data){
|
||||||
if (data && data.length){
|
if (data && data.length){
|
||||||
const building = data[0];
|
const building = data[0];
|
||||||
if (mode === 'multi') {
|
if (mode === 'multi-edit') {
|
||||||
// colour building directly
|
// colour building directly
|
||||||
this.props.colourBuilding(building);
|
this.props.colourBuilding(building);
|
||||||
} else {
|
} else {
|
||||||
this.props.selectBuilding(building);
|
this.props.selectBuilding(building);
|
||||||
this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// deselect but keep/return to expected colour theme
|
// deselect but keep/return to expected colour theme
|
||||||
this.props.selectBuilding(undefined);
|
this.props.selectBuilding(undefined);
|
||||||
this.props.history.push(`/${mode}/${mapCat}.html`);
|
|
||||||
}
|
}
|
||||||
}.bind(this)).catch(
|
}.bind(this)).catch(
|
||||||
(err) => console.error(err)
|
(err) => console.error(err)
|
||||||
@ -91,52 +96,56 @@ class ColouringMap extends Component<any, any> { // TODO: add proper types
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const position = [this.state.lat, this.state.lng];
|
const position: LatLngExpression = [this.state.lat, this.state.lng];
|
||||||
|
|
||||||
// baselayer
|
// baselayer
|
||||||
const key = OS_API_KEY
|
const key = OS_API_KEY;
|
||||||
const tilematrixSet = 'EPSG:3857'
|
const tilematrixSet = 'EPSG:3857';
|
||||||
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
|
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
|
||||||
const url = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`
|
const baseUrl = `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.'
|
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines.';
|
||||||
|
const baseLayer = <TileLayer
|
||||||
|
url={baseUrl}
|
||||||
|
attribution={attribution}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}.png`;
|
||||||
|
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} />;
|
||||||
|
|
||||||
// colour-data tiles
|
// colour-data tiles
|
||||||
const isBuilding = /building/.test(this.props.match.url);
|
const cat = this.props.category;
|
||||||
const isEdit = /edit/.test(this.props.match.url);
|
|
||||||
const cat = parseCategoryURL(this.props.match.url);
|
|
||||||
const tilesetByCat = {
|
const tilesetByCat = {
|
||||||
age: 'date_year',
|
age: 'date_year',
|
||||||
size: 'size_storeys',
|
size: 'size_storeys',
|
||||||
location: 'location',
|
location: 'location',
|
||||||
like: 'likes',
|
like: 'likes',
|
||||||
planning: 'conservation_area',
|
planning: 'conservation_area',
|
||||||
}
|
};
|
||||||
const tileset = tilesetByCat[cat];
|
const tileset = tilesetByCat[cat];
|
||||||
// pick revision id to bust browser cache
|
// pick revision id to bust browser cache
|
||||||
const rev = this.props.revision_id;
|
const rev = this.props.revision_id;
|
||||||
const dataLayer = tileset?
|
const dataLayer = tileset != undefined ?
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={tileset}
|
key={tileset}
|
||||||
url={`/tiles/${tileset}/{z}/{x}/{y}.png?rev=${rev}`}
|
url={`/tiles/${tileset}/{z}/{x}/{y}.png?rev=${rev}`}
|
||||||
minZoom={9} />
|
minZoom={9}
|
||||||
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// highlight
|
// highlight
|
||||||
const geometryId = (this.props.building) ? this.props.building.geometry_id : undefined;
|
const highlightLayer = this.props.building != undefined ?
|
||||||
const highlight = `/tiles/highlight/{z}/{x}/{y}.png?highlight=${geometryId}`
|
|
||||||
const highlightLayer = (isBuilding && this.props.building) ?
|
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={this.props.building.building_id}
|
key={this.props.building.building_id}
|
||||||
url={highlight}
|
url={`/tiles/highlight/{z}/{x}/{y}.png?highlight=${this.props.building.geometry_id}`}
|
||||||
minZoom={14} />
|
minZoom={14}
|
||||||
|
zIndex={100}
|
||||||
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const baseUrl = (this.state.theme === 'light')?
|
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
||||||
'/tiles/base_light/{z}/{x}/{y}.png'
|
|
||||||
: '/tiles/base_night/{z}/{x}/{y}.png'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="map-container">
|
||||||
<Map
|
<Map
|
||||||
center={position}
|
center={position}
|
||||||
zoom={this.state.zoom}
|
zoom={this.state.zoom}
|
||||||
@ -147,30 +156,30 @@ class ColouringMap extends Component<any, any> { // TODO: add proper types
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
<TileLayer url={url} attribution={attribution} />
|
{ baseLayer }
|
||||||
<TileLayer url={baseUrl} minZoom={14} />
|
{ buildingBaseLayer }
|
||||||
{ dataLayer }
|
{ dataLayer }
|
||||||
{ highlightLayer }
|
{ highlightLayer }
|
||||||
<ZoomControl position="topright" />
|
<ZoomControl position="topright" />
|
||||||
<AttributionControl prefix="" />
|
<AttributionControl prefix=""/>
|
||||||
</Map>
|
</Map>
|
||||||
{
|
{
|
||||||
!isBuilding && this.props.match.url !== '/'? (
|
this.props.mode !== 'basic'? (
|
||||||
<div className="map-notice">
|
|
||||||
<HelpIcon /> {isEdit? 'Click a building to edit' : 'Click a building for details'}
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.props.match.url !== '/'? (
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
{
|
||||||
|
this.props.building == undefined ?
|
||||||
|
<div className="map-notice">
|
||||||
|
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
<Legend slug={cat} />
|
<Legend slug={cat} />
|
||||||
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
||||||
<SearchBox onLocate={this.handleLocate} isBuilding={isBuilding} />
|
<SearchBox onLocate={this.handleLocate} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
app/src/frontend/map/react-leaflet-universal.d.ts
vendored
Normal file
9
app/src/frontend/map/react-leaflet-universal.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Export all type declarations available for react-leaflet as types for react-leaflet-universal.
|
||||||
|
* This is because the latter doesn't have type declarations published as of 2019-09-09
|
||||||
|
* but we can re-use types from react-leaflet as universal is mostly a wrapper, so the types
|
||||||
|
* still apply.
|
||||||
|
*/
|
||||||
|
declare module 'react-leaflet-universal' {
|
||||||
|
export * from 'react-leaflet';
|
||||||
|
}
|
@ -5,9 +5,6 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
max-width:80%;
|
max-width:80%;
|
||||||
}
|
}
|
||||||
.building.search-box {
|
|
||||||
top: 0.5rem;
|
|
||||||
}
|
|
||||||
.search-box form,
|
.search-box form,
|
||||||
.search-box .search-box-results {
|
.search-box .search-box-results {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -46,11 +43,6 @@
|
|||||||
margin-right: 0.1rem;
|
margin-right: 0.1rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
/* for large screens adopt the conventional search box position */
|
|
||||||
.search-box{
|
|
||||||
top: 3.625rem;
|
|
||||||
left: 25.5rem;
|
|
||||||
}
|
|
||||||
/* The following is a fix (?) for the truncation of the "Search for postcode" text */
|
/* The following is a fix (?) for the truncation of the "Search for postcode" text */
|
||||||
.form-inline .form-control {
|
.form-inline .form-control {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
@ -8,8 +8,7 @@ import { SearchIcon } from '../components/icons';
|
|||||||
*/
|
*/
|
||||||
class SearchBox extends Component<any, any> { // TODO: add proper types
|
class SearchBox extends Component<any, any> { // TODO: add proper types
|
||||||
static propTypes = { // TODO: generate propTypes from TS
|
static propTypes = { // TODO: generate propTypes from TS
|
||||||
onLocate: PropTypes.func,
|
onLocate: PropTypes.func
|
||||||
isBuilding: PropTypes.bool
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -159,7 +158,7 @@ class SearchBox extends Component<any, any> { // TODO: add proper types
|
|||||||
</ul>
|
</ul>
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<div className={`search-box ${this.props.isBuilding? 'building' : ''}`} onKeyDown={this.handleKeyPress}>
|
<div className="search-box" onKeyDown={this.handleKeyPress}>
|
||||||
<form onSubmit={this.search} className="form-inline">
|
<form onSubmit={this.search} className="form-inline">
|
||||||
<div onClick={this.state.smallScreen ? this.expandSearch : null}>
|
<div onClick={this.state.smallScreen ? this.expandSearch : null}>
|
||||||
<SearchIcon/>
|
<SearchIcon/>
|
||||||
|
@ -6,8 +6,10 @@
|
|||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 1.5em 2.5em 2.5em;
|
padding: 1.5em 2.5em 2.5em;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
.welcome-float.jumbotron {
|
.welcome-float.jumbotron {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
@ -17,7 +17,7 @@ const Welcome = () => (
|
|||||||
volunteers of all ages and abilities to test and provide feedback on the site as we
|
volunteers of all ages and abilities to test and provide feedback on the site as we
|
||||||
build it.
|
build it.
|
||||||
</p>
|
</p>
|
||||||
<Link to="/view/categories.html"
|
<Link to="/view/categories"
|
||||||
className="btn btn-outline-dark btn-lg btn-block">
|
className="btn btn-outline-dark btn-lg btn-block">
|
||||||
Start Colouring Here!
|
Start Colouring Here!
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,20 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* Main Layout
|
* Main Layout
|
||||||
*/
|
*/
|
||||||
main {
|
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 35rem;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px){
|
|
||||||
|
@media(min-width: 768px) {
|
||||||
main {
|
main {
|
||||||
position: absolute;
|
flex-direction: row;
|
||||||
top: 79px; /* matches 79px .main-header */
|
}
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Text pages
|
* Text pages
|
||||||
*/
|
*/
|
||||||
|
@ -116,7 +116,7 @@ class MyAccountPage extends Component<any, any> { // TODO: add proper types
|
|||||||
<ErrorBox msg={this.state.error} />
|
<ErrorBox msg={this.state.error} />
|
||||||
<form onSubmit={this.handleLogout}>
|
<form onSubmit={this.handleLogout}>
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
<Link to="/edit/age.html" className="btn btn-warning">Start colouring</Link>
|
<Link to="/edit/age" className="btn btn-warning">Start colouring</Link>
|
||||||
<input className="btn btn-secondary" type="submit" value="Log out"/>
|
<input className="btn btn-secondary" type="submit" value="Log out"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
110
app/src/frontendRoute.tsx
Normal file
110
app/src/frontendRoute.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import React from 'react';
|
||||||
|
import { StaticRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import serialize from 'serialize-javascript';
|
||||||
|
|
||||||
|
import App from './frontend/app';
|
||||||
|
|
||||||
|
import { parseBuildingURL } from './parse';
|
||||||
|
import { getUserById } from './api/services/user';
|
||||||
|
import {
|
||||||
|
getBuildingById,
|
||||||
|
getBuildingLikeById,
|
||||||
|
getBuildingUPRNsById
|
||||||
|
} from './api/services/building';
|
||||||
|
|
||||||
|
|
||||||
|
// reference packed assets
|
||||||
|
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
||||||
|
|
||||||
|
|
||||||
|
function frontendRoute(req: express.Request, res: express.Response) {
|
||||||
|
const context: any = {}; // TODO: remove any
|
||||||
|
const data: any = {}; // TODO: remove any
|
||||||
|
context.status = 200;
|
||||||
|
|
||||||
|
const userId = req.session.user_id;
|
||||||
|
const buildingId = parseBuildingURL(req.url);
|
||||||
|
const isBuilding = (typeof (buildingId) !== 'undefined');
|
||||||
|
if (isBuilding && isNaN(buildingId)) {
|
||||||
|
context.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
userId ? getUserById(userId) : undefined,
|
||||||
|
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||||
|
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||||
|
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false
|
||||||
|
]).then(function ([user, building, uprns, buildingLike]) {
|
||||||
|
if (isBuilding && typeof (building) === 'undefined') {
|
||||||
|
context.status = 404;
|
||||||
|
}
|
||||||
|
data.user = user;
|
||||||
|
data.building = building;
|
||||||
|
data.building_like = buildingLike;
|
||||||
|
if (data.building != null) {
|
||||||
|
data.building.uprns = uprns;
|
||||||
|
}
|
||||||
|
renderHTML(context, data, req, res);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
data.user = undefined;
|
||||||
|
data.building = undefined;
|
||||||
|
data.building_like = undefined;
|
||||||
|
context.status = 500;
|
||||||
|
renderHTML(context, data, req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHTML(context, data, req, res) {
|
||||||
|
const markup = renderToString(
|
||||||
|
<StaticRouter context={context} location={req.url}>
|
||||||
|
<App user={data.user} building={data.building} building_like={data.building_like} />
|
||||||
|
</StaticRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.url) {
|
||||||
|
res.redirect(context.url);
|
||||||
|
} else {
|
||||||
|
res.status(context.status).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>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default frontendRoute;
|
@ -23,7 +23,7 @@ function strictParseInt(value) {
|
|||||||
* @returns {number|undefined}
|
* @returns {number|undefined}
|
||||||
*/
|
*/
|
||||||
function parseBuildingURL(url) {
|
function parseBuildingURL(url) {
|
||||||
const re = /\/building\/([^/]+).html/;
|
const re = /\/(\d+)$/;
|
||||||
const matches = re.exec(url);
|
const matches = re.exec(url);
|
||||||
|
|
||||||
if (matches && matches.length >= 2) {
|
if (matches && matches.length >= 2) {
|
||||||
|
@ -4,40 +4,25 @@
|
|||||||
* - entry-point to shared React App
|
* - entry-point to shared React App
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
|
||||||
import { StaticRouter } from 'react-router-dom';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { renderToString } from 'react-dom/server';
|
|
||||||
import serialize from 'serialize-javascript';
|
|
||||||
|
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import pgConnect from 'connect-pg-simple';
|
import pgConnect from 'connect-pg-simple';
|
||||||
|
|
||||||
import App from './frontend/app';
|
|
||||||
import db from './db';
|
import db from './db';
|
||||||
import { getUserById } from './api/services/user';
|
|
||||||
import {
|
|
||||||
getBuildingById,
|
|
||||||
getBuildingLikeById,
|
|
||||||
getBuildingUPRNsById
|
|
||||||
} from './api/services/building';
|
|
||||||
import tileserver from './tiles/tileserver';
|
import tileserver from './tiles/tileserver';
|
||||||
import apiServer from './api/api';
|
import apiServer from './api/api';
|
||||||
import { parseBuildingURL } from './parse';
|
import frontendRoute from './frontendRoute';
|
||||||
|
|
||||||
// create server
|
// create server
|
||||||
const server = express();
|
const server = express();
|
||||||
|
|
||||||
// reference packed assets
|
|
||||||
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
|
||||||
|
|
||||||
// disable header
|
// disable header
|
||||||
server.disable('x-powered-by');
|
server.disable('x-powered-by');
|
||||||
|
|
||||||
// serve static files
|
// serve static files
|
||||||
server.use(express.static(process.env.RAZZLE_PUBLIC_DIR));
|
server.use(express.static(process.env.RAZZLE_PUBLIC_DIR));
|
||||||
|
|
||||||
|
|
||||||
// handle user sessions
|
// handle user sessions
|
||||||
const pgSession = pgConnect(session);
|
const pgSession = pgConnect(session);
|
||||||
const sess: any = { // TODO: remove any
|
const sess: any = { // TODO: remove any
|
||||||
@ -59,106 +44,8 @@ if (server.get('env') === 'production') {
|
|||||||
}
|
}
|
||||||
server.use(session(sess));
|
server.use(session(sess));
|
||||||
|
|
||||||
// handle HTML routes (server-side rendered React)
|
|
||||||
server.get('/*.html', frontendRoute);
|
|
||||||
server.get('/', frontendRoute);
|
|
||||||
|
|
||||||
function frontendRoute(req, res) {
|
|
||||||
const context: any = {}; // TODO: remove any
|
|
||||||
const data: any = {}; // TODO: remove any
|
|
||||||
context.status = 200;
|
|
||||||
|
|
||||||
const userId = req.session.user_id;
|
|
||||||
const buildingId = parseBuildingURL(req.url);
|
|
||||||
const isBuilding = (typeof (buildingId) !== 'undefined');
|
|
||||||
if (isBuilding && isNaN(buildingId)) {
|
|
||||||
context.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
userId ? getUserById(userId) : undefined,
|
|
||||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
|
||||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
|
||||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false
|
|
||||||
]).then(function (values) {
|
|
||||||
const user = values[0];
|
|
||||||
const building = values[1];
|
|
||||||
const uprns = values[2];
|
|
||||||
const buildingLike = values[3];
|
|
||||||
if (isBuilding && typeof (building) === 'undefined') {
|
|
||||||
context.status = 404
|
|
||||||
}
|
|
||||||
data.user = user;
|
|
||||||
data.building = building;
|
|
||||||
data.building_like = buildingLike;
|
|
||||||
if (data.building != null) {
|
|
||||||
data.building.uprns = uprns;
|
|
||||||
}
|
|
||||||
renderHTML(context, data, req, res)
|
|
||||||
}).catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
data.user = undefined;
|
|
||||||
data.building = undefined;
|
|
||||||
data.building_like = undefined;
|
|
||||||
context.status = 500;
|
|
||||||
renderHTML(context, data, req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHTML(context, data, req, res) {
|
|
||||||
const markup = renderToString(
|
|
||||||
<StaticRouter context={context} location={req.url}>
|
|
||||||
<App user={data.user} building={data.building} building_like={data.building_like} />
|
|
||||||
</StaticRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.url) {
|
|
||||||
res.redirect(context.url);
|
|
||||||
} else {
|
|
||||||
res.status(context.status).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>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.use('/tiles', tileserver);
|
server.use('/tiles', tileserver);
|
||||||
|
|
||||||
server.use('/api', apiServer);
|
server.use('/api', apiServer);
|
||||||
|
|
||||||
// use the frontend route for anything else - will presumably show the 404 page
|
|
||||||
server.use(frontendRoute);
|
server.use(frontendRoute);
|
||||||
|
|
||||||
export default server;
|
export default server;
|
||||||
|
Loading…
Reference in New Issue
Block a user