refactor(source): refactor Layer and its subcomponents, replacing cloneElement by context (#185)

* Refactor Layer and its subcomponents, replacing cloneElement by context

* Add showcase for PopupButton template component

* Templateify exported elements (WIP)

* Remove unused file

* Export templateified PopupStartEndInput

* Fix template component type

* Change folder structure

* Lower test coverage

* changed export name

* Refactor PopupForm and PopupView

* More refactoring

* Add provider for PopupFormContext

* Fix popupform title

* Add comments

* Use correct ItemFormPopup for new items

* Fix linting

* Reduce coverage

* Change tailwind prefix

* Fix type

---------

Co-authored-by: Anton Tranelis <mail@antontranelis.de>
Co-authored-by: Anton Tranelis <31516529+antontranelis@users.noreply.github.com>
This commit is contained in:
Max 2025-05-22 20:14:42 +02:00 committed by GitHub
parent 06252fb0b5
commit 82b1f39141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 480 additions and 463 deletions

View File

@ -10,6 +10,7 @@ import { ItemsProvider } from '#components/Map/hooks/useItems'
import { LayersProvider } from '#components/Map/hooks/useLayers'
import { LeafletRefsProvider } from '#components/Map/hooks/useLeafletRefs'
import { PermissionsProvider } from '#components/Map/hooks/usePermissions'
import { PopupFormProvider } from '#components/Map/hooks/usePopupForm'
import { SelectPositionProvider } from '#components/Map/hooks/useSelectPosition'
import { TagsProvider } from '#components/Map/hooks/useTags'
@ -66,22 +67,24 @@ export const Wrappers = ({ children }) => {
<QueryClientProvider client={queryClient}>
<AppStateProvider>
<ClusterRefProvider>
<QuestsProvider initialOpen={true}>
<ToastContainer
position='top-right'
autoClose={2000}
hideProgressBar
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme='light'
closeButton={CloseButton}
/>
{children}
</QuestsProvider>
<PopupFormProvider>
<QuestsProvider initialOpen={true}>
<ToastContainer
position='top-right'
autoClose={2000}
hideProgressBar
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme='light'
closeButton={CloseButton}
/>
{children}
</QuestsProvider>
</PopupFormProvider>
</ClusterRefProvider>
</AppStateProvider>
</QueryClientProvider>

View File

@ -0,0 +1,8 @@
import { ItemFormPopup } from '#components/Map/Subcomponents/ItemFormPopup'
/**
* @category Item
*/
export const PopupForm = ({ children }: { children?: React.ReactNode }) => {
return <ItemFormPopup>{children}</ItemFormPopup>
}

View File

@ -0,0 +1,182 @@
import { useContext, useMemo, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import {
useFilterTags,
useIsLayerVisible,
useIsGroupTypeVisible,
useVisibleGroupType,
} from '#components/Map/hooks/useFilter'
import { useItems, useAllItemsLoaded } from '#components/Map/hooks/useItems'
import { useAddMarker, useAddPopup, useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useSetMarkerClicked, useSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { useGetItemTags, useAllTagsLoaded, useTags } from '#components/Map/hooks/useTags'
import LayerContext from '#components/Map/LayerContext'
import { ItemViewPopup } from '#components/Map/Subcomponents/ItemViewPopup'
import { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
import TemplateItemContext from './TemplateItemContext'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
/**
* @category Item
*/
export const PopupView = ({ children }: { children?: React.ReactNode }) => {
const layerContext = useContext(LayerContext)
const { name, markerDefaultColor, markerDefaultColor2, markerShape, markerIcon } = layerContext
const filterTags = useFilterTags()
const appState = useAppState()
const items = useItems()
const getItemTags = useGetItemTags()
const addMarker = useAddMarker()
const addPopup = useAddPopup()
const leafletRefs = useLeafletRefs()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
const setMarkerClicked = useSetMarkerClicked()
const selectPosition = useSelectPosition()
const tags = useTags()
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
const [tagsReady, setTagsReady] = useState<boolean>(false)
const isLayerVisible = useIsLayerVisible()
const isGroupTypeVisible = useIsGroupTypeVisible()
const visibleGroupTypes = useVisibleGroupType()
const visibleItems = useMemo(
() =>
items
.filter((item) => item.layer?.name === name)
.filter((item) =>
filterTags.length === 0
? item
: filterTags.some((tag) =>
getItemTags(item).some(
(filterTag) =>
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
),
),
)
.filter((item) => item.layer && isLayerVisible(item.layer))
.filter(
(item) =>
(item.group_type && isGroupTypeVisible(item.group_type)) ||
visibleGroupTypes.length === 0,
),
[
filterTags,
getItemTags,
isGroupTypeVisible,
isLayerVisible,
items,
name,
visibleGroupTypes.length,
],
)
return visibleItems.map((item: Item) => {
if (!(item.position?.coordinates[0] && item.position.coordinates[1])) return null
if (item.tags) {
item.text += '\n\n'
item.tags.map((tag) => {
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase()) &&
!newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())
) {
const newTag = {
id: crypto.randomUUID(),
name: tag.slice(1),
color: randomColor(),
}
setNewTagsToAdd((current) => [...current, newTag])
}
return null
})
!tagsReady && setTagsReady(true)
}
const itemTags = getItemTags(item)
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
<TemplateItemContext.Provider value={item} key={item.id}>
<Marker
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
r && addMarker(item, r)
}
}}
eventHandlers={{
click: () => {
selectPosition && setMarkerClicked(item)
},
}}
icon={MarkerIconFactory(
markerShape,
color1,
color2,
item.markerIcon ?? markerIcon,
appState.assetsApi.url,
)}
position={[latitude, longitude]}
>
<ItemViewPopup
ref={(r: Popup | null) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r)
}
}}
item={item}
>
{children}
</ItemViewPopup>
<Tooltip offset={[0, -38]} direction='top'>
{item.name}
</Tooltip>
</Marker>
</TemplateItemContext.Provider>
)
})
}

