mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-03-01 12:44:17 +00:00
rebase
This commit is contained in:
parent
05209bb62a
commit
3233efb6d3
@ -1,475 +1 @@
|
||||
/* eslint-disable security/detect-object-injection */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* 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, 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 { FaPlus } from 'react-icons/fa6'
|
||||
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'
|
||||
|
||||
import ChevronSVG from '#assets/chevron.svg'
|
||||
import ClipboardSVG from '#assets/share/clipboard.svg'
|
||||
import FacebookSVG from '#assets/share/facebook.svg'
|
||||
import LinkedinSVG from '#assets/share/linkedin.svg'
|
||||
import TelegramSVG from '#assets/share/telegram.svg'
|
||||
import TwitterSVG from '#assets/share/twitter.svg'
|
||||
import WhatsappSVG from '#assets/share/whatsapp.svg'
|
||||
import XingSVG from '#assets/share/xing.svg'
|
||||
import TargetDotSVG from '#assets/targetDot.svg'
|
||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||
import { useGeoDistance } from '#components/Map/hooks/useGeoDistance'
|
||||
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
|
||||
import { useReverseGeocode } from '#components/Map/hooks/useReverseGeocode'
|
||||
import { useGetItemTags } from '#components/Map/hooks/useTags'
|
||||
import DialogModal from '#components/Templates/DialogModal'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { ItemsApi } from '#types/ItemsApi'
|
||||
|
||||
export function HeaderView({
|
||||
item,
|
||||
api,
|
||||
editCallback,
|
||||
deleteCallback,
|
||||
setPositionCallback,
|
||||
loading,
|
||||
hideMenu = false,
|
||||
big = false,
|
||||
truncateSubname = true,
|
||||
showAddress = true,
|
||||
}: {
|
||||
item?: Item
|
||||
api?: ItemsApi<any>
|
||||
editCallback?: any
|
||||
deleteCallback?: any
|
||||
setPositionCallback?: any
|
||||
loading?: boolean
|
||||
hideMenu?: boolean
|
||||
big?: boolean
|
||||
truncateSubname?: boolean
|
||||
showAddress?: boolean
|
||||
}) {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false)
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
|
||||
const hasUserPermission = useHasUserPermission()
|
||||
const navigate = useNavigate()
|
||||
const appState = useAppState()
|
||||
const getItemTags = useGetItemTags()
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const { distance } = useGeoDistance(item?.position ?? undefined)
|
||||
|
||||
const avatar =
|
||||
(item?.image && appState.assetsApi.url + item.image + '?width=160&heigth=160') ??
|
||||
item?.image_external
|
||||
const title = item?.name ?? item?.layer?.item_default_name
|
||||
const subtitle = item?.subname
|
||||
|
||||
const { address } = useReverseGeocode(
|
||||
item?.position?.coordinates as [number, number] | undefined,
|
||||
showAddress,
|
||||
'municipality',
|
||||
)
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const formatDistance = (dist: number | null): string | null => {
|
||||
if (!dist) return null
|
||||
return dist < 10 ? `${dist.toFixed(1)} km` : `${Math.round(dist)} km`
|
||||
}
|
||||
|
||||
const platformConfigs = {
|
||||
facebook: {
|
||||
shareUrl: 'https://www.facebook.com/sharer/sharer.php?u={url}',
|
||||
icon: <img src={FacebookSVG} alt='Facebook' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Facebook',
|
||||
bgColor: '#3b5998',
|
||||
},
|
||||
twitter: {
|
||||
shareUrl: 'https://twitter.com/intent/tweet?text={title}:%20{url}',
|
||||
icon: <img src={TwitterSVG} alt='Twitter' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Twitter',
|
||||
bgColor: '#55acee',
|
||||
},
|
||||
linkedin: {
|
||||
shareUrl: 'http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}',
|
||||
icon: <img src={LinkedinSVG} alt='Linkedin' className='tw:w-4 tw:h-4' />,
|
||||
label: 'LinkedIn',
|
||||
bgColor: '#4875b4',
|
||||
},
|
||||
whatsapp: {
|
||||
shareUrl: 'https://api.whatsapp.com/send?text={title}%20{url}',
|
||||
icon: <img src={WhatsappSVG} alt='Whatsapp' className='tw:w-4 tw:h-4' />,
|
||||
label: 'WhatsApp',
|
||||
bgColor: '#25D366',
|
||||
},
|
||||
telegram: {
|
||||
shareUrl: 'https://t.me/share/url?url={url}&text={title}',
|
||||
icon: <img src={TelegramSVG} alt='Telegram' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Telegram',
|
||||
bgColor: '#0088cc',
|
||||
},
|
||||
xing: {
|
||||
shareUrl: 'https://www.xing-share.com/app/user?op=share;sc_p=xing-share;url={url}',
|
||||
icon: <img src={XingSVG} alt='Xing' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Xing',
|
||||
bgColor: '#026466',
|
||||
},
|
||||
}
|
||||
|
||||
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(inviteLink)
|
||||
.then(() => {
|
||||
toast.success('Link copied to clipboard')
|
||||
return null
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Error copying link')
|
||||
})
|
||||
}
|
||||
|
||||
const getShareUrl = (platform: keyof typeof platformConfigs) => {
|
||||
const config = platformConfigs[platform]
|
||||
return config.shareUrl
|
||||
.replace('{url}', encodeURIComponent(shareUrl))
|
||||
.replace('{title}', encodeURIComponent(shareTitle))
|
||||
}
|
||||
|
||||
const coordinates = item?.position?.coordinates
|
||||
const latitude = coordinates?.[1]
|
||||
const longitude = coordinates?.[0]
|
||||
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent,
|
||||
)
|
||||
|
||||
const getNavigationUrl = () => {
|
||||
if (!coordinates || !latitude || !longitude) return ''
|
||||
|
||||
// Try geo: link first (works on most mobile devices)
|
||||
if (isMobile) {
|
||||
return `geo:${latitude},${longitude}`
|
||||
}
|
||||
|
||||
// Fallback to web-based maps
|
||||
if (isIOS) {
|
||||
return `https://maps.apple.com/?daddr=${latitude},${longitude}`
|
||||
} else {
|
||||
return `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`
|
||||
}
|
||||
}
|
||||
|
||||
const openDeleteModal = async (event: React.MouseEvent<HTMLElement>) => {
|
||||
setModalOpen(true)
|
||||
event.stopPropagation()
|
||||
}
|
||||
if (!item) return null
|
||||
return (
|
||||
<>
|
||||
<div className='tw:flex tw:flex-row'>
|
||||
<div className={'tw:grow tw:flex tw:flex-1 tw:min-w-0'}>
|
||||
<div className='tw:flex tw:flex-1 tw:min-w-0 tw:items-center'>
|
||||
{avatar && (
|
||||
<div className='tw:avatar'>
|
||||
<div
|
||||
className={`${
|
||||
big ? 'tw:w-16' : 'tw:w-10'
|
||||
} tw:inline tw:items-center tw:justify-center tw:overflow-visible`}
|
||||
>
|
||||
<img
|
||||
className={
|
||||
'tw:w-full tw:h-full tw:object-cover tw:rounded-full tw:border-white'
|
||||
}
|
||||
src={avatar}
|
||||
alt={item.name + ' logo'}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageLoaded(false)}
|
||||
style={{ display: imageLoaded ? 'block' : 'none' }}
|
||||
/>
|
||||
{!imageLoaded && (
|
||||
<div className='tw:w-full tw:h-full tw:bg-gray-200 tw:rounded-full' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${avatar ? 'tw:ml-3' : ''} tw:overflow-hidden tw:flex-1 tw:min-w-0 `}>
|
||||
<div
|
||||
className={`${big ? 'tw:xl:text-3xl tw:text-2xl' : 'tw:text-xl'} tw:font-bold`}
|
||||
title={title}
|
||||
data-cy='profile-title'
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{showAddress && address && (
|
||||
<div className='tw:text-sm tw:flex tw:items-center tw:text-gray-500 tw:w-full'>
|
||||
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
|
||||
<span title={address} className='tw:truncate'>
|
||||
{address}
|
||||
{distance && distance >= 0.1 && ` (${formatDistance(distance)})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{subtitle && !showAddress && (
|
||||
<div
|
||||
className={`tw:text-sm tw:opacity-50 tw:items-center ${truncateSubname && 'tw:truncate'}`}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className={`${big ? 'tw:mt-5' : 'tw:mt-1'}`}>
|
||||
{(api?.deleteItem ?? item.layer?.api?.updateItem) &&
|
||||
(hasUserPermission(api?.collectionName!, 'delete', item) ??
|
||||
hasUserPermission(api?.collectionName!, 'update', item)) &&
|
||||
!hideMenu && (
|
||||
<div className='tw:dropdown tw:dropdown-bottom tw:dropdown-center'>
|
||||
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:px-2.5'>
|
||||
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='tw:dropdown-content tw:menu tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:z-1000'
|
||||
>
|
||||
{api?.updateItem &&
|
||||
hasUserPermission(api.collectionName!, 'update', item) &&
|
||||
editCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-base-content! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Edit'
|
||||
onClick={(e) =>
|
||||
item.layer?.customEditLink
|
||||
? navigate(
|
||||
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${item.id}${params && '?' + params}` : ''} `,
|
||||
)
|
||||
: editCallback(e)
|
||||
}
|
||||
>
|
||||
<PencilIcon className='tw:h-5 tw:w-5' />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{api?.updateItem &&
|
||||
hasUserPermission(api.collectionName!, 'update', item) &&
|
||||
setPositionCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-base-content! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Set position'
|
||||
onClick={setPositionCallback}
|
||||
>
|
||||
<SVG src={TargetDotSVG} className='tw:w-5 tw:h-5' />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{api?.deleteItem &&
|
||||
hasUserPermission(api.collectionName!, 'delete', item) &&
|
||||
deleteCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-error! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Delete'
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='tw:loading tw:loading-spinner tw:loading-sm'></span>
|
||||
) : (
|
||||
<TrashIcon className='tw:h-5 tw:w-5' />
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{big && (
|
||||
<div className='tw:flex tw:row tw:mt-2 '>
|
||||
<div className='tw:grow'></div>
|
||||
<div className=''>
|
||||
<button
|
||||
style={{
|
||||
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:mr-2 tw:tooltip tw:tooltip-top '
|
||||
data-tip={item.layer?.itemType.cta_button_label}
|
||||
>
|
||||
<FaPlus className='tw:w-5' /> Connect
|
||||
</button>
|
||||
{item?.position?.coordinates ? (
|
||||
<a
|
||||
href={getNavigationUrl()}
|
||||
target='_blank'
|
||||
data-tip='Navigate'
|
||||
rel='noopener noreferrer'
|
||||
className='tw:btn tw:mr-2 tw:px-3 tw:tooltip tw:tooltip-top'
|
||||
style={{ color: 'inherit' }}
|
||||
title={`Navigate with ${isMobile ? 'default navigation app' : isIOS ? 'Apple Maps' : 'Google Maps'}`}
|
||||
>
|
||||
<LuNavigation className='tw:h-4 tw:w-4' />
|
||||
</a>
|
||||
) : (
|
||||
<div className='tw:btn tw:mr-2 tw:px-3 tw:btn-disabled'>
|
||||
<LuNavigation className='tw:h-4 tw:w-4' />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setQrModalOpen(true)}
|
||||
className='tw:btn tw:mr-2 tw:px-3 tw:tooltip tw:tooltip-top'
|
||||
title='QR-Code'
|
||||
data-tip='QR Code'
|
||||
>
|
||||
<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 tw:tooltip tw:tooltip-top'
|
||||
data-tip='Share'
|
||||
>
|
||||
<ShareIcon className='tw:w-4 tw:h-4' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='tw:dropdown-content tw:menu tw:bg-base-100 tw:rounded-box tw:z-[1] tw:p-2 tw:shadow-sm'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
onClick={copyLink}
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center'
|
||||
style={{ backgroundColor: '#888' }}
|
||||
>
|
||||
<img src={ClipboardSVG} className='tw:w-3 tw:h-3' alt='Copy' />
|
||||
</div>
|
||||
Copy Link
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:text-white'
|
||||
style={{ backgroundColor: '#444' }}
|
||||
>
|
||||
<img src={ChevronSVG} className='tw:w-3 tw:h-3' alt='Copy' />
|
||||
</div>
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
{Object.entries(platformConfigs).map(([platform, config]) => (
|
||||
<li key={platform}>
|
||||
<a
|
||||
href={getShareUrl(platform as keyof typeof platformConfigs)}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center'
|
||||
style={{ backgroundColor: config.bgColor }}
|
||||
>
|
||||
{config.icon}
|
||||
</div>
|
||||
{config.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogModal
|
||||
isOpened={modalOpen}
|
||||
title='Are you sure?'
|
||||
showCloseButton={false}
|
||||
onClose={() => setModalOpen(false)}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<span>
|
||||
Do you want to delete <b>{item.name}</b>?
|
||||
</span>
|
||||
<div className='tw:grid'>
|
||||
<div className='tw:flex tw:justify-between'>
|
||||
<label
|
||||
className='tw:btn tw:mt-4 tw:btn-error'
|
||||
onClick={(e) => {
|
||||
deleteCallback(e)
|
||||
setModalOpen(false)
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</label>
|
||||
<label className='tw:btn tw:mt-4' onClick={() => setModalOpen(false)}>
|
||||
No
|
||||
</label>
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export { HeaderView } from './HeaderView/index'
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
import { QrCodeIcon, ShareIcon } from '@heroicons/react/24/solid'
|
||||
import { LuNavigation } from 'react-icons/lu'
|
||||
|
||||
import ChevronSVG from '#assets/chevron.svg'
|
||||
import ClipboardSVG from '#assets/share/clipboard.svg'
|
||||
import FacebookSVG from '#assets/share/facebook.svg'
|
||||
import LinkedinSVG from '#assets/share/linkedin.svg'
|
||||
import TelegramSVG from '#assets/share/telegram.svg'
|
||||
import TwitterSVG from '#assets/share/twitter.svg'
|
||||
import WhatsappSVG from '#assets/share/whatsapp.svg'
|
||||
import XingSVG from '#assets/share/xing.svg'
|
||||
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
||||
|
||||
import { useNavigationUrl, useShareLogic } from './hooks'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { PlatformConfig, SharePlatformConfigs } from './types'
|
||||
|
||||
interface ActionButtonsProps {
|
||||
item: Item
|
||||
onQrModalOpen: () => void
|
||||
}
|
||||
|
||||
export function ActionButtons({ item, onQrModalOpen }: ActionButtonsProps) {
|
||||
const myProfile = useMyProfile()
|
||||
const { getNavigationUrl, isMobile, isIOS } = useNavigationUrl(
|
||||
item.position?.coordinates as [number, number] | undefined,
|
||||
)
|
||||
const { shareUrl, shareTitle, copyLink, getShareUrl } = useShareLogic(item)
|
||||
|
||||
const platformConfigs: SharePlatformConfigs = {
|
||||
facebook: {
|
||||
shareUrl: 'https://www.facebook.com/sharer/sharer.php?u={url}',
|
||||
icon: <img src={FacebookSVG} alt='Facebook' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Facebook',
|
||||
bgColor: '#3b5998',
|
||||
},
|
||||
twitter: {
|
||||
shareUrl: 'https://twitter.com/intent/tweet?text={title}:%20{url}',
|
||||
icon: <img src={TwitterSVG} alt='Twitter' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Twitter',
|
||||
bgColor: '#55acee',
|
||||
},
|
||||
linkedin: {
|
||||
shareUrl: 'http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}',
|
||||
icon: <img src={LinkedinSVG} alt='Linkedin' className='tw:w-4 tw:h-4' />,
|
||||
label: 'LinkedIn',
|
||||
bgColor: '#4875b4',
|
||||
},
|
||||
whatsapp: {
|
||||
shareUrl: 'https://api.whatsapp.com/send?text={title}%20{url}',
|
||||
icon: <img src={WhatsappSVG} alt='Whatsapp' className='tw:w-4 tw:h-4' />,
|
||||
label: 'WhatsApp',
|
||||
bgColor: '#25D366',
|
||||
},
|
||||
telegram: {
|
||||
shareUrl: 'https://t.me/share/url?url={url}&text={title}',
|
||||
icon: <img src={TelegramSVG} alt='Telegram' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Telegram',
|
||||
bgColor: '#0088cc',
|
||||
},
|
||||
xing: {
|
||||
shareUrl: 'https://www.xing-share.com/app/user?op=share;sc_p=xing-share;url={url}',
|
||||
icon: <img src={XingSVG} alt='Xing' className='tw:w-4 tw:h-4' />,
|
||||
label: 'Xing',
|
||||
bgColor: '#026466',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{item.position?.coordinates && myProfile.myProfile?.id !== item.id && (
|
||||
<a
|
||||
href={getNavigationUrl()}
|
||||
target='_blank'
|
||||
data-tip='Navigate'
|
||||
rel='noopener noreferrer'
|
||||
className='tw:btn tw:mr-2 tw:px-3 tw:tooltip tw:tooltip-top'
|
||||
style={{ color: 'inherit' }}
|
||||
title={`Navigate with ${isMobile ? 'default navigation app' : isIOS ? 'Apple Maps' : 'Google Maps'}`}
|
||||
>
|
||||
<LuNavigation className='tw:h-4 tw:w-4' />
|
||||
</a>
|
||||
)}
|
||||
{myProfile.myProfile?.id === item.id && (
|
||||
<button
|
||||
onClick={onQrModalOpen}
|
||||
className='tw:btn tw:mr-2 tw:px-3 tw:tooltip tw:tooltip-top'
|
||||
title='QR-Code'
|
||||
data-tip='QR Code'
|
||||
>
|
||||
<QrCodeIcon className='tw:h-4 tw:w-4' />
|
||||
</button>
|
||||
)}
|
||||
{myProfile.myProfile?.id !== item.id && (
|
||||
<div className='tw:dropdown tw:dropdown-end'>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className='tw:btn tw:px-3 tw:tooltip tw:tooltip-top'
|
||||
data-tip='Share'
|
||||
>
|
||||
<ShareIcon className='tw:w-4 tw:h-4' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='tw:dropdown-content tw:menu tw:bg-base-100 tw:rounded-box tw:z-[1] tw:p-2 tw:shadow-sm'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
onClick={copyLink}
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center'
|
||||
style={{ backgroundColor: '#888' }}
|
||||
>
|
||||
<img src={ClipboardSVG} className='tw:w-3 tw:h-3' alt='Copy' />
|
||||
</div>
|
||||
Copy Link
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:text-white'
|
||||
style={{ backgroundColor: '#444' }}
|
||||
>
|
||||
<img src={ChevronSVG} className='tw:w-3 tw:h-3' alt='Copy' />
|
||||
</div>
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
{Object.entries(platformConfigs).map(([platform, config]) => (
|
||||
<li key={platform}>
|
||||
<a
|
||||
href={getShareUrl(platform as keyof SharePlatformConfigs, platformConfigs)}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='tw:flex tw:items-center tw:gap-3'
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<div
|
||||
className='tw:w-6 tw:h-6 tw:rounded-full tw:flex tw:items-center tw:justify-center'
|
||||
style={{ backgroundColor: (config as PlatformConfig).bgColor }}
|
||||
>
|
||||
{(config as PlatformConfig).icon}
|
||||
</div>
|
||||
{(config as PlatformConfig).label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { FaPlus } from 'react-icons/fa6'
|
||||
|
||||
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
||||
import { useGetItemTags } from '#components/Map/hooks/useTags'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
item: Item
|
||||
}
|
||||
|
||||
export function ConnectionStatus({ item }: ConnectionStatusProps) {
|
||||
const myProfile = useMyProfile()
|
||||
const getItemTags = useGetItemTags()
|
||||
|
||||
if (myProfile.myProfile?.id === item.id) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isConnected = item.relations?.some(
|
||||
(r) =>
|
||||
r.type === item.layer?.itemType.cta_relation &&
|
||||
r.related_items_id === myProfile.myProfile?.id,
|
||||
)
|
||||
|
||||
if (isConnected) {
|
||||
return <p className='tw:flex tw:items-center tw:mr-2'>✅ Connected</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: `${item.color ?? (getItemTags(item)[0]?.color ? getItemTags(item)[0].color : (item.layer?.markerDefaultColor ?? '#000'))}`,
|
||||
}}
|
||||
className='tw:btn tw:text-white tw:mr-2 tw:tooltip tw:tooltip-top '
|
||||
data-tip={'Connect'}
|
||||
>
|
||||
<FaPlus className='tw:w-5' /> Connect
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import DialogModal from '#components/Templates/DialogModal'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface DeleteModalProps {
|
||||
item: Item
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
export function DeleteModal({ item, isOpen, onClose, onConfirm }: DeleteModalProps) {
|
||||
const handleConfirm = (e: React.MouseEvent) => {
|
||||
onConfirm(e)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogModal isOpened={isOpen} title='Are you sure?' showCloseButton={false} onClose={onClose}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<span>
|
||||
Do you want to delete <b>{item.name}</b>?
|
||||
</span>
|
||||
<div className='tw:grid'>
|
||||
<div className='tw:flex tw:justify-between'>
|
||||
<label className='tw:btn tw:mt-4 tw:btn-error' onClick={handleConfirm}>
|
||||
Yes
|
||||
</label>
|
||||
<label className='tw:btn tw:mt-4' onClick={onClose}>
|
||||
No
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogModal>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
|
||||
import PencilIcon from '@heroicons/react/24/solid/PencilIcon'
|
||||
import TrashIcon from '@heroicons/react/24/solid/TrashIcon'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import TargetDotSVG from '#assets/targetDot.svg'
|
||||
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { ItemsApi } from '#types/ItemsApi'
|
||||
|
||||
interface EditMenuProps {
|
||||
item: Item
|
||||
api?: ItemsApi<unknown>
|
||||
editCallback?: (e: React.MouseEvent) => void
|
||||
deleteCallback?: (e: React.MouseEvent) => void
|
||||
setPositionCallback?: () => void
|
||||
loading?: boolean
|
||||
hideMenu?: boolean
|
||||
big?: boolean
|
||||
onDeleteModalOpen: () => void
|
||||
}
|
||||
|
||||
export function EditMenu({
|
||||
item,
|
||||
api,
|
||||
editCallback,
|
||||
deleteCallback,
|
||||
setPositionCallback,
|
||||
loading = false,
|
||||
hideMenu = false,
|
||||
big = false,
|
||||
onDeleteModalOpen,
|
||||
}: EditMenuProps) {
|
||||
const hasUserPermission = useHasUserPermission()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const handleDeleteClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
onDeleteModalOpen()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
if (hideMenu) return null
|
||||
|
||||
const hasDeletePermission =
|
||||
api?.deleteItem && api.collectionName && hasUserPermission(api.collectionName, 'delete', item)
|
||||
const hasUpdatePermission =
|
||||
api?.updateItem && api.collectionName && hasUserPermission(api.collectionName, 'update', item)
|
||||
|
||||
if (!hasDeletePermission && !hasUpdatePermission) return null
|
||||
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()} className={`${big ? 'tw:mt-5' : 'tw:mt-1'}`}>
|
||||
<div className='tw:dropdown tw:dropdown-bottom tw:dropdown-center'>
|
||||
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:px-2.5'>
|
||||
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='tw:dropdown-content tw:menu tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:z-1000'
|
||||
>
|
||||
{hasUpdatePermission && editCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-base-content! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Edit'
|
||||
onClick={(e) =>
|
||||
item.layer?.customEditLink
|
||||
? navigate(
|
||||
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${item.id}${params.toString() ? '?' + params.toString() : ''}` : ''} `,
|
||||
)
|
||||
: editCallback(e)
|
||||
}
|
||||
>
|
||||
<PencilIcon className='tw:h-5 tw:w-5' />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{hasUpdatePermission && setPositionCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-base-content! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Set position'
|
||||
onClick={setPositionCallback}
|
||||
>
|
||||
<SVG src={TargetDotSVG} className='tw:w-5 tw:h-5' />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{hasDeletePermission && deleteCallback && (
|
||||
<li>
|
||||
<a
|
||||
className='tw:text-error! tw:tooltip tw:tooltip-top tw:cursor-pointer'
|
||||
data-tip='Delete'
|
||||
onClick={handleDeleteClick}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='tw:loading tw:loading-spinner tw:loading-sm'></span>
|
||||
) : (
|
||||
<TrashIcon className='tw:h-5 tw:w-5' />
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface ItemAvatarProps {
|
||||
item: Item
|
||||
big?: boolean
|
||||
}
|
||||
|
||||
export function ItemAvatar({ item, big = false }: ItemAvatarProps) {
|
||||
const appState = useAppState()
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
|
||||
const avatar =
|
||||
(item.image && appState.assetsApi.url + item.image + '?width=160&heigth=160') ??
|
||||
item.image_external
|
||||
|
||||
if (!avatar) return null
|
||||
|
||||
return (
|
||||
<div className='tw:avatar'>
|
||||
<div
|
||||
className={`${
|
||||
big ? 'tw:w-16' : 'tw:w-10'
|
||||
} tw:inline tw:items-center tw:justify-center tw:overflow-visible`}
|
||||
>
|
||||
<img
|
||||
className='tw:w-full tw:h-full tw:object-cover tw:rounded-full tw:border-white'
|
||||
src={avatar}
|
||||
alt={item.name + ' logo'}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageLoaded(false)}
|
||||
style={{ display: imageLoaded ? 'block' : 'none' }}
|
||||
/>
|
||||
{!imageLoaded && <div className='tw:w-full tw:h-full tw:bg-gray-200 tw:rounded-full' />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { MapPinIcon } from '@heroicons/react/24/solid'
|
||||
|
||||
import { useGeoDistance } from '#components/Map/hooks/useGeoDistance'
|
||||
import { useReverseGeocode } from '#components/Map/hooks/useReverseGeocode'
|
||||
|
||||
import { useFormatDistance } from './hooks'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface ItemTitleProps {
|
||||
item: Item
|
||||
big?: boolean
|
||||
truncateSubname?: boolean
|
||||
showAddress?: boolean
|
||||
hasAvatar?: boolean
|
||||
}
|
||||
|
||||
export function ItemTitle({
|
||||
item,
|
||||
big = false,
|
||||
truncateSubname = true,
|
||||
showAddress = true,
|
||||
hasAvatar = false,
|
||||
}: ItemTitleProps) {
|
||||
const { distance } = useGeoDistance(item.position ?? undefined)
|
||||
const { formatDistance } = useFormatDistance()
|
||||
|
||||
const { address } = useReverseGeocode(
|
||||
item.position?.coordinates as [number, number] | undefined,
|
||||
showAddress,
|
||||
'municipality',
|
||||
)
|
||||
|
||||
const title = item.name ?? item.layer?.item_default_name
|
||||
const subtitle = item.subname
|
||||
|
||||
return (
|
||||
<div className={`${hasAvatar ? 'tw:ml-3' : ''} tw:overflow-hidden tw:flex-1 tw:min-w-0 `}>
|
||||
<div
|
||||
className={`${big ? 'tw:xl:text-3xl tw:text-2xl' : 'tw:text-xl'} tw:font-bold`}
|
||||
title={title}
|
||||
data-cy='profile-title'
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{showAddress && address && (
|
||||
<div className='tw:text-sm tw:flex tw:items-center tw:text-gray-500 tw:w-full'>
|
||||
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
|
||||
<span title={address} className='tw:truncate'>
|
||||
{address}
|
||||
{distance && distance >= 0.1 && ` (${formatDistance(distance) ?? ''})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{subtitle && !showAddress && (
|
||||
<div
|
||||
className={`tw:text-sm tw:opacity-50 tw:items-center ${truncateSubname ? 'tw:truncate' : ''}`}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import QRCode from 'react-qr-code'
|
||||
|
||||
import ClipboardSVG from '#assets/share/clipboard.svg'
|
||||
import DialogModal from '#components/Templates/DialogModal'
|
||||
|
||||
import { useShareLogic } from './hooks'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface QRModalProps {
|
||||
item: Item
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function QRModal({ item, isOpen, onClose }: QRModalProps) {
|
||||
const { inviteLink, copyLink } = useShareLogic(item)
|
||||
|
||||
return (
|
||||
<DialogModal
|
||||
isOpened={isOpen}
|
||||
showCloseButton={true}
|
||||
onClose={onClose}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { SharePlatformConfigs } from './types'
|
||||
|
||||
export const useNavigationUrl = (coordinates?: [number, number]) => {
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent,
|
||||
)
|
||||
|
||||
const getNavigationUrl = () => {
|
||||
if (!coordinates) return ''
|
||||
|
||||
const [longitude, latitude] = coordinates
|
||||
|
||||
if (isMobile) {
|
||||
return `geo:${latitude},${longitude}`
|
||||
}
|
||||
|
||||
if (isIOS) {
|
||||
return `https://maps.apple.com/?daddr=${latitude},${longitude}`
|
||||
} else {
|
||||
return `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getNavigationUrl,
|
||||
isMobile,
|
||||
isIOS,
|
||||
}
|
||||
}
|
||||
|
||||
export const useShareLogic = (item?: Item) => {
|
||||
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(inviteLink)
|
||||
.then(() => {
|
||||
toast.success('Link copied to clipboard')
|
||||
return null
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Error copying link')
|
||||
})
|
||||
}
|
||||
|
||||
const getShareUrl = (
|
||||
platform: keyof SharePlatformConfigs,
|
||||
platformConfigs: SharePlatformConfigs,
|
||||
) => {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const config = platformConfigs[platform]
|
||||
return config.shareUrl
|
||||
.replace('{url}', encodeURIComponent(shareUrl))
|
||||
.replace('{title}', encodeURIComponent(shareTitle))
|
||||
}
|
||||
|
||||
return {
|
||||
shareUrl,
|
||||
shareTitle,
|
||||
inviteLink,
|
||||
copyLink,
|
||||
getShareUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export const useFormatDistance = () => {
|
||||
const formatDistance = (dist: number | null): string | null => {
|
||||
if (!dist) return null
|
||||
return dist < 10 ? `${dist.toFixed(1)} km` : `${Math.round(dist)} km`
|
||||
}
|
||||
|
||||
return { formatDistance }
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ActionButtons } from './ActionButtons'
|
||||
import { ConnectionStatus } from './ConnectionStatus'
|
||||
import { DeleteModal } from './DeleteModal'
|
||||
import { EditMenu } from './EditMenu'
|
||||
import { ItemAvatar } from './ItemAvatar'
|
||||
import { ItemTitle } from './ItemTitle'
|
||||
import { QRModal } from './QRModal'
|
||||
|
||||
import type { HeaderViewProps } from './types'
|
||||
|
||||
export function HeaderView({
|
||||
item,
|
||||
api,
|
||||
editCallback,
|
||||
deleteCallback,
|
||||
setPositionCallback,
|
||||
loading,
|
||||
hideMenu = false,
|
||||
big = false,
|
||||
truncateSubname = true,
|
||||
showAddress = true,
|
||||
}: HeaderViewProps) {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false)
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
|
||||
if (!item) return null
|
||||
|
||||
const hasAvatar = !!(item.image ?? item.image_external)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='tw:flex tw:flex-row'>
|
||||
<div className={'tw:grow tw:flex tw:flex-1 tw:min-w-0'}>
|
||||
<div className='tw:flex tw:flex-1 tw:min-w-0 tw:items-center'>
|
||||
{hasAvatar && <ItemAvatar item={item} big={big} />}
|
||||
<ItemTitle
|
||||
item={item}
|
||||
big={big}
|
||||
truncateSubname={truncateSubname}
|
||||
showAddress={showAddress}
|
||||
hasAvatar={hasAvatar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EditMenu
|
||||
item={item}
|
||||
api={api}
|
||||
editCallback={editCallback}
|
||||
deleteCallback={deleteCallback}
|
||||
setPositionCallback={setPositionCallback}
|
||||
loading={loading}
|
||||
hideMenu={hideMenu}
|
||||
big={big}
|
||||
onDeleteModalOpen={() => setModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{big && (
|
||||
<div className='tw:flex tw:row tw:mt-2 '>
|
||||
<div className='tw:grow'></div>
|
||||
<div className='tw:flex'>
|
||||
<ConnectionStatus item={item} />
|
||||
<ActionButtons item={item} onQrModalOpen={() => setQrModalOpen(true)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DeleteModal
|
||||
item={item}
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onConfirm={deleteCallback ?? (() => undefined)}
|
||||
/>
|
||||
|
||||
<QRModal item={item} isOpen={qrModalOpen} onClose={() => setQrModalOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import type { Item } from '#types/Item'
|
||||
import type { ItemsApi } from '#types/ItemsApi'
|
||||
|
||||
export interface HeaderViewProps {
|
||||
item?: Item
|
||||
api?: ItemsApi<unknown>
|
||||
editCallback?: (e: React.MouseEvent) => void
|
||||
deleteCallback?: (e: React.MouseEvent) => void
|
||||
setPositionCallback?: () => void
|
||||
loading?: boolean
|
||||
hideMenu?: boolean
|
||||
big?: boolean
|
||||
truncateSubname?: boolean
|
||||
showAddress?: boolean
|
||||
}
|
||||
|
||||
export interface PlatformConfig {
|
||||
shareUrl: string
|
||||
icon: JSX.Element
|
||||
label: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export interface SharePlatformConfigs {
|
||||
facebook: PlatformConfig
|
||||
twitter: PlatformConfig
|
||||
linkedin: PlatformConfig
|
||||
whatsapp: PlatformConfig
|
||||
telegram: PlatformConfig
|
||||
xing: PlatformConfig
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
2
lib/src/types/Item.d.ts
vendored
2
lib/src/types/Item.d.ts
vendored
@ -27,7 +27,7 @@ interface ItemSecret {
|
||||
*/
|
||||
export interface Item {
|
||||
id: string
|
||||
name: string
|
||||
name?: string
|
||||
text?: string
|
||||
data?: string
|
||||
position?: Point | null
|
||||
|
||||
1
lib/src/types/ItemType.d.ts
vendored
1
lib/src/types/ItemType.d.ts
vendored
@ -23,4 +23,5 @@ export interface ItemType {
|
||||
show_header_view_in_form?: boolean
|
||||
cta_button_label?: string
|
||||
show_address?: boolean
|
||||
cta_relation?: string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user