mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-04-06 01:25:33 +00:00
added qr code dialog, optimized address
This commit is contained in:
parent
085be8cf0e
commit
5b958a26ff
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }
|
||||||
}
|
}
|
||||||
@ -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 && (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user