Refactor Layer and its subcomponents, replacing cloneElement by context

This commit is contained in:
Maximilian Harz 2025-03-06 17:38:18 +01:00
parent 9e6bcf1846
commit d739977ca1
11 changed files with 270 additions and 278 deletions

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,31 +1,12 @@
/* 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 { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
import LayerContext from '#components/Profile/templateComponents/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 { useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddTag } from './hooks/useTags'
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'
@ -59,32 +40,12 @@ export const Layer = ({
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 [newTagsToAdd] = useState<Tag[]>([])
const [tagsReady] = useState<boolean>(false)
useEffect(() => {
data &&
@ -156,178 +117,18 @@ 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,
)}
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,
itemFormPopup,
setItemFormPopup,
}}
>
{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

View File

@ -2,8 +2,6 @@ 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'

View File

@ -2,3 +2,5 @@ export { UserSettings } from './UserSettings'
// export { PlusButton } from './Subcomponents/PlusButton'
export { ProfileView } from './ProfileView'
export { ProfileForm } from './ProfileForm'
export { CardForm } from './templateComponents/CardForm'
export { CardView } from './templateComponents/CardView'

View File

@ -0,0 +1,29 @@
import { useContext } from 'react'
import { ItemFormPopup } from '#components/Map/Subcomponents/ItemFormPopup'
import LayerContext from './LayerContext'
import TemplateItemContext from './TemplateItemContext'
/**
* @category Map
*/
export const CardForm = ({ children }: { children?: React.ReactNode }) => {
const { itemFormPopup, setItemFormPopup } = useContext(LayerContext)
return (
itemFormPopup && (
<ItemFormPopup
key={setItemFormPopup?.name}
position={itemFormPopup.position}
layer={itemFormPopup.layer}
setItemFormPopup={setItemFormPopup}
item={itemFormPopup.item}
>
<TemplateItemContext.Provider value={itemFormPopup.item}>
{children}
</TemplateItemContext.Provider>
</ItemFormPopup>
)
)
}

View File

@ -0,0 +1,186 @@
import { useContext, useMemo, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
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 { 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 LayerContext from './LayerContext'
import TemplateItemContext from './TemplateItemContext'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
// TODO Think about folder structure. This is not for profile, but for card / popup. Both can use the same template components.
/**
* @category Profile
*/
export const CardView = ({ children }: { children?: React.ReactNode }) => {
const cardViewContext = useContext(LayerContext)
const {
name,
markerDefaultColor,
markerDefaultColor2,
markerShape,
markerIcon,
setItemFormPopup,
} = cardViewContext
const filterTags = useFilterTags()
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,
],
)
if (!setItemFormPopup) {
throw new Error('setItemFormPopup is not defined')
}
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)}
position={[latitude, longitude]}
>
<ItemViewPopup
ref={(r: Popup | null) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r)
}
}}
item={item}
setItemFormPopup={setItemFormPopup}
>
{children}
</ItemViewPopup>
<Tooltip offset={[0, -38]} direction='top'>
{item.name}
</Tooltip>
</Marker>
</TemplateItemContext.Provider>
)
})
}

View File

@ -0,0 +1,27 @@
import { createContext } from 'react'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
// Where should we define defaults, here or in Layer.tsx?
interface LayerContextType {
name: string
markerDefaultColor: string
markerDefaultColor2: string
markerShape: string
markerIcon: string
itemFormPopup: ItemFormPopupProps | null | undefined
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>> | undefined
}
const LayerContext = createContext<LayerContextType>({
name: '',
markerDefaultColor: '#777',
markerDefaultColor2: 'RGBA(35, 31, 32, 0.2)',
markerShape: 'circle',
markerIcon: '',
itemFormPopup: undefined,
setItemFormPopup: undefined,
})
export default LayerContext

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