1
Fork 0

Rewrite PlacesPage to Typescript

This commit is contained in:
viktorstrate 2021-04-15 15:15:24 +02:00
parent de76360fbf
commit 788a8e7c62
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
12 changed files with 208 additions and 111 deletions

View File

@ -13,7 +13,7 @@ module.exports = function (api) {
plugins.push([
'i18next-extract',
{
locales: ['en', 'da'],
locales: ['en', 'da', 'fr', 'sv'],
defaultValue: null,
},
])

View File

@ -75,6 +75,9 @@
"photos_page": {
"title": "Billeder"
},
"places_page": {
"title": null
},
"routes": {
"page_not_found": "Side ikke fundet"
},

View File

@ -75,6 +75,9 @@
"photos_page": {
"title": "Photos"
},
"places_page": {
"title": "Places"
},
"routes": {
"page_not_found": "Page not found"
},

View File

@ -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"

View File

@ -75,6 +75,9 @@
"photos_page": {
"title": "Bilder"
},
"places_page": {
"title": null
},
"routes": {
"page_not_found": "Sidan hittades inte"
},

32
ui/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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) => {
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<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

View File

@ -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.

View File

@ -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
}

View File

@ -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
}