From 3233efb6d3834217deb01f8fdadc9eb077fe4146 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Fri, 10 Oct 2025 09:39:11 +0200 Subject: [PATCH] rebase --- .../ItemPopupComponents/HeaderView.tsx | 476 +----------------- .../HeaderView/ActionButtons.tsx | 163 ++++++ .../HeaderView/ConnectionStatus.tsx | 41 ++ .../HeaderView/DeleteModal.tsx | 37 ++ .../HeaderView/EditMenu.tsx | 112 +++++ .../HeaderView/ItemAvatar.tsx | 41 ++ .../HeaderView/ItemTitle.tsx | 64 +++ .../HeaderView/QRModal.tsx | 46 ++ .../ItemPopupComponents/HeaderView/hooks.ts | 82 +++ .../ItemPopupComponents/HeaderView/index.tsx | 80 +++ .../ItemPopupComponents/HeaderView/types.ts | 31 ++ lib/src/Components/Profile/ProfileView.tsx | 1 - lib/src/types/Item.d.ts | 2 +- lib/src/types/ItemType.d.ts | 1 + 14 files changed, 700 insertions(+), 477 deletions(-) create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ActionButtons.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ConnectionStatus.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/DeleteModal.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/EditMenu.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemAvatar.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/QRModal.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/hooks.ts create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/types.ts diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx index c18b7cee..21219a4a 100644 --- a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx @@ -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 - editCallback?: any - deleteCallback?: any - setPositionCallback?: any - loading?: boolean - hideMenu?: boolean - big?: boolean - truncateSubname?: boolean - showAddress?: boolean -}) { - const [modalOpen, setModalOpen] = useState(false) - const [qrModalOpen, setQrModalOpen] = useState(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: Facebook, - label: 'Facebook', - bgColor: '#3b5998', - }, - twitter: { - shareUrl: 'https://twitter.com/intent/tweet?text={title}:%20{url}', - icon: Twitter, - label: 'Twitter', - bgColor: '#55acee', - }, - linkedin: { - shareUrl: 'http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}', - icon: Linkedin, - label: 'LinkedIn', - bgColor: '#4875b4', - }, - whatsapp: { - shareUrl: 'https://api.whatsapp.com/send?text={title}%20{url}', - icon: Whatsapp, - label: 'WhatsApp', - bgColor: '#25D366', - }, - telegram: { - shareUrl: 'https://t.me/share/url?url={url}&text={title}', - icon: Telegram, - label: 'Telegram', - bgColor: '#0088cc', - }, - xing: { - shareUrl: 'https://www.xing-share.com/app/user?op=share;sc_p=xing-share;url={url}', - icon: Xing, - 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) => { - setModalOpen(true) - event.stopPropagation() - } - if (!item) return null - return ( - <> -
-
-
- {avatar && ( -
-
- {item.name setImageLoaded(true)} - onError={() => setImageLoaded(false)} - style={{ display: imageLoaded ? 'block' : 'none' }} - /> - {!imageLoaded && ( -
- )} -
-
- )} -
-
- {title} -
- {showAddress && address && ( -
- - - {address} - {distance && distance >= 0.1 && ` (${formatDistance(distance)})`} - -
- )} - {subtitle && !showAddress && ( -
- {subtitle} -
- )} -
-
-
-
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 && ( -
- - -
- )} -
-
- {big && ( -
-
-
- - {item?.position?.coordinates ? ( - - - - ) : ( -
- -
- )} - -
-
- -
- -
-
-
- )} - - setModalOpen(false)} - > -
e.stopPropagation()}> - - Do you want to delete {item.name}? - -
-
- - -
-
-
-
- - setQrModalOpen(false)} - className='tw:w-[calc(100vw-2rem)] tw:max-w-96' - > -
e.stopPropagation()} className='tw:text-center tw:p-4'> -

Share your profile with others to expand your network.