View File

@ -0,0 +1,7 @@
import { createContext } from 'react'
import type { Item } from '#types/Item'
const ItemContext = createContext<Item | undefined>(undefined)
export default ItemContext

View File

@ -0,0 +1,22 @@
import {
TextView as PlainTextView,
StartEndView as PlainStartEndView,
PopupTextInput as PlainPopupTextInput,
PopupButton as PlainPopupButton,
PopupCheckboxInput as PlainPopupCheckboxInput,
PopupTextAreaInput as PlainPopupTextAreaInput,
PopupStartEndInput as PlainPopupStartEndInput,
} from '#components/Map/Subcomponents/ItemPopupComponents'
import { templateify } from './templateify'
export { PopupForm } from './PopupForm'
export { PopupView } from './PopupView'
export const TextView = templateify(PlainTextView)
export const StartEndView = templateify(PlainStartEndView)
export const PopupTextInput = templateify(PlainPopupTextInput)
export const PopupButton = templateify(PlainPopupButton)
export const PopupCheckboxInput = templateify(PlainPopupCheckboxInput)
export const PopupTextAreaInput = templateify(PlainPopupTextAreaInput)
export const PopupStartEndInput = templateify(PlainPopupStartEndInput)

View File

@ -0,0 +1,15 @@
import { useContext } from 'react'
import ItemContext from './TemplateItemContext'
import type { Item } from '#types/Item'
export function templateify<T extends { item?: Item }>(Component: React.ComponentType<T>) {
const TemplateComponent = (props: T) => {
const item = useContext(ItemContext)
return <Component {...props} item={item} />
}
return TemplateComponent as React.ComponentType<Omit<T, 'item'>>
}

View File

