Rewrite PlacesPage to Typescript
This commit is contained in:
parent
de76360fbf
commit
788a8e7c62
|
@ -13,7 +13,7 @@ module.exports = function (api) {
|
|||
plugins.push([
|
||||
'i18next-extract',
|
||||
{
|
||||
locales: ['en', 'da'],
|
||||
locales: ['en', 'da', 'fr', 'sv'],
|
||||
defaultValue: null,
|
||||
},
|
||||
])
|
||||
|
|
|
@ -75,6 +75,9 @@
|
|||
"photos_page": {
|
||||
"title": "Billeder"
|
||||
},
|
||||
"places_page": {
|
||||
"title": null
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Side ikke fundet"
|
||||
},
|
||||
|
|
|
@ -75,6 +75,9 @@
|
|||
"photos_page": {
|
||||
"title": "Photos"
|
||||
},
|
||||
"places_page": {
|
||||
"title": "Places"
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Page not found"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -75,6 +75,9 @@
|
|||
"photos_page": {
|
||||
"title": "Bilder"
|
||||
},
|
||||
"places_page": {
|
||||
"title": null
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Sidan hittades inte"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<React.SetStateAction<PresentMarker | null>>
|
||||
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 = ({
|
|||
<Wrapper onClick={presentMedia}>
|
||||
<PopupImage src={imagePopupSrc} />
|
||||
<ThumbnailImage src={thumbnail.url} />
|
||||
{cluster && (
|
||||
<PointCountCircle>{point_count_abbreviated}</PointCountCircle>
|
||||
{marker.cluster && (
|
||||
<PointCountCircle>{marker.point_count_abbreviated}</PointCountCircle>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
|
@ -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<MediaMarker[]>((resolve, reject) => {
|
||||
const { cluster, id } = presentMarker
|
||||
|
||||
if (cluster) {
|
||||
map
|
||||
.getSource('media')
|
||||
.getClusterLeaves(id, 1000, 0, (error, features) => {
|
||||
const mediaSource = map.getSource('media') as mapboxgl.GeoJSONSource
|
||||
|
||||
mediaSource.getClusterLeaves(id, 1000, 0, (error, features) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
const media = features.map(feat => feat.properties)
|
||||
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<React.SetStateAction<PresentMarker | null>>
|
||||
}
|
||||
|
||||
const MapPresentMarker = ({
|
||||
map,
|
||||
presentMarker,
|
||||
setPresentMarker,
|
||||
}: MapPresetMarkerProps) => {
|
||||
const [mediaMarkers, setMediaMarkers] = useState<MediaMarker[] | null>(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
|
|
@ -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<typeof mapboxgl | null>()
|
||||
const [presentMarker, setPresentMarker] = useState<PresentMarker | null>(null)
|
||||
const mapContainer = useRef<HTMLDivElement | null>(null)
|
||||
const map = useRef<mapboxgl.Map | null>(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 (
|
||||
<Layout>
|
||||
<Layout title={t('places_page.title', 'Places')}>
|
||||
<h1>Mapbox token is not set</h1>
|
||||
<p>
|
||||
To use map related features a mapbox token is needed.
|
|
@ -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(
|
||||
<MapClusterMarker {...geojsonProps} setPresentMarker={setPresentMarker} />,
|
||||
el
|
||||
)
|
||||
return el
|
||||
}
|
|
@ -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<React.SetStateAction<PresentMarker | null>>
|
||||
}
|
||||
|
||||
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<React.SetStateAction<PresentMarker | null>>
|
||||
) {
|
||||
const el = document.createElement('div')
|
||||
ReactDOM.render(
|
||||
<MapClusterMarker
|
||||
marker={geojsonProps}
|
||||
setPresentMarker={setPresentMarker}
|
||||
/>,
|
||||
el
|
||||
)
|
||||
return el
|
||||
}
|
Loading…
Reference in New Issue