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-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/solid' import { MapPinIcon, ShareIcon, QrCodeIcon } 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'
import { LuNavigation } from 'react-icons/lu' import { LuNavigation } from 'react-icons/lu'
import SVG from 'react-inlinesvg' import SVG from 'react-inlinesvg'
import QRCode from 'react-qr-code'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@ -63,6 +64,7 @@ export function HeaderView({
showAddress?: boolean showAddress?: boolean
}) { }) {
const [modalOpen, setModalOpen] = useState<boolean>(false) const [modalOpen, setModalOpen] = useState<boolean>(false)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const hasUserPermission = useHasUserPermission() const hasUserPermission = useHasUserPermission()
const navigate = useNavigate() const navigate = useNavigate()
@ -80,6 +82,7 @@ export function HeaderView({
const { address } = useReverseGeocode( const { address } = useReverseGeocode(
item?.position?.coordinates as [number, number] | undefined, item?.position?.coordinates as [number, number] | undefined,
showAddress, showAddress,
'municipality',
) )
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
@ -130,10 +133,14 @@ export function HeaderView({
const shareUrl = window.location.href const shareUrl = window.location.href
const shareTitle = item?.name ?? 'Utopia Map Item' 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 = () => { const copyLink = () => {
navigator.clipboard navigator.clipboard
.writeText(shareUrl) .writeText(inviteLink)
.then(() => { .then(() => {
toast.success('Link copied to clipboard') toast.success('Link copied to clipboard')
return null return null
@ -221,6 +228,7 @@ export function HeaderView({
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' /> <MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
<span title={address} className='tw:truncate'> <span title={address} className='tw:truncate'>
{address} {address}
{distance && distance >= 1 && ` (${formatDistance(distance)})`}
</span> </span>
</div> </div>
)} )}
@ -303,18 +311,7 @@ export function HeaderView({
</div> </div>
{big && ( {big && (
<div className='tw:flex tw:row tw:mt-2 '> <div className='tw:flex tw:row tw:mt-2 '>
<div className='tw:w-16 tw:text-center tw:font-bold tw:text-primary '> <div className='tw:w-16 tw:text-center tw:font-bold tw:text-primary '></div>
{' '}
{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:grow'></div> <div className='tw:grow'></div>
<div className=''> <div className=''>
<button <button
@ -341,6 +338,13 @@ export function HeaderView({
<LuNavigation className='tw:h-4 tw:w-4' /> <LuNavigation className='tw:h-4 tw:w-4' />
</div> </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 className='tw:dropdown tw:dropdown-end'>
<div tabIndex={0} role='button' className='tw:btn tw:px-3'> <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' />
@ -432,6 +436,32 @@ export function HeaderView({
</div> </div>
</div> </div>
</DialogModal> </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 city?: string
town?: string town?: string
village?: string village?: string
district?: string
suburb?: string
neighbourhood?: string
state?: string
country?: string
} }
interface GeocodeFeature { interface GeocodeFeature {
@ -17,7 +22,11 @@ interface GeocodeResponse {
features?: GeocodeFeature[] 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 [address, setAddress] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null) 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) { if (data.features && data.features.length > 0) {
const props = data.features[0].properties const props = data.features[0].properties
const parts: string[] = [] const municipality = props.city || props.town || props.village
// Straße und Hausnummer zusammen let addressString = ''
if (props.street) {
const streetPart = props.housenumber switch (accuracy) {
? `${props.street} ${props.housenumber}` case 'municipality':
: props.street addressString = municipality || ''
parts.push(streetPart) break
} else if (props.housenumber) { case 'street':
parts.push(props.housenumber) 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 setAddress(addressString)
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 { } else {
setAddress('') setAddress('')
} }
@ -84,7 +98,7 @@ export function useReverseGeocode(coordinates?: [number, number] | null, enabled
} }
void reverseGeocode() void reverseGeocode()
}, [coordinates, enabled]) }, [coordinates, enabled, accuracy])
return { address, loading, error } return { address, loading, error }
} }

View File

@ -9,7 +9,7 @@ const isClickInsideRectangle = (e: MouseEvent, element: HTMLElement) => {
} }
interface Props { interface Props {
title: string title?: string
isOpened: boolean isOpened: boolean
onClose: () => void onClose: () => void
children: React.ReactNode children: React.ReactNode
@ -52,7 +52,9 @@ const DialogModal = ({
} }
> >
<div className='tw:card-body tw:p-2'> <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} {children}
{showCloseButton && ( {showCloseButton && (