added qr code dialog, optimized address

This commit is contained in:
Anton Tranelis 2025-09-13 23:41:00 +02:00
parent 085be8cf0e
commit 5b958a26ff
6 changed files with 202 additions and 37 deletions

View File

@ -0,0 +1,31 @@
{
"collection": "types",
"field": "Header",
"type": "alias",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "Header",
"group": null,
"hidden": false,
"interface": "group-detail",
"note": null,
"options": {
"start": "closed"
},
"readonly": false,
"required": false,
"sort": 7,
"special": [
"alias",
"no-data",
"group"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
}
}

View File

@ -0,0 +1,43 @@
{
"collection": "types",
"field": "cta_button_label",
"type": "string",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "cta_button_label",
"group": "Header",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "cta_button_label",
"table": "types",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -0,0 +1,45 @@
{
"collection": "types",
"field": "show_address",
"type": "boolean",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "show_address",
"group": "Header",
"hidden": false,
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "show_address",
"table": "types",
"data_type": "boolean",
"default_value": true,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -11,12 +11,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
import { MapPinIcon, ShareIcon } from '@heroicons/react/24/solid'
import { MapPinIcon, ShareIcon, QrCodeIcon } from '@heroicons/react/24/solid'
import PencilIcon from '@heroicons/react/24/solid/PencilIcon'
import TrashIcon from '@heroicons/react/24/solid/TrashIcon'
import { useState } from 'react'
import { LuNavigation } from 'react-icons/lu'
import SVG from 'react-inlinesvg'
import QRCode from 'react-qr-code'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
@ -63,6 +64,7 @@ export function HeaderView({
showAddress?: boolean
}) {
const [modalOpen, setModalOpen] = useState<boolean>(false)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const hasUserPermission = useHasUserPermission()
const navigate = useNavigate()
@ -80,6 +82,7 @@ export function HeaderView({
const { address } = useReverseGeocode(
item?.position?.coordinates as [number, number] | undefined,
showAddress,
'municipality',
)
const params = new URLSearchParams(window.location.search)
@ -130,10 +133,14 @@ export function HeaderView({
const shareUrl = window.location.href
const shareTitle = item?.name ?? 'Utopia Map Item'
const inviteLink =
item?.secrets && item.secrets.length > 0
? `${window.location.origin}/invite/${item.secrets[0].secret}`
: shareUrl
const copyLink = () => {
navigator.clipboard
.writeText(shareUrl)
.writeText(inviteLink)
.then(() => {
toast.success('Link copied to clipboard')
return null
@ -221,6 +228,7 @@ export function HeaderView({
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
<span title={address} className='tw:truncate'>
{address}
{distance && distance >= 1 && ` (${formatDistance(distance)})`}
</span>
</div>
)}
@ -303,18 +311,7 @@ export function HeaderView({
</div>
{big && (
<div className='tw:flex tw:row tw:mt-2 '>
<div className='tw:w-16 tw:text-center tw:font-bold tw:text-primary '>
{' '}
{formatDistance(distance) && (
<span
style={{
color: `${item?.color ?? (item && (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : (item?.layer?.markerDefaultColor ?? '#000')))}`,
}}
>
{formatDistance(distance)}
</span>
)}
</div>
<div className='tw:w-16 tw:text-center tw:font-bold tw:text-primary '></div>
<div className='tw:grow'></div>
<div className=''>
<button
@ -341,6 +338,13 @@ export function HeaderView({
<LuNavigation className='tw:h-4 tw:w-4' />
</div>
)}
<button
onClick={() => setQrModalOpen(true)}
className='tw:btn tw:mr-2 tw:px-3'
title='QR-Code teilen'
>
<QrCodeIcon className='tw:h-4 tw:w-4' />
</button>
<div className='tw:dropdown tw:dropdown-end'>
<div tabIndex={0} role='button' className='tw:btn tw:px-3'>
<ShareIcon className='tw:w-4 tw:h-4' />
@ -432,6 +436,32 @@ export function HeaderView({
</div>
</div>
</DialogModal>
<DialogModal
isOpened={qrModalOpen}
showCloseButton={true}
onClose={() => setQrModalOpen(false)}
className='tw:w-[calc(100vw-2rem)] tw:max-w-96'
>
<div onClick={(e) => e.stopPropagation()} className='tw:text-center tw:p-4'>
<p className='tw:text-xl'>Share your profile with others to expand your network.</p>
<div className='tw:p-8 tw:my-8 tw:rounded-lg tw:inline-block tw:border-base-300 tw:border-2 '>
<QRCode value={inviteLink} size={192} />
</div>
<div className='tw:flex tw:items-center tw:gap-2 tw:w-full tw:border-base-300 tw:border-2 tw:rounded-lg tw:p-3'>
<span className='tw:text-sm tw:truncate tw:flex-1 tw:min-w-0'>{inviteLink}</span>
<button
onClick={copyLink}
className='tw:btn tw:btn-primary tw:btn-sm tw:flex-shrink-0'
title='Link kopieren'
>
<img src={ClipboardSVG} className='tw:w-4 tw:h-4' alt='Copy' />
</button>
</div>
</div>
</DialogModal>
</>
)
}

View File

@ -7,6 +7,11 @@ interface GeocodeResult {
city?: string
town?: string
village?: string
district?: string
suburb?: string
neighbourhood?: string
state?: string
country?: string
}
interface GeocodeFeature {
@ -17,7 +22,11 @@ interface GeocodeResponse {
features?: GeocodeFeature[]
}
export function useReverseGeocode(coordinates?: [number, number] | null, enabled: boolean = true) {
export function useReverseGeocode(
coordinates?: [number, number] | null,
enabled: boolean = true,
accuracy: 'municipality' | 'street' | 'house_number' = 'municipality'
) {
const [address, setAddress] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
@ -50,28 +59,33 @@ export function useReverseGeocode(coordinates?: [number, number] | null, enabled
if (data.features && data.features.length > 0) {
const props = data.features[0].properties
const parts: string[] = []
const municipality = props.city || props.town || props.village
// 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)
let addressString = ''
switch (accuracy) {
case 'municipality':
addressString = municipality || ''
break
case 'street':
if (props.street && municipality) {
addressString = `${props.street}, ${municipality}`
} else {
addressString = municipality || ''
}
break
case 'house_number':
if (props.street && props.housenumber && municipality) {
addressString = `${props.street} ${props.housenumber}, ${municipality}`
} else if (props.street && municipality) {
addressString = `${props.street}, ${municipality}`
} else {
addressString = municipality || ''
}
break
}
// 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(', '))
setAddress(addressString)
} else {
setAddress('')
}
@ -84,7 +98,7 @@ export function useReverseGeocode(coordinates?: [number, number] | null, enabled
}
void reverseGeocode()
}, [coordinates, enabled])
}, [coordinates, enabled, accuracy])
return { address, loading, error }
}

View File

@ -9,7 +9,7 @@ const isClickInsideRectangle = (e: MouseEvent, element: HTMLElement) => {
}
interface Props {
title: string
title?: string
isOpened: boolean
onClose: () => void
children: React.ReactNode
@ -52,7 +52,9 @@ const DialogModal = ({
}
>
<div className='tw:card-body tw:p-2'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>{title}</h2>
{title && (
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>{title}</h2>
)}
{children}
{showCloseButton && (