- -
- -
- -
- {inviteLink} - -
-
-
- - ) -} +export { HeaderView } from './HeaderView/index' diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ActionButtons.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ActionButtons.tsx new file mode 100644 index 00000000..435b92bf --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ActionButtons.tsx @@ -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: Facebook, + label: 'Facebook', + bgColor: '#3b5998', + }, + twitter: { + shareUrl: 'https://twitter.com/intent/tweet?text={title}:%20{url}', + icon: Twitter, + label: 'Twitter', + bgColor: '#55acee', + }, + linkedin: { + shareUrl: 'http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}', + icon: Linkedin, + label: 'LinkedIn', + bgColor: '#4875b4', + }, + whatsapp: { + shareUrl: 'https://api.whatsapp.com/send?text={title}%20{url}', + icon: Whatsapp, + label: 'WhatsApp', + bgColor: '#25D366', + }, + telegram: { + shareUrl: 'https://t.me/share/url?url={url}&text={title}', + icon: Telegram, + label: 'Telegram', + bgColor: '#0088cc', + }, + xing: { + shareUrl: 'https://www.xing-share.com/app/user?op=share;sc_p=xing-share;url={url}', + icon: Xing, + label: 'Xing', + bgColor: '#026466', + }, + } + + return ( + <> + {item.position?.coordinates && myProfile.myProfile?.id !== item.id && ( + + + + )} + {myProfile.myProfile?.id === item.id && ( + + )} + {myProfile.myProfile?.id !== item.id && ( +
+
+ +
+ +
+ )} + + ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ConnectionStatus.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ConnectionStatus.tsx new file mode 100644 index 00000000..b3309ccf --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ConnectionStatus.tsx @@ -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

✅ Connected

+ } + + return ( + + ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/DeleteModal.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/DeleteModal.tsx new file mode 100644 index 00000000..c721ba84 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/DeleteModal.tsx @@ -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 ( + +
e.stopPropagation()}> + + Do you want to delete {item.name}? + +
+
+ + +
+
+
+
+ ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/EditMenu.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/EditMenu.tsx new file mode 100644 index 00000000..4a16e1f6 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/EditMenu.tsx @@ -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 + 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) => { + 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 ( +
e.stopPropagation()} className={`${big ? 'tw:mt-5' : 'tw:mt-1'}`}> +
+ + +
+
+ ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemAvatar.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemAvatar.tsx new file mode 100644 index 00000000..0a364d32 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemAvatar.tsx @@ -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 ( +
+
+ {item.name setImageLoaded(true)} + onError={() => setImageLoaded(false)} + style={{ display: imageLoaded ? 'block' : 'none' }} + /> + {!imageLoaded &&
} +
+
+ ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx new file mode 100644 index 00000000..5434de77 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx @@ -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 ( +
+
+ {title} +
+ {showAddress && address && ( +
+ + + {address} + {distance && distance >= 0.1 && ` (${formatDistance(distance) ?? ''})`} + +
+ )} + {subtitle && !showAddress && ( +
+ {subtitle} +
+ )} +
+ ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/QRModal.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/QRModal.tsx new file mode 100644 index 00000000..c1dd7ccf --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/QRModal.tsx @@ -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 ( + +
e.stopPropagation()} className='tw:text-center tw:p-4'> +

Share your profile with others to expand your network.

+ +
+ +
+ +
+ {inviteLink} + +
+
+
+ ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/hooks.ts b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/hooks.ts new file mode 100644 index 00000000..9034ab53 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/hooks.ts @@ -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 } +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx new file mode 100644 index 00000000..3b2405cd --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx @@ -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(false) + const [qrModalOpen, setQrModalOpen] = useState(false) + + if (!item) return null + + const hasAvatar = !!(item.image ?? item.image_external) + + return ( + <> +
+
+
+ {hasAvatar && } + +
+
+ setModalOpen(true)} + /> +
+ + {big && ( +
+
+
+ + setQrModalOpen(true)} /> +
+
+ )} + + setModalOpen(false)} + onConfirm={deleteCallback ?? (() => undefined)} + /> + + setQrModalOpen(false)} /> + + ) +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/types.ts b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/types.ts new file mode 100644 index 00000000..6f95df4a --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/types.ts @@ -0,0 +1,31 @@ +import type { Item } from '#types/Item' +import type { ItemsApi } from '#types/ItemsApi' + +export interface HeaderViewProps { + item?: Item + api?: ItemsApi + 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 +} diff --git a/lib/src/Components/Profile/ProfileView.tsx b/lib/src/Components/Profile/ProfileView.tsx index 5558d13f..0af9a9ce 100644 --- a/lib/src/Components/Profile/ProfileView.tsx +++ b/lib/src/Components/Profile/ProfileView.tsx @@ -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 */ diff --git a/lib/src/types/Item.d.ts b/lib/src/types/Item.d.ts index ece0d01d..967277b7 100644 --- a/lib/src/types/Item.d.ts +++ b/lib/src/types/Item.d.ts @@ -27,7 +27,7 @@ interface ItemSecret { */ export interface Item { id: string - name: string + name?: string text?: string data?: string position?: Point | null diff --git a/lib/src/types/ItemType.d.ts b/lib/src/types/ItemType.d.ts index 22882ece..23b814ec 100644 --- a/lib/src/types/ItemType.d.ts +++ b/lib/src/types/ItemType.d.ts @@ -23,4 +23,5 @@ export interface ItemType { show_header_view_in_form?: boolean cta_button_label?: string show_address?: boolean + cta_relation?: string }