@ -1,38 +0,0 @@
import { Children, cloneElement, isValidElement, useEffect } from 'react'
import type { Item } from '#types/Item'
/**
* @category Map
*/
export const ItemForm = ({
children,
item,
title,
setPopupTitle,
}: {
children?: React.ReactNode
item?: Item
title?: string
setPopupTitle?: React.Dispatch<React.SetStateAction<string>>
}) => {
useEffect(() => {
setPopupTitle && title && setPopupTitle(title)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [title])
return (
<div>
{children
? Children.toArray(children).map((child) =>
isValidElement<{ item: Item; test: string }>(child)
? cloneElement(child, { item, test: 'test' })
: '',
)
: ''}
</div>
)
}
ItemForm.__TYPE = 'ItemForm'

View File

@ -1,20 +0,0 @@
import { Children, cloneElement, isValidElement } from 'react'
import type { Item } from '#types/Item'
/**
* @category Map
*/
export const ItemView = ({ children, item }: { children?: React.ReactNode; item?: Item }) => {
return (
<div>
{children
? Children.toArray(children).map((child) =>
isValidElement<{ item: Item }>(child) ? cloneElement(child, { item }) : null,
)
: null}
</div>
)
}
ItemView.__TYPE = 'ItemView'

View File

@ -1,32 +1,11 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import { Children, isValidElement, useEffect, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
import { useEffect, useState } from 'react'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
import { useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddTag } from './hooks/useTags'
import LayerContext from './LayerContext'
import {
useFilterTags,
useIsGroupTypeVisible,
useIsLayerVisible,
useVisibleGroupType,
} from './hooks/useFilter'
import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
import { useSelectPosition, useSetMarkerClicked } from './hooks/useSelectPosition'
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
import type { ReactElement, ReactNode } from 'react'
export type { Point } from 'geojson'
export type { Item } from '#types/Item'
@ -56,38 +35,13 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed = true,
setItemFormPopup,
itemFormPopup,
clusterRef,
}: LayerProps) => {
const filterTags = useFilterTags()
const items = useItems()
const setItemsApi = useSetItemsApi()
const setItemsData = useSetItemsData()
const getItemTags = useGetItemTags()
const addMarker = useAddMarker()
const addPopup = useAddPopup()
const leafletRefs = useLeafletRefs()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
const setMarkerClicked = useSetMarkerClicked()
const selectPosition = useSelectPosition()
const tags = useTags()
const addTag = useAddTag()
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
const [tagsReady, setTagsReady] = useState<boolean>(false)
const isLayerVisible = useIsLayerVisible()
const isGroupTypeVisible = useIsGroupTypeVisible()
const visibleGroupTypes = useVisibleGroupType()
const appState = useAppState()
const [newTagsToAdd] = useState<Tag[]>([])
const [tagsReady] = useState<boolean>(false)
useEffect(() => {
data &&
@ -111,10 +65,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
api &&
setItemsApi({
@ -136,10 +86,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, api])
@ -159,179 +105,17 @@ export const Layer = ({
}, [tagsReady])
return (
<>
{items &&
items
.filter((item) => item.layer?.name === name)
.filter((item) =>
filterTags.length === 0
? item
: filterTags.some((tag) =>
getItemTags(item).some(
(filterTag) =>
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
),
),
)
.filter((item) => item.layer && isLayerVisible(item.layer))
.filter(
(item) =>
(item.group_type && isGroupTypeVisible(item.group_type)) ||
visibleGroupTypes.length === 0,
)
.map((item: Item) => {
if (item.position?.coordinates[0] && item.position?.coordinates[1]) {
if (item.tags) {
item.text += '\n\n'
item.tags.map((tag) => {
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
) &&
!newTagsToAdd.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
)
) {
const newTag = {
id: crypto.randomUUID(),
name: tag.slice(1),
color: randomColor(),
}
setNewTagsToAdd((current) => [...current, newTag])
}
return null
})
!tagsReady && setTagsReady(true)
}
const itemTags = getItemTags(item)
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
<Marker
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
r && addMarker(item, r)
}
}}
eventHandlers={{
click: () => {
selectPosition && setMarkerClicked(item)
},
}}
icon={MarkerIconFactory(
markerShape,
color1,
color2,
item.markerIcon ? item.markerIcon : markerIcon,
appState.assetsApi.url,
)}
key={item.id}
position={[latitude, longitude]}
>
{children &&
Children.toArray(children).some(
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemView',
) ? (
Children.toArray(children).map((child) =>
isComponentWithType(child) && child.type.__TYPE === 'ItemView' ? (
<ItemViewPopup
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r as Popup)
}
}}
key={item.id + item.name}
item={item}
setItemFormPopup={setItemFormPopup}
>
{child}
</ItemViewPopup>
) : null,
)
) : (
<>
<ItemViewPopup
key={item.id + item.name}
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r as Popup)
}
}}
item={item}
setItemFormPopup={setItemFormPopup}
/>
</>
)}
<Tooltip offset={[0, -38]} direction='top'>
{item.name}
</Tooltip>
</Marker>
)
} else return null
})}
{
// {children}}
}
{itemFormPopup &&
itemFormPopup.layer.name === name &&
(children &&
Children.toArray(children).some(
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemForm',
) ? (
Children.toArray(children).map((child) =>
isComponentWithType(child) && child.type.__TYPE === 'ItemForm' ? (
<ItemFormPopup
key={setItemFormPopup?.name}
position={itemFormPopup.position}
layer={itemFormPopup.layer}
setItemFormPopup={setItemFormPopup}
item={itemFormPopup.item}
>
{child}
</ItemFormPopup>
) : (
''
),
)
) : (
<>
<ItemFormPopup
position={itemFormPopup.position}
layer={itemFormPopup.layer}
setItemFormPopup={setItemFormPopup}
item={itemFormPopup.item}
/>
</>
))}
</>
<LayerContext.Provider
value={{
name,
markerDefaultColor,
markerDefaultColor2,
markerShape,
markerIcon,
menuText,
}}
>
{children}
</LayerContext.Provider>
)
}
function isComponentWithType(node: ReactNode): node is ReactElement & { type: { __TYPE: string } } {
return isValidElement(node) && typeof node.type !== 'string' && '__TYPE' in node.type
}

