diff --git a/ui/babel.config.js b/ui/babel.config.js index 8967ba44..80ce92a3 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -13,7 +13,7 @@ module.exports = function (api) { plugins.push([ 'i18next-extract', { - locales: ['en', 'da'], + locales: ['en', 'da', 'fr', 'sv'], defaultValue: null, }, ]) diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 5c106251..bfa96825 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -75,6 +75,9 @@ "photos_page": { "title": "Billeder" }, + "places_page": { + "title": null + }, "routes": { "page_not_found": "Side ikke fundet" }, diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 09f8ced2..820513e0 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -75,6 +75,9 @@ "photos_page": { "title": "Photos" }, + "places_page": { + "title": "Places" + }, "routes": { "page_not_found": "Page not found" }, diff --git a/ui/extractedTranslations/fr/translation.json b/ui/extractedTranslations/fr/translation.json index 83ca3fd4..6897f30f 100644 --- a/ui/extractedTranslations/fr/translation.json +++ b/ui/extractedTranslations/fr/translation.json @@ -75,6 +75,9 @@ "photos_page": { "title": "Photos" }, + "places_page": { + "title": null + }, "routes": { "page_not_found": "Page non trouvée" }, @@ -218,7 +221,7 @@ "exposure": "Exposure", "exposure_program": "Programme", "flash": "Flash", - "de prise": "Distance focale", + "focal_length": "Distance focale", "iso": "ISO", "lens": "Objectif", "maker": "Marque" diff --git a/ui/extractedTranslations/sv/translation.json b/ui/extractedTranslations/sv/translation.json index 3b55f608..72be0a1c 100644 --- a/ui/extractedTranslations/sv/translation.json +++ b/ui/extractedTranslations/sv/translation.json @@ -75,6 +75,9 @@ "photos_page": { "title": "Bilder" }, + "places_page": { + "title": null + }, "routes": { "page_not_found": "Sidan hittades inte" }, diff --git a/ui/package-lock.json b/ui/package-lock.json index c0733f65..221dce9e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -55,7 +55,9 @@ "@babel/preset-env": "^7.13.15", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", + "@types/geojson": "^7946.0.7", "@types/jest": "^26.0.22", + "@types/mapbox-gl": "^2.1.1", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-helmet": "^6.1.1", @@ -2735,6 +2737,12 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "node_modules/@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 + }, "node_modules/@types/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -2795,6 +2803,15 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" }, + "node_modules/@types/mapbox-gl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.1.1.tgz", + "integrity": "sha512-oDgHhkAC9iYIj/H/9iwm6gHHKKPzweTUgOlozcrvWr8T07dyymGty6mON2j4kEeBGyTTI291vI6gP9k2ArxvHw==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "14.11.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", @@ -16369,6 +16386,12 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "@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/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -16429,6 +16452,15 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" }, + "@types/mapbox-gl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.1.1.tgz", + "integrity": "sha512-oDgHhkAC9iYIj/H/9iwm6gHHKKPzweTUgOlozcrvWr8T07dyymGty6mON2j4kEeBGyTTI291vI6gP9k2ArxvHw==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, "@types/node": { "version": "14.11.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", diff --git a/ui/package.json b/ui/package.json index c207876a..83aabc91 100644 --- a/ui/package.json +++ b/ui/package.json @@ -66,7 +66,9 @@ "@babel/preset-env": "^7.13.15", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", + "@types/geojson": "^7946.0.7", "@types/jest": "^26.0.22", + "@types/mapbox-gl": "^2.1.1", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-helmet": "^6.1.1", diff --git a/ui/src/Pages/PlacesPage/MapClusterMarker.js b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx similarity index 64% rename from ui/src/Pages/PlacesPage/MapClusterMarker.js rename to ui/src/Pages/PlacesPage/MapClusterMarker.tsx index 52e10bcb..40165d72 100644 --- a/ui/src/Pages/PlacesPage/MapClusterMarker.js +++ b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx @@ -1,8 +1,9 @@ import React from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import imagePopupSrc from './image-popup.svg' +import { MediaMarker } from './MapPresentMarker' +import { PresentMarker } from './PlacesPage' const Wrapper = styled.div` width: 56px; @@ -40,20 +41,21 @@ const PointCountCircle = styled.div` padding-top: 2px; ` +type MapClusterMarkerProps = { + setPresentMarker: React.Dispatch> + marker: MediaMarker +} + const MapClusterMarker = ({ - thumbnail: thumbJson, - point_count_abbreviated, - cluster, - cluster_id, - media_id, + marker, setPresentMarker, -}) => { - const thumbnail = JSON.parse(thumbJson) +}: MapClusterMarkerProps) => { + const thumbnail = JSON.parse(marker.thumbnail) const presentMedia = () => { setPresentMarker({ - cluster: !!cluster, - id: cluster ? cluster_id : media_id, + cluster: !!marker.cluster, + id: marker.cluster ? marker.cluster_id : marker.media_id, }) } @@ -61,20 +63,11 @@ const MapClusterMarker = ({ - {cluster && ( - {point_count_abbreviated} + {marker.cluster && ( + {marker.point_count_abbreviated} )} ) } -MapClusterMarker.propTypes = { - thumbnail: PropTypes.string, - cluster: PropTypes.bool, - point_count_abbreviated: PropTypes.number, - cluster_id: PropTypes.number, - media_id: PropTypes.number, - setPresentMarker: PropTypes.func, -} - export default MapClusterMarker diff --git a/ui/src/Pages/PlacesPage/MapPresentMarker.js b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx similarity index 55% rename from ui/src/Pages/PlacesPage/MapPresentMarker.js rename to ui/src/Pages/PlacesPage/MapPresentMarker.tsx index 57605ccc..defddf17 100644 --- a/ui/src/Pages/PlacesPage/MapPresentMarker.js +++ b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx @@ -1,8 +1,9 @@ import { gql } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' import { useLazyQuery } from '@apollo/client' import PresentView from '../../components/photoGallery/presentView/PresentView' +import type mapboxgl from 'mapbox-gl' +import { PresentMarker } from './PlacesPage' const QUERY_MEDIA = gql` query placePageQueryMedia($mediaIDs: [ID!]!) { @@ -29,31 +30,57 @@ const QUERY_MEDIA = gql` } ` -const getMediaFromMarker = (map, presentMarker) => - new Promise((resolve, reject) => { +const getMediaFromMarker = (map: mapboxgl.Map, presentMarker: PresentMarker) => + new Promise((resolve, reject) => { const { cluster, id } = presentMarker if (cluster) { - map - .getSource('media') - .getClusterLeaves(id, 1000, 0, (error, features) => { - if (error) { - reject(error) - return - } + const mediaSource = map.getSource('media') as mapboxgl.GeoJSONSource - const media = features.map(feat => feat.properties) - resolve(media) - }) + mediaSource.getClusterLeaves(id, 1000, 0, (error, features) => { + if (error) { + reject(error) + return + } + + const media = features.map(feat => feat.properties) as MediaMarker[] + resolve(media) + }) } else { const features = map.querySourceFeatures('media') - const media = features.find(f => f.properties.media_id == id).properties + const media = features.find(f => f.properties?.media_id == id) + ?.properties as MediaMarker | undefined + + if (media === undefined) { + reject('ERROR: media is undefined') + return + } + resolve([media]) } }) -const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => { - const [mediaMarkers, setMediaMarkers] = useState(null) +export interface MediaMarker { + id: number + thumbnail: string + cluster: boolean + point_count_abbreviated: number + cluster_id: number + media_id: number +} + +type MapPresetMarkerProps = { + map: mapboxgl.Map | null + presentMarker: PresentMarker | null + setPresentMarker: React.Dispatch> +} + +const MapPresentMarker = ({ + map, + presentMarker, + setPresentMarker, +}: MapPresetMarkerProps) => { + const [mediaMarkers, setMediaMarkers] = useState(null) const [currentIndex, setCurrentIndex] = useState(0) const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA) @@ -78,11 +105,12 @@ const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => { }) }, [mediaMarkers]) - if (presentMarker == null || map == null) { - return null - } - - if (loadedMedia == null) { + if ( + presentMarker == null || + map == null || + mediaMarkers == null || + loadedMedia == null + ) { return null } @@ -105,10 +133,4 @@ const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => { ) } -MapPresentMarker.propTypes = { - map: PropTypes.object, - presentMarker: PropTypes.object, - setPresentMarker: PropTypes.func.isRequired, -} - export default MapPresentMarker diff --git a/ui/src/Pages/PlacesPage/PlacesPage.js b/ui/src/Pages/PlacesPage/PlacesPage.tsx similarity index 82% rename from ui/src/Pages/PlacesPage/PlacesPage.js rename to ui/src/Pages/PlacesPage/PlacesPage.tsx index 610a64ac..9e8b2b20 100644 --- a/ui/src/Pages/PlacesPage/PlacesPage.js +++ b/ui/src/Pages/PlacesPage/PlacesPage.tsx @@ -1,14 +1,15 @@ +import { gql, useQuery } from '@apollo/client' +import type mapboxgl from 'mapbox-gl' import React, { useEffect, useRef, useState } from 'react' -import { useQuery, gql } from '@apollo/client' +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' - -// Will be bundled to dist/src/Pages/PlacesPage/PlacesPage.css -import 'mapbox-gl/dist/mapbox-gl.css' - import Layout from '../../Layout' import { makeUpdateMarkers } from './mapboxHelperFunctions' import MapPresentMarker from './MapPresentMarker' -import { Helmet } from 'react-helmet' + +// Will be bundled to dist/src/Pages/PlacesPage/PlacesPage.css +import 'mapbox-gl/dist/mapbox-gl.css' const MapWrapper = styled.div` width: 100%; @@ -27,17 +28,25 @@ const MAPBOX_DATA_QUERY = gql` } ` +export type PresentMarker = { + id: number + cluster: boolean +} + const MapPage = () => { - const [mapboxLibrary, setMapboxLibrary] = useState(null) - const [presentMarker, setPresentMarker] = useState(null) - const mapContainer = useRef() - const map = useRef() + const { t } = useTranslation() + + const [mapboxLibrary, setMapboxLibrary] = useState() + const [presentMarker, setPresentMarker] = useState(null) + const mapContainer = useRef(null) + const map = useRef(null) const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY) useEffect(() => { async function loadMapboxLibrary() { const mapbox = (await import('mapbox-gl')).default + setMapboxLibrary(mapbox) } loadMapboxLibrary() @@ -65,6 +74,11 @@ const MapPage = () => { map.current.addControl(new mapboxLibrary.NavigationControl()) map.current.on('load', () => { + if (map.current == null) { + console.error('ERROR: map is null') + return + } + map.current.addSource('media', { type: 'geojson', data: mapboxData.myMediaGeoJson, @@ -98,7 +112,7 @@ const MapPage = () => { if (mapboxData && mapboxData.mapboxToken == null) { return ( - +

