mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-04-06 01:25:33 +00:00
added reverse geocoder
This commit is contained in:
parent
8816a1f9fb
commit
9ec29f8366
@ -11,7 +11,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
|
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
|
||||||
import { MapPinIcon, ShareIcon } from '@heroicons/react/24/outline'
|
import { MapPinIcon, ShareIcon } from '@heroicons/react/24/solid'
|
||||||
import PencilIcon from '@heroicons/react/24/solid/PencilIcon'
|
import PencilIcon from '@heroicons/react/24/solid/PencilIcon'
|
||||||
import TrashIcon from '@heroicons/react/24/solid/TrashIcon'
|
import TrashIcon from '@heroicons/react/24/solid/TrashIcon'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
@ -32,6 +32,7 @@ import TargetDotSVG from '#assets/targetDot.svg'
|
|||||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||||
import { useGeoDistance } from '#components/Map/hooks/useGeoDistance'
|
import { useGeoDistance } from '#components/Map/hooks/useGeoDistance'
|
||||||
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
|
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
|
||||||
|
import { useReverseGeocode } from '#components/Map/hooks/useReverseGeocode'
|
||||||
import { useGetItemTags } from '#components/Map/hooks/useTags'
|
import { useGetItemTags } from '#components/Map/hooks/useTags'
|
||||||
import DialogModal from '#components/Templates/DialogModal'
|
import DialogModal from '#components/Templates/DialogModal'
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ export function HeaderView({
|
|||||||
big = false,
|
big = false,
|
||||||
truncateSubname = true,
|
truncateSubname = true,
|
||||||
hideSubname = false,
|
hideSubname = false,
|
||||||
showAddress = false,
|
showAddress = true,
|
||||||
}: {
|
}: {
|
||||||
item?: Item
|
item?: Item
|
||||||
api?: ItemsApi<any>
|
api?: ItemsApi<any>
|
||||||
@ -78,7 +79,10 @@ export function HeaderView({
|
|||||||
const title = item?.name ?? item?.layer?.item_default_name
|
const title = item?.name ?? item?.layer?.item_default_name
|
||||||
const subtitle = item?.subname
|
const subtitle = item?.subname
|
||||||
|
|
||||||
const [address] = useState<string>('')
|
const { address } = useReverseGeocode(
|
||||||
|
item?.position?.coordinates as [number, number] | undefined,
|
||||||
|
showAddress,
|
||||||
|
)
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
@ -181,8 +185,8 @@ export function HeaderView({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='tw:flex tw:flex-row'>
|
<div className='tw:flex tw:flex-row'>
|
||||||
<div className={'tw:grow'}>
|
<div className={'tw:grow tw:flex tw:flex-1 tw:min-w-0'}>
|
||||||
<div className='tw:flex tw:items-center'>
|
<div className='tw:flex tw:flex-1 tw:min-w-0 tw:items-center'>
|
||||||
{avatar && (
|
{avatar && (
|
||||||
<div className='tw:avatar'>
|
<div className='tw:avatar'>
|
||||||
<div
|
<div
|
||||||
@ -206,24 +210,27 @@ export function HeaderView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`${avatar ? 'tw:ml-3' : ''} tw:overflow-hidden `}>
|
<div className={`${avatar ? 'tw:ml-3' : ''} tw:overflow-hidden tw:flex-1 tw:min-w-0 `}>
|
||||||
<div
|
<div
|
||||||
className={`${big ? 'tw:xl:text-3xl tw:text-2xl' : 'tw:text-xl'} tw:font-bold tw:truncate`}
|
className={`${big ? 'tw:xl:text-3xl tw:text-2xl' : 'tw:text-xl'} tw:font-bold`}
|
||||||
title={title}
|
title={title}
|
||||||
data-cy='profile-title'
|
data-cy='profile-title'
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{showAddress && address && !hideSubname && (
|
{showAddress && address && (
|
||||||
<div className={`tw:text-xs tw:text-gray-500 ${truncateSubname && 'tw:truncate'}`}>
|
<div className='tw:text-sm tw:flex tw:items-center tw:text-gray-500 tw:w-full'>
|
||||||
{address}
|
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
|
||||||
|
<span title={address} className='tw:truncate'>
|
||||||
|
{address}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{subtitle && !hideSubname && (
|
{subtitle && !showAddress && (
|
||||||
<div
|
<div
|
||||||
className={`tw:text-xs tw:opacity-50 tw:inline-flex tw:items-center ${truncateSubname && 'tw:truncate'}`}
|
className={`tw:text-sm tw:opacity-50 tw:items-center ${truncateSubname && 'tw:truncate'}`}
|
||||||
>
|
>
|
||||||
<MapPinIcon className='tw:w-4 tw:mr-1' /> {subtitle}
|
{subtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -235,7 +242,7 @@ export function HeaderView({
|
|||||||
hasUserPermission(api?.collectionName!, 'update', item)) &&
|
hasUserPermission(api?.collectionName!, 'update', item)) &&
|
||||||
!hideMenu && (
|
!hideMenu && (
|
||||||
<div className='tw:dropdown tw:dropdown-bottom tw:dropdown-center'>
|
<div className='tw:dropdown tw:dropdown-bottom tw:dropdown-center'>
|
||||||
<label tabIndex={0} className='tw:btn tw:btn-sm tw:px-2'>
|
<label tabIndex={0} className='tw:btn tw:px-3'>
|
||||||
<EllipsisVerticalIcon className='tw:h-4 tw:w-4' />
|
<EllipsisVerticalIcon className='tw:h-4 tw:w-4' />
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
@ -316,28 +323,28 @@ export function HeaderView({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: `${item?.color ?? (item && (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : (item?.layer?.markerDefaultColor ?? '#000')))}`,
|
backgroundColor: `${item?.color ?? (item && (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : (item?.layer?.markerDefaultColor ?? '#000')))}`,
|
||||||
}}
|
}}
|
||||||
className='tw:btn tw:text-white tw:btn-sm tw:mr-2 '
|
className='tw:btn tw:text-white tw:mr-2 '
|
||||||
>
|
>
|
||||||
Follow
|
{item.layer?.itemType.cta_button_label ?? 'Follow'}
|
||||||
</button>
|
</button>
|
||||||
{item?.position?.coordinates ? (
|
{item?.position?.coordinates ? (
|
||||||
<a
|
<a
|
||||||
href={getNavigationUrl()}
|
href={getNavigationUrl()}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='tw:btn tw:btn-sm tw:mr-2 tw:px-2 tw:no-underline hover:tw:no-underline'
|
className='tw:btn tw:mr-2 tw:px-3 tw:no-underline hover:tw:no-underline'
|
||||||
style={{ color: 'inherit' }}
|
style={{ color: 'inherit' }}
|
||||||
title={`Navigate with ${isMobile ? 'default navigation app' : isIOS ? 'Apple Maps' : 'Google Maps'}`}
|
title={`Navigate with ${isMobile ? 'default navigation app' : isIOS ? 'Apple Maps' : 'Google Maps'}`}
|
||||||
>
|
>
|
||||||
<LuNavigation className='tw:h-4 tw:w-4' />
|
<LuNavigation className='tw:h-4 tw:w-4' />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className='tw:btn tw:btn-sm tw:mr-2 tw:px-2 tw:btn-disabled'>
|
<div className='tw:btn tw:mr-2 tw:px-3 tw:btn-disabled'>
|
||||||
<LuNavigation className='tw:h-4 tw:w-4' />
|
<LuNavigation className='tw:h-4 tw:w-4' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='tw:dropdown tw:dropdown-end'>
|
<div className='tw:dropdown tw:dropdown-end'>
|
||||||
<div tabIndex={0} role='button' className='tw:btn tw:btn-sm tw:px-2'>
|
<div tabIndex={0} role='button' className='tw:btn tw:px-3'>
|
||||||
<ShareIcon className='tw:w-4 tw:h-4' />
|
<ShareIcon className='tw:w-4 tw:h-4' />
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
|
|||||||
90
lib/src/Components/Map/hooks/useReverseGeocode.ts
Normal file
90
lib/src/Components/Map/hooks/useReverseGeocode.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface GeocodeResult {
|
||||||
|
street?: string
|
||||||
|
housenumber?: string
|
||||||
|
postcode?: string
|
||||||
|
city?: string
|
||||||
|
town?: string
|
||||||
|
village?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeocodeFeature {
|
||||||
|
properties: GeocodeResult
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeocodeResponse {
|
||||||
|
features?: GeocodeFeature[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReverseGeocode(coordinates?: [number, number] | null, enabled: boolean = true) {
|
||||||
|
const [address, setAddress] = useState<string>('')
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !coordinates) {
|
||||||
|
setAddress('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [longitude, latitude] = coordinates
|
||||||
|
if (!latitude || !longitude) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reverseGeocode = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://photon.komoot.io/reverse?lat=${latitude}&lon=${longitude}&lang=de&limit=1`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Geocoding request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as GeocodeResponse
|
||||||
|
|
||||||
|
if (data.features && data.features.length > 0) {
|
||||||
|
const props = data.features[0].properties
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
// Straße und Hausnummer zusammen
|
||||||
|
if (props.street) {
|
||||||
|
const streetPart = props.housenumber
|
||||||
|
? `${props.street} ${props.housenumber}`
|
||||||
|
: props.street
|
||||||
|
parts.push(streetPart)
|
||||||
|
} else if (props.housenumber) {
|
||||||
|
parts.push(props.housenumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PLZ und Ort zusammen
|
||||||
|
if (props.postcode || props.city || props.town || props.village) {
|
||||||
|
const locationPart = [
|
||||||
|
props.postcode,
|
||||||
|
props.city || props.town || props.village
|
||||||
|
].filter(Boolean).join(' ')
|
||||||
|
if (locationPart) parts.push(locationPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAddress(parts.join(', '))
|
||||||
|
} else {
|
||||||
|
setAddress('')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||||
|
setAddress('')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reverseGeocode()
|
||||||
|
}, [coordinates, enabled])
|
||||||
|
|
||||||
|
return { address, loading, error }
|
||||||
|
}
|
||||||
2
lib/src/types/ItemType.d.ts
vendored
2
lib/src/types/ItemType.d.ts
vendored
@ -21,4 +21,6 @@ export interface ItemType {
|
|||||||
botton_label?: string
|
botton_label?: string
|
||||||
text_input_label?: string
|
text_input_label?: string
|
||||||
show_header_view_in_form?: boolean
|
show_header_view_in_form?: boolean
|
||||||
|
cta_button_label?: string
|
||||||
|
show_address?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user