diff --git a/.env b/.env new file mode 100644 index 0000000..57a45af --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +#REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyBZxy09BXS_K-RtyIpqv7Spu4y6c5qLrfo + +REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCqhXDnXAnPgXk_4n9EC1u2jeEP5E10k-E +REACT_APP_GRAPHQL_HOST=start-v3.hasura.app diff --git a/hasura/hasura.env b/hasura/hasura.env new file mode 100644 index 0000000..6000555 --- /dev/null +++ b/hasura/hasura.env @@ -0,0 +1,4 @@ +HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public +HASURA_GRAPHQL_CORS_DOMAIN=* +HASURA_GRAPHQL_ADMIN_SECRET= +HASURA_GRAPHQL_DATABASE_URL= diff --git a/hasura/metadata_export.json b/hasura/metadata_export.json new file mode 100644 index 0000000..8dc1177 --- /dev/null +++ b/hasura/metadata_export.json @@ -0,0 +1,155 @@ +{ + "version": 2, + "tables": [ + { + "table": { + "schema": "public", + "name": "artists" + }, + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": [ + "preferred_name" + ], + "filter": {} + } + } + ] + }, + { + "table": { + "schema": "public", + "name": "artworks" + }, + "object_relationships": [ + { + "name": "artist_details", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "artists" + }, + "column_mapping": { + "artist": "id" + } + } + } + }, + { + "name": "location_details", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "locations" + }, + "column_mapping": { + "location": "id" + } + } + } + }, + { + "name": "organization_details", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "organizations" + }, + "column_mapping": { + "organizations": "id" + } + } + } + }, + { + "name": "program_details", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "programs" + }, + "column_mapping": { + "program": "id" + } + } + } + } + ], + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": [ + "description", + "featured_media", + "title", + "uid", + "ward", + "year" + ], + "filter": {} + } + } + ] + }, + { + "table": { + "schema": "public", + "name": "locations" + }, + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": [ + "address", + "latitude", + "longitude" + ], + "filter": {} + } + } + ] + }, + { + "table": { + "schema": "public", + "name": "organizations" + }, + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": [ + "name" + ], + "filter": {} + } + } + ] + }, + { + "table": { + "schema": "public", + "name": "programs" + }, + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": [ + "program_name" + ], + "filter": {} + } + } + ] + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 71fe6e8..650c5b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,26 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollo/client": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.11.tgz", + "integrity": "sha512-54+D5FB6RJlQ+g37f432gaexnyvDsG5X6L9VO5kqN54HJlbF8hCf/8CXtAQEHCWodAwZhy6kOLp2RM96829q3A==", + "requires": { + "@graphql-typed-document-node/core": "^3.0.0", + "@types/zen-observable": "^0.8.0", + "@wry/context": "^0.5.2", + "@wry/equality": "^0.3.0", + "fast-json-stable-stringify": "^2.0.0", + "graphql-tag": "^2.12.0", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.14.0", + "prop-types": "^15.7.2", + "symbol-observable": "^2.0.0", + "ts-invariant": "^0.6.0", + "tslib": "^1.10.0", + "zen-observable": "^0.8.14" + } + }, "@babel/cli": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.12.1.tgz", @@ -1391,6 +1411,11 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@graphql-typed-document-node/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", + "integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==" + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -2246,6 +2271,11 @@ } } }, + "@types/ungap__global-this": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz", + "integrity": "sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==" + }, "@types/yargs": { "version": "13.0.11", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", @@ -2259,6 +2289,11 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, + "@types/zen-observable": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz", + "integrity": "sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==" + }, "@typescript-eslint/eslint-plugin": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", @@ -2333,6 +2368,11 @@ } } }, + "@ungap/global-this": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz", + "integrity": "sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==" + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -2509,6 +2549,30 @@ "@xtuc/long": "4.2.2" } }, + "@wry/context": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.4.tgz", + "integrity": "sha512-/pktJKHUXDr4D6TJqWgudOPJW2Z+Nb+bqk40jufA3uTkLbnCRKdJPiYDIa/c7mfcPH8Hr6O8zjCERpg5Sq04Zg==", + "requires": { + "tslib": "^1.14.1" + } + }, + "@wry/equality": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.3.3.tgz", + "integrity": "sha512-pMrKHIgDAWxLDTGsbaVag+USmwZ2+gGrSBrtyGUxp2pxRg1Cad70lI/hd0NTPtJ4zJxN16EQ679U1Rts83AF5g==", + "requires": { + "tslib": "^1.14.1" + } + }, + "@wry/trie": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.2.2.tgz", + "integrity": "sha512-OxqBB39x6MfHaa2HpMiRMfhuUnQTddD32Ko020eBeJXq87ivX6xnSSnzKHVbA21p7iqBASz8n/07b6W5wW1BVQ==", + "requires": { + "tslib": "^1.14.1" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7192,6 +7256,19 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, + "graphql": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz", + "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==" + }, + "graphql-tag": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.1.tgz", + "integrity": "sha512-LPewEE1vzGkHnCO8zdOGogKsHHBdtpGyihow1UuMwp6RnZa0lAS7NcbvltLOuo4pi5diQCPASAXZkQq44ffixA==", + "requires": { + "tslib": "^1.14.1" + } + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -7376,6 +7453,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -9275,6 +9360,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -9617,14 +9707,22 @@ "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", + "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", "requires": { - "mime-db": "1.44.0" + "mime-db": "1.46.0" + }, + "dependencies": { + "mime-db": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" + } } }, "mimic-fn": { @@ -10424,6 +10522,15 @@ "is-wsl": "^1.1.0" } }, + "optimism": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.14.0.tgz", + "integrity": "sha512-ygbNt8n4DOCVpkwiLF+IrKKeNHOjtr9aXLWGP9HNJGoblSGsnVbJLstcH6/nE9Xy5ZQtlkSioFQNnthmENW6FQ==", + "requires": { + "@wry/context": "^0.5.2", + "@wry/trie": "^0.2.1" + } + }, "optimize-css-assets-webpack-plugin": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", @@ -14606,6 +14713,11 @@ } } }, + "symbol-observable": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -15062,6 +15174,16 @@ "glob": "^7.1.2" } }, + "ts-invariant": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.6.1.tgz", + "integrity": "sha512-QQgN33g8E8yrdDuH29HASveLtbzMnRRgWh0i/JNTW4+zcLsdIOnfsgEDi/NKx4UckQyuMFt9Ujm6TWLWQ58Kvg==", + "requires": { + "@types/ungap__global-this": "^0.3.1", + "@ungap/global-this": "^0.4.2", + "tslib": "^1.9.3" + } + }, "ts-pnp": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.6.tgz", @@ -15071,8 +15193,7 @@ "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tsutils": { "version": "3.17.1", @@ -16265,6 +16386,11 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" } } + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" } } } diff --git a/package.json b/package.json index 2e3e8fd..c2e0f1c 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ ], "homepage": "https://streetartoronto.ca", "dependencies": { + "@apollo/client": "^3.3.11", "@mars/heroku-js-runtime-env": "^3.0.2", "@nomadiclabs/google-maps-react": "^2.0.6", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "fast-sort": "^1.5.6", + "graphql": "^15.5.0", + "lodash-es": "^4.17.21", + "mime-types": "^2.1.29", "node-sass": "^4.13.1", "react-app-polyfill": "^1.0.6", "react-ga": "^2.7.0", @@ -45,6 +49,7 @@ "react-dom": "^16.13.1" }, "scripts": { + "clean": "rm -rf node_modules/", "analyze": "npm run build && source-map-explorer 'build/static/js/*.js'", "start": "react-scripts start", "build": "react-scripts build", diff --git a/public/index.html b/public/index.html index ac37a0c..8540bf0 100644 --- a/public/index.html +++ b/public/index.html @@ -19,6 +19,7 @@ font-src 'self' fonts.gstatic.com; media-src dl.airtable.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com unpkg.com; + connect-src 'self' %REACT_APP_GRAPHQL_HOST%; " /> diff --git a/src/lib/components/App.js b/src/lib/components/App.js index 63adb0f..04a16c6 100644 --- a/src/lib/components/App.js +++ b/src/lib/components/App.js @@ -3,6 +3,8 @@ import ReactGA from 'react-ga'; import sort from 'fast-sort'; import runtimeEnv from '@mars/heroku-js-runtime-env'; import { forceCheck } from 'react-lazyload'; +import * as _ from 'lodash'; +import { gql, ApolloClient, InMemoryCache } from '@apollo/client'; import BetaBanner from "./BetaBanner"; import Splash from "./Splash"; @@ -22,18 +24,51 @@ const Filters = lazy(() => import('./Filters')); const Header = lazy(() => import('./Header')); const Footer = lazy(() => import('./Footer')); +const client = new ApolloClient({ + uri: constants.GRAPHQL_ENDPOINT, + cache: new InMemoryCache() +}); + +const GET_ARTWORKS = gql` + query GetArtworks { + artworks(limit: 500, order_by: { uid: asc }) { + uid + title + description + year + ward + featured_media + program_details { + program_name + } + location_details { + address + latitude + longitude + } + artist_details { + preferred_name + } + organization_details { + name + } + } + } +`; + export default class App extends React.Component { state = { - /** Array of visible feature points in maps and lists. (visibleFeatures) */ + /** Array of all artwork objects that may show in maps and lists. */ allFeatures: [], - visFtrs: [], + /** Array of visible artwork IDs in maps and lists. */ + visibleFeatureIds: [], /** The type of view. * Options: list, detail, map, filter * Last two only display differently on mobile. */ viewType: "map", - /** Full object representing active artwork. */ - activeFeature: null, + /** Integer representing active artwork ID. */ + activeFeatureId: null, /** Keep track of whether any filters are applied. */ isFiltered: false, /** Array of year OptionTypes to filter features by. */ @@ -66,22 +101,15 @@ export default class App extends React.Component { } fetchFeatures() { - const isArtwork = (feature) => ( - feature.geometry && - feature.geometry.type === 'Point' - ) - - fetch(this.props.featuresDataSource) - .then(response => response.json()) - .then(json => { - const visFtrs = json.features.map(f => { - if (!isArtwork(f)) return null - return f - }).filter(Boolean) - this.setState({ allFeatures: visFtrs, visFtrs }, - // Sort after first load. - () => { this.sortList() } - ); + client.query({ query: GET_ARTWORKS }) + .then(result => { + this.setState({ + allFeatures: result.data.artworks, + visibleFeatureIds: result.data.artworks.map(f => f.uid), + }, + // Sort after first load. + () => { this.sortList() } + ) }); } @@ -103,9 +131,9 @@ export default class App extends React.Component { }) } - setVisibleFeatures = (visFtrs) => { + setVisibleFeatureIds = (visibleFeatureIds) => { this.setState( - {visFtrs: visFtrs}, + { visibleFeatureIds }, () => { this.sortList() } ); } @@ -124,35 +152,13 @@ export default class App extends React.Component { * @returns {undefined} */ filterFeatures = (activeYearOpts, activeWardOpts, activeProgramOpts) => { - - const checkForKeep = (feature, propName, activeOpts) => { - for (let i = 0; i < activeOpts.length; i++) { - if (feature.properties[propName] && - feature.properties[propName].toString() === activeOpts[i].value.toString() - ) { - return true; - } - } - return false; - } - - const isArtwork = (feature) => ( - feature.geometry !== null && - feature.geometry.type === 'Point' + const visibleFeatureIds = _.intersection( + _.filter(this.state.allFeatures, f => _.includes(activeYearOpts.map(o => o.value), f.year.toString())).map(f => f.uid), + _.filter(this.state.allFeatures, f => _.includes(activeWardOpts.map(o => o.value), f.ward.toString())).map(f => f.uid), + _.filter(this.state.allFeatures, f => _.includes(activeProgramOpts.map(o => o.value), f.program_details?.program_name.toString())).map(f => f.uid), ) - - const visibleFeatures = this.state.allFeatures.filter(feature => { - if (!isArtwork(feature)) { return false } - - let keepForYear = checkForKeep(feature, 'year', activeYearOpts) - let keepForWard = checkForKeep(feature, 'ward', activeWardOpts) - let keepForProgram = checkForKeep(feature, 'program', activeProgramOpts) - - return keepForYear && keepForWard && keepForProgram; - }) - - this.setVisibleFeatures(visibleFeatures); + this.setVisibleFeatureIds(visibleFeatureIds); } handleSelectYears = (selectedOptions) => { @@ -212,47 +218,46 @@ export default class App extends React.Component { switch(this.state.sortType) { case 'artist-asc': default: - sortedList = sort(this.state.visFtrs).asc(u => u.properties.title ? u.properties.title.toLowerCase() : u.properties.title) + sortedList = sort(this.state.visibleFeatureIds).asc(id => _.find(this.state.allFeatures, { uid: id }).title?.toLowerCase()) break case 'artist-desc': - sortedList = sort(this.state.visFtrs).desc(u => u.properties.title ? u.properties.title.toLowerCase() : u.properties.title) + sortedList = sort(this.state.visibleFeatureIds).desc(id => _.find(this.state.allFeatures, { uid: id }).title?.toLowerCase()) break case 'year-asc': - sortedList = sort(this.state.visFtrs).asc(u => u.properties.year) + sortedList = sort(this.state.visibleFeatureIds).asc(id => _.find(this.state.allFeatures, { uid: id }).year) break case 'year-desc': - sortedList = sort(this.state.visFtrs).desc(u => u.properties.year) + sortedList = sort(this.state.visibleFeatureIds).desc(id => _.find(this.state.allFeatures, { uid: id }).year) break } - this.setState({visFtrs: sortedList}) + this.setState({ visibleFeatureIds: sortedList }) } - handleMapClick = (feature) => { + handleMapClick = (featureId) => { ReactGA.event({ category: 'Map', action: 'Clicked feature', label: 'ward or artwork', }) - this.setActiveFeature(feature) + this.setActiveFeatureId(featureId) } - setActiveFeature = (feature) => { + setActiveFeatureId = (featureId) => { this.setState({ - activeFeature: feature, + activeFeatureId: featureId, }); } handleCloseFeature = () => { - const uid = this.state.activeFeature.properties.uid if (typeof(document) !== 'undefined') { - const featureBtn = document.getElementById(uid) + const featureBtn = document.getElementById(this.state.activeFeatureId) featureBtn.scrollIntoView() featureBtn.focus() } this.setState({ - activeFeature: null + activeFeatureId: null }) } @@ -275,14 +280,17 @@ export default class App extends React.Component { render() { const { showSplash, - visFtrs, - activeFeature, + allFeatures, + visibleFeatureIds, + activeFeatureId, isMobileView, isFiltered, viewType, showWardLayer, } = this.state; + const activeFeature = _.find(allFeatures, { uid: activeFeatureId }) + return (
@@ -299,8 +307,9 @@ export default class App extends React.Component { }> @@ -319,8 +328,9 @@ export default class App extends React.Component { /> @@ -331,7 +341,8 @@ export default class App extends React.Component { { let mediaData = []; - if (ftr.geometry.type === "Point") { - if (ftr.properties.media) { - mediaData = ftr.properties.media.map( mediaItem => ({ - type: mediaItem.type, - mediaSrc: mediaItem.thumbnails ? mediaItem.thumbnails.large.url : mediaItem.url, - mediaAltText: "Photo of artwork.", - })) - } else { - mediaData = [{ - type: 'image/', - mediaSrc: placeholder, - mediaAltText: "Image not available.", - }] - } + if (ftr.featured_media.length > 0) { + mediaData = ftr.featured_media.map( mediaItem => ({ + type: mime.lookup(mediaItem), + mediaSrc: mediaItem, + mediaAltText: "Photo of artwork.", + })) + } else { + mediaData = [{ + type: 'image/jpg', + mediaSrc: placeholder, + mediaAltText: "Image not available.", + }] } - return mediaData; + // Because airtable returns media items in reverse order of Airtable UI. + return mediaData.reverse(); } @@ -64,37 +64,37 @@ class FeatureDetail extends React.Component {

- {feature.properties['title']} + {feature.title}

- {feature.properties['artist']} + {feature.artist_details?.preferred_name}

- {feature.properties['address']} + {feature.location_details?.address}

- {feature.properties['description']} + {feature.description}

{ - feature.properties['organizations'] && + feature.organization_details &&
Partner organization
-
{feature.properties['organizations']}
+
{feature.organization_details.name}
}
Ward
-
{feature.properties['ward']}
+
{feature.ward[0] || ''}
Program
-
{feature.properties['program']}
+
{feature.program_details?.program_name}
Year
-
{feature.properties['year']}
+
{feature.year}
diff --git a/src/lib/components/FeatureList.js b/src/lib/components/FeatureList.js index c43e2f2..6d20b12 100644 --- a/src/lib/components/FeatureList.js +++ b/src/lib/components/FeatureList.js @@ -1,27 +1,29 @@ import React, { useEffect } from 'react'; import PropTypes from "prop-types"; import { forceCheck } from 'react-lazyload'; +import * as _ from 'lodash'; import FeatureListItem from './FeatureListItem'; -const FeatureList = ({ features, onItemClick, isMobile, activeFeature }) => { +const FeatureList = ({ allFeatures = [], featureIds = [], onItemClick, isMobile, activeFeature }) => { useEffect(() => { forceCheck() }); return (
-

{features.length} Results

+

{featureIds.length} Results

    - {features.map(feature => - { + const feature = _.find(allFeatures, { uid: id }) + return - )} + })}
); @@ -29,8 +31,8 @@ const FeatureList = ({ features, onItemClick, isMobile, activeFeature }) => { FeatureListItem.propTypes = { - features: PropTypes.arrayOf(PropTypes.object), + featureIds: PropTypes.arrayOf(PropTypes.number), onItemClick: PropTypes.func, } -export default FeatureList \ No newline at end of file +export default FeatureList diff --git a/src/lib/components/FeatureListItem.js b/src/lib/components/FeatureListItem.js index baca9a0..b3b041e 100644 --- a/src/lib/components/FeatureListItem.js +++ b/src/lib/components/FeatureListItem.js @@ -6,7 +6,7 @@ import * as utils from "../utils"; const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => { - const { uid=0, year, artist, title, media=[], address } = feature.properties; + const { uid, year, artist='', title, featured_media, location_details } = feature; const handleClick = () => { ReactGA.event({ @@ -16,7 +16,7 @@ const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => { value: uid, }) - onClick(feature) + onClick(uid) } const handleKeyPress = (event) => { @@ -33,7 +33,7 @@ const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => { onKeyPress={handleKeyPress} tabIndex={0} role="button" - aria-expanded={activeFeature && (activeFeature.properties.uid === uid) ? 'true' : 'false'} + aria-expanded={activeFeature && (activeFeature.uid === uid) ? 'true' : 'false'} aria-controls='detail' id={uid} > @@ -44,7 +44,7 @@ const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => { aria-label="Thumbnail Preview" alt="Photo of artwork" className="list-img" - src={utils.getCoverImage(media)} + src={utils.getCoverImage(featured_media)} onError={utils.handleMissingImage} /> @@ -59,7 +59,7 @@ const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => {

}

- {address} + {location_details?.address}

{year} @@ -72,7 +72,7 @@ const FeatureListItem = ({ feature, onClick, isMobile, activeFeature }) => { FeatureListItem.propTypes = { uid: PropTypes.number, - media: PropTypes.arrayOf(PropTypes.object), + featured_media: PropTypes.arrayOf(PropTypes.string), artistName: PropTypes.string, address: PropTypes.string, year: PropTypes.number, @@ -81,7 +81,7 @@ FeatureListItem.propTypes = { FeatureListItem.defaultProps = { uid: 0, - media: [], + featured_media: [], } -export default FeatureListItem \ No newline at end of file +export default FeatureListItem diff --git a/src/lib/components/InteractiveMap.js b/src/lib/components/InteractiveMap.js index 7022765..1dc00b4 100644 --- a/src/lib/components/InteractiveMap.js +++ b/src/lib/components/InteractiveMap.js @@ -1,6 +1,7 @@ import React, { createRef, lazy, Suspense } from 'react'; import PropTypes from "prop-types"; import { Map, Marker, GoogleApiWrapper } from '@nomadiclabs/google-maps-react'; +import * as _ from 'lodash'; import * as constants from "../constants"; @@ -17,8 +18,8 @@ class InteractiveMap extends React.Component { constructor(props) { super(props) this.state = { - prevActiveFeature: {}, - features: this.props.features, + allFeatures: this.props.allFeatures, + visibleFeatureIds: this.props.visibleFeatureIds, wards: {}, } this.mapRef = createRef() @@ -36,8 +37,8 @@ class InteractiveMap extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.features !== this.props.features) { - this.setState({ features: this.props.features }) + if (prevProps.visibleFeatureIds !== this.props.visibleFeatureIds) { + this.setState({ visibleFeatureIds: this.props.visibleFeatureIds }) } if (prevProps.showWardLayer !== this.props.showWardLayer) { @@ -47,8 +48,13 @@ class InteractiveMap extends React.Component { if (!this.props.isMobile && this.props.activeFeature && prevProps.activeFeature !== this.props.activeFeature) { const map = this.map if (map != null) { - const coords = this.props.activeFeature.geometry.coordinates - let center = new this.props.google.maps.LatLng(coords[1], coords[0]) + const { + // Shouldn't happen, but if latlngs are missing, + // use map center so won't error. + latitude = constants.DEFAULT_MAP_CENTER.lat, + longitude = constants.DEFAULT_MAP_CENTER.lng, + } = this.props.activeFeature.location_details || {} + let center = new this.props.google.maps.LatLng(latitude, longitude) map.panTo(center) } } @@ -124,8 +130,8 @@ class InteractiveMap extends React.Component { render() { - const { loaded, google, activeFeature, onFeatureMapClick } = this.props; - const { features } = this.state; + const { allFeatures, loaded, google, activeFeature, onFeatureMapClick } = this.props; + const { visibleFeatureIds } = this.state; const zoom = activeFeature ? constants.MAP_ZOOM_LEVEL.FEATURE : constants.MAP_ZOOM_LEVEL.DEFAULT const settings = { ...this.mapSettings, zoom } const center = activeFeature ? null : constants.DEFAULT_MAP_CENTER; @@ -146,10 +152,11 @@ class InteractiveMap extends React.Component { > { - features.map((feature, i) => { + visibleFeatureIds.map((id) => { + const feature = _.find(allFeatures, { uid: id }); const validPrograms = ["StART Support", "Partnership Program", "Outside the Box"] - const program = validPrograms.includes(feature.properties.program) ? feature.properties.program : "Other" - const isSelected = activeFeature && feature.properties.uid === activeFeature.properties.uid + const program = validPrograms.includes(feature.program_details?.program_name) ? feature.program_details.program_name : "Other" + const isSelected = activeFeature && feature.uid === activeFeature.uid const icon = { url: constants.ICONS_REG[program].icon, anchor: isSelected ? new google.maps.Point(20, 20) : new google.maps.Point(10, 10), @@ -159,10 +166,10 @@ class InteractiveMap extends React.Component { return( onFeatureMapClick(feature) } + position={{ lng: feature.location_details?.longitude, lat: feature.location_details?.latitude }} + onClick={ () => onFeatureMapClick(feature.uid) } zIndex={isSelected ? 2 : 1} /> ) diff --git a/src/lib/components/MapMarkers.js b/src/lib/components/MapMarkers.js index a1a4559..7691f6f 100644 --- a/src/lib/components/MapMarkers.js +++ b/src/lib/components/MapMarkers.js @@ -67,7 +67,7 @@ const MapMarkers = ({ features, activeFeature, onFeatureMapClick }) => { key={feature.properties.uid} position={[feature.geometry.coordinates[1], feature.geometry.coordinates[0]]} icon={icon} - onClick={() => onFeatureMapClick(feature) } + onClick={() => onFeatureMapClick(feature.uid) } zIndexOffset={isSelected ? 9999 : 0} /> ) diff --git a/src/lib/constants.js b/src/lib/constants.js index 06529f1..105bcaf 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -3,6 +3,8 @@ import markerRed from './assets/img/marker-red.svg'; import markerGreen from './assets/img/marker-green.svg'; import markerYellow from './assets/img/marker-yellow.svg'; +export const GRAPHQL_ENDPOINT = 'https://start-v3.hasura.app/v1/graphql'; + export const ICONS_REG = { "Partnership Program": { icon: markerBlue diff --git a/src/lib/utils.js b/src/lib/utils.js index 7cd2b03..f69e8c8 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -7,9 +7,9 @@ export function handleMissingImage(e) { export const getCoverImage = (media) => { for (const mediaItem of media) { - if (!!mediaItem.thumbnails) { - return mediaItem.thumbnails.large.url - } + // TODO: Check on pending feature request for thumbnails from SyncInc. + // Not currently available. + return mediaItem } return '' }