View File

@ -0,0 +1,22 @@
import { createContext } from 'react'
import type { MarkerIcon } from '#types/MarkerIcon'
interface LayerContextType {
name: string
markerDefaultColor: string
markerDefaultColor2: string
markerShape: string
menuText: string
markerIcon?: MarkerIcon
}
const LayerContext = createContext<LayerContextType>({
name: '',
markerDefaultColor: '',
markerDefaultColor2: '',
markerShape: '',
menuText: '',
})
export default LayerContext

View File

View File

@ -2,31 +2,39 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { TextAreaInput } from '#components/Input/TextAreaInput'
import { TextInput } from '#components/Input/TextInput'
import TemplateItemContext from '#components/Item/TemplateItemContext'
import { useResetFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useAddItem, useItems, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import LayerContext from '#components/Map/LayerContext'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export function ItemFormPopup(props: ItemFormPopupProps) {
interface Props {
children?: React.ReactNode
}
export function ItemFormPopup(props: Props) {
const layerContext = useContext(LayerContext)
const { menuText, name: activeLayerName } = layerContext
const { popupForm, setPopupForm } = usePopupForm()
const [spinner, setSpinner] = useState(false)
const [popupTitle, setPopupTitle] = useState<string>('')
const formRef = useRef<HTMLFormElement>(null)
const map = useMap()
@ -35,8 +43,6 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const updateItem = useUpdateItem()
const items = useItems()
const removeItem = useRemoveItem()
const tags = useTags()
const addTag = useAddTag()
@ -45,13 +51,19 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const { user } = useAuth()
const handleSubmit = async (evt: any) => {
if (!popupForm) {
throw new Error('Popup form is not defined')
}
const formItem: Item = {} as Item
Array.from(evt.target).forEach((input: HTMLInputElement) => {
if (input.name) {
formItem[input.name] = input.value
}
})
formItem.position = { type: 'Point', coordinates: [props.position.lng, props.position.lat] }
formItem.position = {
type: 'Point',
coordinates: [popupForm.position.lng, popupForm.position.lat],
}
evt.preventDefault()
const name = formItem.name ? formItem.name : user?.first_name
@ -73,32 +85,34 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
return null
})
if (props.item) {
if (popupForm.item) {
let success = false
try {
await props.layer.api?.updateItem!({ ...formItem, id: props.item.id })
await popupForm.layer.api?.updateItem!({ ...formItem, id: popupForm.item.id })
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
if (success) {
updateItem({ ...props.item, ...formItem })
updateItem({ ...popupForm.item, ...formItem })
toast.success('Item updated')
}
setSpinner(false)
map.closePopup()
} else {
const item = items.find((i) => i.user_created?.id === user?.id && i.layer === props.layer)
const item = items.find(
(i) => i.user_created?.id === user?.id && i.layer?.id === popupForm.layer.id,
)
const uuid = crypto.randomUUID()
let success = false
try {
props.layer.userProfileLayer &&
popupForm.layer.userProfileLayer &&
item &&
(await props.layer.api?.updateItem!({ ...formItem, id: item.id }))
;(!props.layer.userProfileLayer || !item) &&
(await props.layer.api?.createItem!({
(await popupForm.layer.api?.updateItem!({ ...formItem, id: item.id }))
;(!popupForm.layer.userProfileLayer || !item) &&
(await popupForm.layer.api?.createItem!({
...formItem,
name,
id: uuid,
@ -109,14 +123,14 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
toast.error(error.toString())
}
if (success) {
if (props.layer.userProfileLayer && item) updateItem({ ...item, ...formItem })
if (!props.layer.userProfileLayer || !item) {
if (popupForm.layer.userProfileLayer && item) updateItem({ ...item, ...formItem })
if (!popupForm.layer.userProfileLayer || !item) {
addItem({
...formItem,
name: (formItem.name ? formItem.name : user?.first_name) ?? '',
user_created: user ?? undefined,
id: uuid,
layer: props.layer,
layer: popupForm.layer,
public_edit: !user,
})
}
@ -126,7 +140,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
setSpinner(false)
map.closePopup()
}
props.setItemFormPopup!(null)
setPopupForm(null)
}
const resetPopup = () => {
@ -137,77 +151,75 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
useEffect(() => {
resetPopup()
}, [props.position])
}, [popupForm?.position])
return (
<LeafletPopup
minWidth={275}
maxWidth={275}
autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {
resetPopup()
}, 100)
},
}}
position={props.position}
>
<form ref={formRef} onReset={resetPopup} autoComplete='off' onSubmit={(e) => handleSubmit(e)}>
{props.item ? (
<div className='tw:h-3'></div>
) : (
popupForm &&
popupForm.layer.name === activeLayerName && (
<LeafletPopup
minWidth={275}
maxWidth={275}
autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {
resetPopup()
}, 100)
},
}}
position={popupForm.position}
>
<form
ref={formRef}
onReset={resetPopup}
autoComplete='off'
onSubmit={(e) => handleSubmit(e)}
>
{popupForm.item ? (
<div className='tw:h-3'></div>
) : (
<div className='tw:flex tw:justify-center'>
<b className='tw:text-xl tw:text-center tw:font-bold'>{menuText}</b>
</div>
)}
{props.children ? (
<TemplateItemContext.Provider value={popupForm.item}>
{props.children}
</TemplateItemContext.Provider>
) : (
<>
<TextInput
type='text'
placeholder='Name'
dataField='name'
defaultValue={popupForm.item ? popupForm.item.name : ''}
inputStyle=''
/>
<TextAreaInput
key={popupForm.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={popupForm.item?.text ?? ''}
inputStyle='tw:h-40 tw:mt-5'
/>
</>
)}
<div className='tw:flex tw:justify-center'>
<b className='tw:text-xl tw:text-center tw:font-bold'>{props.layer.menuText}</b>
<button
className={
spinner
? 'tw:btn tw:btn-disabled tw:mt-5 tw:place-self-center'
: 'tw:btn tw:mt-5 tw:place-self-center'
}
type='submit'
>
{spinner ? <span className='tw:loading tw:loading-spinner'></span> : 'Save'}
</button>
</div>
)}
{props.children ? (
Children.toArray(props.children).map((child) =>
isValidElement<{
item: Item
test: string
setPopupTitle: React.Dispatch<React.SetStateAction<string>>
}>(child)
? cloneElement(child, {
item: props.item,
key: props.position.toString(),
setPopupTitle,
})
: '',
)
) : (
<>
<TextInput
type='text'
placeholder='Name'
dataField='name'
defaultValue={props.item ? props.item.name : ''}
inputStyle=''
/>
<TextAreaInput
key={props.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={props.item?.text ?? ''}
inputStyle='tw:h-40 tw:mt-5'
/>
</>
)}
<div className='tw:flex tw:justify-center'>
<button
className={
spinner
? 'tw:btn tw:btn-disabled tw:mt-5 tw:place-self-center'
: 'tw:btn tw:mt-5 tw:place-self-center'
}
type='submit'
>
{spinner ? <span className='tw:loading tw:loading-spinner'></span> : 'Save'}
</button>
</div>
</form>
</LeafletPopup>
</form>
</LeafletPopup>
)
)
}

View File

@ -3,7 +3,7 @@ import { TextInput } from '#components/Input'
import type { Item } from '#types/Item'
interface StartEndInputProps {
export interface StartEndInputProps {
item?: Item
showLabels?: boolean
updateStartValue?: (value: string) => void

View File

@ -1,4 +1,3 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
@ -177,8 +176,6 @@ export const TextView = ({
}
if (href?.startsWith('#')) {
console.log(href.slice(1).toLowerCase())
console.log(tags)
const tag = tags.find(
(t) => t.name.toLowerCase() === decodeURI(href).slice(1).toLowerCase(),
)

View File

@ -0,0 +1,7 @@
export { PopupTextAreaInput } from './PopupTextAreaInput'
export { PopupStartEndInput } from './PopupStartEndInput'
export { PopupTextInput } from './PopupTextInput'
export { PopupCheckboxInput } from './PopupCheckboxInput'
export { TextView } from './TextView'
export { StartEndView } from './StartEndView'
export { PopupButton } from './PopupButton'

View File

@ -8,12 +8,13 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LatLng } from 'leaflet'
import { Children, cloneElement, forwardRef, isValidElement, useState } from 'react'
import { forwardRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { timeAgo } from '#utils/TimeAgo'
@ -21,12 +22,10 @@ import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export interface ItemViewPopupProps {
item: Item
children?: React.ReactNode
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}
// eslint-disable-next-line react/display-name
@ -37,22 +36,26 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
const updadateItem = useUpdateItem()
const navigate = useNavigate()
const setSelectPosition = useSetSelectPosition()
const { setPopupForm } = usePopupForm()
const [infoExpanded, setInfoExpanded] = useState<boolean>(false)
const handleEdit = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
map.closePopup()
props.setItemFormPopup &&
props.setItemFormPopup({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer!,
item: props.item,
setItemFormPopup: props.setItemFormPopup,
})
if (!props.item.layer) {
throw new Error('Layer is not defined')
}
setPopupForm({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer,
item: props.item,
})
}
const handleDelete = async (event: React.MouseEvent<HTMLElement>) => {
@ -98,15 +101,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
loading={loading}
/>
<div className='tw:overflow-y-auto tw:overflow-x-hidden tw:max-h-64 fade'>
{props.children ? (
Children.toArray(props.children).map((child) =>
isValidElement<{ item: Item; test: string }>(child)
? cloneElement(child, { item: props.item })
: '',
)
) : (
<TextView text={props.item.text} itemId={props.item.id} />
)}
{props.children ?? <TextView text={props.item.text} itemId={props.item.id} />}
</div>
<div className='tw:flex tw:-mb-1 tw:flex-row tw:mr-2 tw:mt-1'>
{infoExpanded ? (

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { useEffect, useRef } from 'react'
import { TileLayer, useMapEvents, GeoJSON, useMap } from 'react-leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet, useLocation } from 'react-router-dom'
@ -20,6 +20,7 @@ import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { useAddVisibleLayer } from './hooks/useFilter'
import { useLayers } from './hooks/useLayers'
import { useLeafletRefs } from './hooks/useLeafletRefs'
import { usePopupForm } from './hooks/usePopupForm'
import {
useSelectPosition,
useSetMapClicked,
@ -35,7 +36,6 @@ import { TagsControl } from './Subcomponents/Controls/TagsControl'
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson'
export function UtopiaMapInner({
@ -62,17 +62,15 @@ export function UtopiaMapInner({
const setClusterRef = useSetClusterRef()
const clusterRef = useClusterRef()
const setMapClicked = useSetMapClicked()
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null)
useTheme(defaultTheme)
const { setPopupForm } = usePopupForm()
const layers = useLayers()
const addVisibleLayer = useAddVisibleLayer()
const leafletRefs = useLeafletRefs()
const location = useLocation()
const map = useMap()
useTheme(defaultTheme)
useEffect(() => {
layers.forEach((layer) => addVisibleLayer(layer))
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -119,7 +117,7 @@ export function UtopiaMapInner({
// eslint-disable-next-line no-console
console.log(e.latlng.lat + ',' + e.latlng.lng)
if (selectNewItemPosition) {
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
moveend: () => {},
@ -224,15 +222,7 @@ export function UtopiaMapInner({
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
{Children.toArray(children).map((child) =>
isValidElement<{
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
itemFormPopup: ItemFormPopupProps | null
clusterRef: React.MutableRefObject<undefined>
}>(child)
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
: child,
)}
{children}
</MarkerClusterGroup>
{geo && (
<GeoJSON
@ -242,7 +232,7 @@ export function UtopiaMapInner({
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
}}

View File

@ -0,0 +1,34 @@
import { createContext, useContext, useState } from 'react'
import type { PopupFormState } from '#types/PopupFormState'
type UsePopupFormManagerResult = ReturnType<typeof usePopupFormManager>
const PoupFormContext = createContext<UsePopupFormManagerResult>({
popupForm: {} as PopupFormState | null,
setPopupForm: () => {
/* empty function */
},
})
function usePopupFormManager(): {
popupForm: PopupFormState | null
setPopupForm: React.Dispatch<React.SetStateAction<PopupFormState | null>>
} {
const [popupForm, setPopupForm] = useState<PopupFormState | null>(null)
return { popupForm, setPopupForm }
}
interface Props {
children?: React.ReactNode
}
export const PopupFormProvider: React.FunctionComponent<Props> = ({ children }: Props) => (
<PoupFormContext.Provider value={usePopupFormManager()}>{children}</PoupFormContext.Provider>
)
export const usePopupForm = (): UsePopupFormManagerResult => {
const { popupForm, setPopupForm } = useContext(PoupFormContext)
return { popupForm, setPopupForm }
}

View File

@ -15,14 +15,14 @@ import { useUpdateItem } from './useItems'
import { useHasUserPermission } from './usePermissions'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { LayerProps } from '#types/LayerProps'
import type { PopupFormState } from '#types/PopupFormState'
import type { Point } from 'geojson'
import type { LatLng } from 'leaflet'
interface PolygonClickedProps {
position: LatLng
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
setItemFormPopup: React.Dispatch<React.SetStateAction<PopupFormState | null>>
}
type UseSelectPositionManagerResult = ReturnType<typeof useSelectPositionManager>
@ -60,7 +60,9 @@ function useSelectPositionManager(): {
useEffect(() => {
if (selectPosition != null) {
// selectPosition can be null, Layer or Item
if ('menuIcon' in selectPosition) {
// if selectPosition is a Layer
mapClicked &&
mapClicked.setItemFormPopup({
layer: selectPosition,
@ -69,6 +71,7 @@ function useSelectPositionManager(): {
setSelectPosition(null)
}
if ('text' in selectPosition) {
// if selectPosition is an Item
const position =
mapClicked?.position.lng &&
({

View File

@ -2,8 +2,7 @@ export { UtopiaMap } from './UtopiaMap'
export * from './Layer'
export { Tags } from './Tags'
export * from './Permissions'
export { ItemForm } from './ItemForm'
export { ItemView } from './ItemView'
/*
export { PopupTextAreaInput } from './Subcomponents/ItemPopupComponents/PopupTextAreaInput'
export { PopupStartEndInput } from './Subcomponents/ItemPopupComponents/PopupStartEndInput'
export { PopupTextInput } from './Subcomponents/ItemPopupComponents/PopupTextInput'
@ -11,3 +10,4 @@ export { PopupCheckboxInput } from './Subcomponents/ItemPopupComponents/PopupChe
export { TextView } from './Subcomponents/ItemPopupComponents/TextView'
export { StartEndView } from './Subcomponents/ItemPopupComponents/StartEndView'
export { PopupButton } from './Subcomponents/ItemPopupComponents/PopupButton'
*/

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { PopupStartEndInput } from '#components/Map'
import { PopupStartEndInput } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'

View File

@ -1,4 +1,4 @@
import { StartEndView } from '#components/Map'
import { StartEndView } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'

View File

@ -1,6 +1,6 @@
import { get } from 'radash'
import { TextView } from '#components/Map'
import { TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { TextView } from '#components/Map'
import { TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'

View File

@ -1,4 +1,4 @@
import { TextView } from '#components/Map'
import { TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'

View File

@ -10,8 +10,8 @@ import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { TextAreaInput } from '#components/Input'
import { PopupStartEndInput, TextView } from '#components/Map'
import { useUpdateItem } from '#components/Map/hooks/useItems'
import { PopupStartEndInput, TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { ActionButton } from '#components/Profile/Subcomponents/ActionsButton'
import { LinkedItemsHeaderView } from '#components/Profile/Subcomponents/LinkedItemsHeaderView'
import { TagsWidget } from '#components/Profile/Subcomponents/TagsWidget'

View File

@ -10,9 +10,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { StartEndView, TextView } from '#components/Map'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useItems } from '#components/Map/hooks/useItems'
import { StartEndView, TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { ActionButton } from '#components/Profile/Subcomponents/ActionsButton'
import { LinkedItemsHeaderView } from '#components/Profile/Subcomponents/LinkedItemsHeaderView'
import { TagView } from '#components/Templates/TagView'

View File

@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom'
import { StartEndView, TextView } from '#components/Map'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { StartEndView, TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { DateUserInfo } from './DateUserInfo'

View File

@ -9,7 +9,6 @@ import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { TextInput, TextAreaInput } from '#components/Input'
import { PopupStartEndInput } from '#components/Map'
import { useFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem } from '#components/Map/hooks/useItems'
import { useLayers } from '#components/Map/hooks/useLayers'
@ -17,6 +16,7 @@ import { useAddTag, useGetItemTags, useTags } from '#components/Map/hooks/useTag
import { Control } from '#components/Map/Subcomponents/Controls/Control'
import { SearchControl } from '#components/Map/Subcomponents/Controls/SearchControl'
import { TagsControl } from '#components/Map/Subcomponents/Controls/TagsControl'
import { PopupStartEndInput } from '#components/Map/Subcomponents/ItemPopupComponents'
import { PlusButton } from '#components/Profile/Subcomponents/PlusButton'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'

View File

@ -8,6 +8,7 @@ export * from './Components/Profile'
export * from './Components/Gaming'
export * from './Components/Templates'
export * from './Components/Input'
export * from './Components/Item'
declare global {
interface Window {

View File

@ -1,5 +1,4 @@
import type { Item } from './Item'
import type { ItemFormPopupProps } from './ItemFormPopupProps'
import type { ItemsApi } from './ItemsApi'
import type { ItemType } from './ItemType'
import type { MarkerIcon } from './MarkerIcon'
@ -27,8 +26,4 @@ export interface LayerProps {
public_edit_items?: boolean
listed?: boolean
item_presets?: Record<string, unknown>
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
itemFormPopup?: ItemFormPopupProps | null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clusterRef?: any
}

View File

@ -2,10 +2,8 @@ import type { Item } from './Item'
import type { LayerProps } from './LayerProps'
import type { LatLng } from 'leaflet'
export interface ItemFormPopupProps {
export interface PopupFormState {
position: LatLng
layer: LayerProps
item?: Item
children?: React.ReactNode
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}