Mapbox token is not set

To use map related features a mapbox token is needed. diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js deleted file mode 100644 index 32e412c3..00000000 --- a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import MapClusterMarker from './MapClusterMarker' - -let markers = {} -let markersOnScreen = {} - -export const makeUpdateMarkers = ({ - map, - mapboxLibrary, - setPresentMarker, -}) => () => { - let newMarkers = {} - const features = map.querySourceFeatures('media') - - // for every media on the screen, create an HTML marker for it (if we didn't yet), - // and add it to the map if it's not there already - for (let i = 0; i < features.length; i++) { - const coords = features[i].geometry.coordinates - const props = features[i].properties - const id = props.cluster - ? `cluster_${props.cluster_id}` - : `media_${props.media_id}` - - let marker = markers[id] - if (!marker) { - let el = createClusterPopupElement(props, setPresentMarker) - marker = markers[id] = new mapboxLibrary.Marker({ - element: el, - }).setLngLat(coords) - } - newMarkers[id] = marker - - if (!markersOnScreen[id]) marker.addTo(map) - } - // for every marker we've added previously, remove those that are no longer visible - for (const id in markersOnScreen) { - if (!newMarkers[id]) markersOnScreen[id].remove() - } - markersOnScreen = newMarkers -} - -function createClusterPopupElement(geojsonProps, setPresentMarker) { - const el = document.createElement('div') - ReactDOM.render( - , - el - ) - return el -} diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx new file mode 100644 index 00000000..86dd9656 --- /dev/null +++ b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx @@ -0,0 +1,72 @@ +import type mapboxgl from 'mapbox-gl' +import type geojson from 'geojson' +import React from 'react' +import ReactDOM from 'react-dom' +import MapClusterMarker from './MapClusterMarker' +import { MediaMarker } from './MapPresentMarker' +import { PresentMarker } from './PlacesPage' + +const markers: { [key: string]: mapboxgl.Marker } = {} +let markersOnScreen: typeof markers = {} + +type makeUpdateMarkersArgs = { + map: mapboxgl.Map + mapboxLibrary: typeof mapboxgl + setPresentMarker: React.Dispatch> +} + +export const makeUpdateMarkers = ({ + map, + mapboxLibrary, + setPresentMarker, +}: makeUpdateMarkersArgs) => () => { + const newMarkers: typeof markers = {} + const features = map.querySourceFeatures('media') + + // for every media on the screen, create an HTML marker for it (if we didn't yet), + // and add it to the map if it's not there already + for (const feature of features) { + const point = feature.geometry as geojson.Point + const coords = point.coordinates as [number, number] + const props = feature.properties as MediaMarker + if (props == null) { + console.warn('WARN: geojson feature had no properties', feature) + continue + } + + const id = props.cluster + ? `cluster_${props.cluster_id}` + : `media_${props.media_id}` + + let marker = markers[id] + if (!marker) { + const el = createClusterPopupElement(props, setPresentMarker) + marker = markers[id] = new mapboxLibrary.Marker({ + element: el, + }).setLngLat(coords) + } + newMarkers[id] = marker + + if (!markersOnScreen[id]) marker.addTo(map) + } + // for every marker we've added previously, remove those that are no longer visible + for (const id in markersOnScreen) { + if (!newMarkers[id]) markersOnScreen[id].remove() + } + markersOnScreen = newMarkers +} + +function createClusterPopupElement( + geojsonProps: MediaMarker, + setPresentMarker: React.Dispatch> +) { + const el = document.createElement('div') + ReactDOM.render( + , + el + ) + return el +}