mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
- Separated types and moved them into the proper ./types folder defined in the tsconfig.json. - Defined a new folder alias `#types`. - New eslint rule to enforce `import type` when a type is imported. - Removed Geometry Class and used manual Point types from `geojson`
196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
import { LatLng } from 'leaflet'
|
|
import {
|
|
Children,
|
|
cloneElement,
|
|
createRef,
|
|
isValidElement,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import { TileLayer, MapContainer, useMapEvents, GeoJSON } from 'react-leaflet'
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import 'leaflet/dist/leaflet.css'
|
|
import MarkerClusterGroup from 'react-leaflet-cluster'
|
|
import { Outlet } from 'react-router-dom'
|
|
import { toast } from 'react-toastify'
|
|
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import './UtopiaMap.css'
|
|
|
|
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
|
|
import { useAddVisibleLayer } from './hooks/useFilter'
|
|
import { useLayers } from './hooks/useLayers'
|
|
import {
|
|
useSelectPosition,
|
|
useSetMapClicked,
|
|
useSetSelectPosition,
|
|
} from './hooks/useSelectPosition'
|
|
import AddButton from './Subcomponents/AddButton'
|
|
import { Control } from './Subcomponents/Controls/Control'
|
|
import { FilterControl } from './Subcomponents/Controls/FilterControl'
|
|
import { GratitudeControl } from './Subcomponents/Controls/GratitudeControl'
|
|
import { LayerControl } from './Subcomponents/Controls/LayerControl'
|
|
import { SearchControl } from './Subcomponents/Controls/SearchControl'
|
|
import { TagsControl } from './Subcomponents/Controls/TagsControl'
|
|
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
|
|
import { SelectPosition } from './Subcomponents/SelectPosition'
|
|
|
|
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
|
|
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
|
|
import type { Feature, Geometry as GeoJSONGeometry } from 'geojson'
|
|
|
|
const mapDivRef = createRef()
|
|
|
|
export function UtopiaMapInner({
|
|
height = '500px',
|
|
width = '100%',
|
|
center = [50.6, 9.5],
|
|
zoom = 10,
|
|
children,
|
|
geo,
|
|
showFilterControl = false,
|
|
showGratitudeControl = false,
|
|
showLayerControl = true,
|
|
infoText,
|
|
}: UtopiaMapProps) {
|
|
// Hooks that rely on contexts, called after ContextWrapper is provided
|
|
const selectNewItemPosition = useSelectPosition()
|
|
const setSelectNewItemPosition = useSetSelectPosition()
|
|
const setClusterRef = useSetClusterRef()
|
|
const clusterRef = useClusterRef()
|
|
const setMapClicked = useSetMapClicked()
|
|
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null)
|
|
|
|
const layers = useLayers()
|
|
const addVisibleLayer = useAddVisibleLayer()
|
|
|
|
useEffect(() => {
|
|
layers.forEach((layer) => addVisibleLayer(layer))
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [layers])
|
|
|
|
const init = useRef(false)
|
|
useEffect(() => {
|
|
if (!init.current) {
|
|
infoText &&
|
|
setTimeout(() => {
|
|
toast(<TextView rawText={infoText} />, { autoClose: false })
|
|
}, 4000)
|
|
init.current = true
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
function MapEventListener() {
|
|
useMapEvents({
|
|
click: (e) => {
|
|
resetMetaTags()
|
|
// eslint-disable-next-line no-console
|
|
console.log(e.latlng.lat + ',' + e.latlng.lng)
|
|
if (selectNewItemPosition) {
|
|
setMapClicked({ position: e.latlng, setItemFormPopup })
|
|
}
|
|
},
|
|
moveend: () => {},
|
|
})
|
|
return null
|
|
}
|
|
|
|
const resetMetaTags = () => {
|
|
const params = new URLSearchParams(window.location.search)
|
|
if (!window.location.pathname.includes('/item/')) {
|
|
window.history.pushState({}, '', '/' + `${params.toString() !== '' ? `?${params}` : ''}`)
|
|
}
|
|
document.title = document.title.split('-')[0]
|
|
document.querySelector('meta[property="og:title"]')?.setAttribute('content', document.title)
|
|
document
|
|
.querySelector('meta[property="og:description"]')
|
|
?.setAttribute(
|
|
'content',
|
|
`${document.querySelector('meta[name="description"]')?.getAttribute('content')}`,
|
|
)
|
|
}
|
|
|
|
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {
|
|
if (feature.properties) {
|
|
layer.bindPopup(feature.properties.name)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`tw-h-full ${selectNewItemPosition != null ? 'crosshair-cursor-enabled' : undefined}`}
|
|
>
|
|
<MapContainer
|
|
ref={mapDivRef}
|
|
style={{ height, width }}
|
|
center={new LatLng(center[0], center[1])}
|
|
zoom={zoom}
|
|
zoomControl={false}
|
|
maxZoom={19}
|
|
>
|
|
<Outlet />
|
|
<Control position='topLeft' zIndex='1000' absolute>
|
|
<SearchControl />
|
|
<TagsControl />
|
|
</Control>
|
|
<Control position='bottomLeft' zIndex='999' absolute>
|
|
{showFilterControl && <FilterControl />}
|
|
{showLayerControl && <LayerControl />}
|
|
{showGratitudeControl && <GratitudeControl />}
|
|
</Control>
|
|
<TileLayer
|
|
maxZoom={19}
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
|
|
/>
|
|
<MarkerClusterGroup
|
|
ref={(r) => setClusterRef(r)}
|
|
showCoverageOnHover
|
|
chunkedLoading
|
|
maxClusterRadius={50}
|
|
removeOutsideVisibleBounds={false}
|
|
>
|
|
{Children.toArray(children).map((child) =>
|
|
isValidElement<{
|
|
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
|
|
itemFormPopup: ItemFormPopupProps | null
|
|
clusterRef: React.MutableRefObject<undefined>
|
|
}>(child)
|
|
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
|
|
: child,
|
|
)}
|
|
</MarkerClusterGroup>
|
|
{geo && (
|
|
<GeoJSON
|
|
data={geo}
|
|
onEachFeature={onEachFeature}
|
|
eventHandlers={{
|
|
click: (e) => {
|
|
if (selectNewItemPosition) {
|
|
e.layer.closePopup()
|
|
setMapClicked({ position: e.latlng, setItemFormPopup })
|
|
}
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
<MapEventListener />
|
|
</MapContainer>
|
|
<AddButton triggerAction={setSelectNewItemPosition} />
|
|
{selectNewItemPosition != null && (
|
|
<SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|