Refactor layers and items into one common state (WIP)

This commit is contained in:
Maximilian Harz 2025-07-11 01:32:29 +02:00
parent ed4cdce37c
commit 81e2d53afa
13 changed files with 240 additions and 209 deletions

View File

@ -7,7 +7,6 @@ import { QuestsProvider } from '#components/Gaming/hooks/useQuests'
import { ClusterRefProvider } from '#components/Map/hooks/useClusterRef'
import { FilterProvider } from '#components/Map/hooks/useFilter'
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'
@ -59,40 +58,38 @@ export const Wrappers = ({ children }) => {
return (
<PermissionsProvider initialPermissions={[]}>
<TagsProvider initialTags={[]}>
<LayersProvider initialLayers={[]}>
<ItemsProvider initialItems={[]} initialLayers={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<SelectPositionProvider>
<LeafletRefsProvider initialLeafletRefs={{}}>
<QueryClientProvider client={queryClient}>
<AppStateProvider>
<ClusterRefProvider>
<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>
</LeafletRefsProvider>
</SelectPositionProvider>
</ItemsProvider>
<SelectPositionProvider>
<LeafletRefsProvider initialLeafletRefs={{}}>
<QueryClientProvider client={queryClient}>
<AppStateProvider>
<ClusterRefProvider>
<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>
</LeafletRefsProvider>
</SelectPositionProvider>
</FilterProvider>
</LayersProvider>
</ItemsProvider>
</TagsProvider>
</PermissionsProvider>
)

View File

@ -7,8 +7,9 @@ import {
useIsLayerVisible,
useIsGroupTypeVisible,
useVisibleGroupType,
useAllVisibleLayersInitialized,
} from '#components/Map/hooks/useFilter'
import { useItems, useAllItemsLoaded } from '#components/Map/hooks/useItems'
import { useItems } 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'
@ -44,7 +45,8 @@ export const PopupView = ({ children }: { children?: React.ReactNode }) => {
const leafletRefs = useLeafletRefs()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
const allVisibleLayersInitialized = useAllVisibleLayersInitialized()
const setMarkerClicked = useSetMarkerClicked()
const selectPosition = useSelectPosition()
@ -103,7 +105,7 @@ export const PopupView = ({ children }: { children?: React.ReactNode }) => {
})
}
if (allTagsLoaded && allItemsLoaded) {
if (allTagsLoaded && allVisibleLayersInitialized) {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase()) &&

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddTag } from './hooks/useTags'
@ -43,52 +43,98 @@ export const Layer = ({
const [newTagsToAdd] = useState<Tag[]>([])
const [tagsReady] = useState<boolean>(false)
const initializeWithData = useCallback(() => {
if (!data) return
setItemsData({
data,
children,
name,
menuIcon,
menuText,
menuColor,
markerIcon,
markerShape,
markerDefaultColor,
markerDefaultColor2,
api,
itemType,
userProfileLayer,
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
public_edit_items,
listed,
})
}, [
api,
children,
customEditLink,
customEditParameter,
data,
itemType,
listed,
markerDefaultColor,
markerDefaultColor2,
markerIcon,
markerShape,
menuColor,
menuIcon,
menuText,
name,
// eslint-disable-next-line camelcase
public_edit_items,
setItemsData,
userProfileLayer,
])
const initializeWithApi = useCallback(() => {
if (!api) return
setItemsApi({
data,
children,
name,
menuIcon,
menuText,
menuColor,
markerIcon,
markerShape,
markerDefaultColor,
markerDefaultColor2,
api,
itemType,
userProfileLayer,
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
public_edit_items,
listed,
})
}, [
api,
children,
customEditLink,
customEditParameter,
data,
itemType,
listed,
markerDefaultColor,
markerDefaultColor2,
markerIcon,
markerShape,
menuColor,
menuIcon,
menuText,
name,
// eslint-disable-next-line camelcase
public_edit_items,
setItemsApi,
userProfileLayer,
])
useEffect(() => {
data &&
setItemsData({
data,
children,
name,
menuIcon,
menuText,
menuColor,
markerIcon,
markerShape,
markerDefaultColor,
markerDefaultColor2,
api,
itemType,
userProfileLayer,
// Can we just use editCallback for all cases?
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
public_edit_items,
listed,
})
api &&
setItemsApi({
data,
children,
name,
menuIcon,
menuText,
menuColor,
markerIcon,
markerShape,
markerDefaultColor,
markerDefaultColor2,
api,
itemType,
userProfileLayer,
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
public_edit_items,
listed,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, api])
if (data) initializeWithData()
if (api) initializeWithApi()
}, [data, api, initializeWithData, initializeWithApi])
useEffect(() => {
if (tagsReady) {

View File

@ -3,7 +3,7 @@
import SVG from 'react-inlinesvg'
import PlusSVG from '#assets/plus.svg'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useLayers } from '#components/Map/hooks/useItems'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
export default function AddButton({

View File

@ -3,7 +3,7 @@ import SVG from 'react-inlinesvg'
import LayerSVG from '#assets/layer.svg'
import { useIsLayerVisible, useToggleVisibleLayer } from '#components/Map/hooks/useFilter'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useLayers } from '#components/Map/hooks/useItems'
export function LayerControl({ expandLayerControl = false }: { expandLayerControl: boolean }) {
const [open, setOpen] = useState(expandLayerControl)

View File

@ -24,7 +24,7 @@ import {
useResetFilterTags,
useToggleVisibleLayer,
} from './hooks/useFilter'
import { useLayers } from './hooks/useLayers'
import { useLayers } from './hooks/useItems'
import { useLeafletRefs } from './hooks/useLeafletRefs'
import { usePopupForm } from './hooks/usePopupForm'
import {
@ -44,6 +44,7 @@ import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson'
import { LayerProps } from '#types/LayerProps'
export function UtopiaMapInner({
children,
@ -85,9 +86,8 @@ export function UtopiaMapInner({
useTheme(defaultTheme)
useEffect(() => {
layers.forEach((layer) => addVisibleLayer(layer))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layers])
layers.forEach((layer: LayerProps) => addVisibleLayer(layer))
}, [addVisibleLayer, layers])
const setAppState = useSetAppState()

View File

@ -7,7 +7,7 @@
import { useCallback, useReducer, createContext, useContext, useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLayers } from './useLayers'
import { useLayers, useLayerState } from './useItems'
import useWindowDimensions from './useWindowDimension'
import type { LayerProps } from '#types/LayerProps'
@ -350,3 +350,12 @@ export const useVisibleGroupType = (): UseFilterManagerResult['visibleGroupTypes
const { visibleGroupTypes } = useContext(FilterContext)
return visibleGroupTypes
}
export const useAllVisibleLayersInitialized = (): boolean => {
const { visibleLayers } = useContext(FilterContext)
const layers = useLayerState()
return visibleLayers.every((layer) => {
const foundLayer = layers.find((l) => l.props.name === layer.name)
return foundLayer ? foundLayer.isInitialized : false
})
}

View File

@ -5,15 +5,24 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-misused-promises */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { useCallback, useReducer, createContext, useContext } from 'react'
import { toast } from 'react-toastify'
import { useAddLayer } from './useLayers'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
type LayerState = {
props: LayerProps
isInitialized: boolean
}[]
interface State {
layers: LayerState
items: Item[]
}
type ActionType =
| { type: 'ADD_LAYER'; layer: LayerProps; items: Item[] }
| { type: 'ADD'; item: Item }
| { type: 'UPDATE'; item: Item }
| { type: 'REMOVE'; item: Item }
@ -22,6 +31,7 @@ type ActionType =
type UseItemManagerResult = ReturnType<typeof useItemsManager>
const ItemContext = createContext<UseItemManagerResult>({
layers: [],
items: [],
addItem: () => {},
updateItem: () => {},
@ -29,10 +39,13 @@ const ItemContext = createContext<UseItemManagerResult>({
resetItems: () => {},
setItemsApi: () => {},
setItemsData: () => {},
allItemsLoaded: false,
})
function useItemsManager(initialItems: Item[]): {
function useItemsManager(
initialItems: Item[],
initialLayers: LayerState,
): {
layers: LayerState
items: Item[]
addItem: (item: Item) => void
updateItem: (item: Item) => void
@ -40,39 +53,62 @@ function useItemsManager(initialItems: Item[]): {
resetItems: (layer: LayerProps) => void
setItemsApi: (layer: LayerProps) => void
setItemsData: (layer: LayerProps) => void
allItemsLoaded: boolean
} {
const addLayer = useAddLayer()
const [allItemsLoaded, setallItemsLoaded] = useState<boolean>(false)
const [items, dispatch] = useReducer((state: Item[], action: ActionType) => {
switch (action.type) {
case 'ADD':
// eslint-disable-next-line no-case-declarations
const exist = state.find((item) => item.id === action.item.id)
if (!exist) {
return [...state, action.item]
} else return state
case 'UPDATE':
return state.map((item) => {
if (item.id === action.item.id) {
return action.item
const [{ items, layers }, dispatch] = useReducer(
(state: State, action: ActionType) => {
switch (action.type) {
case 'ADD_LAYER':
return {
layers: [
...state.layers,
{
props: action.layer,
isInitialized: true,
},
],
items: [
...state.items,
...action.items.map((item) => ({ ...item, layer: action.layer })),
],
}
return item
})
case 'REMOVE':
return state.filter((item) => item !== action.item)
case 'RESET':
return state.filter((item) => item.layer?.name !== action.layer.name)
default:
throw new Error()
}
}, initialItems)
case 'ADD':
// eslint-disable-next-line no-case-declarations
const exist = state.items.find((item) => item.id === action.item.id)
if (!exist) {
return {
...state,
items: [...state.items, action.item],
}
} else return state
case 'UPDATE':
return {
...state,
items: state.items.map((item) => {
if (item.id === action.item.id) {
return action.item
}
return item
}),
}
case 'REMOVE':
return {
...state,
items: state.items.filter((item) => item !== action.item),
}
case 'RESET':
return {
...state,
items: state.items.filter((item) => item.layer?.name !== action.layer.name),
}
default:
throw new Error()
}
},
{ items: initialItems, layers: initialLayers } as State,
)
const setItemsApi = useCallback(async (layer: LayerProps) => {
addLayer(layer)
const result = await toast.promise(layer.api!.getItems(), {
const items = await toast.promise(layer.api!.getItems(), {
pending: `loading ${layer.name} ...`,
success: `${layer.name} loaded`,
error: {
@ -81,22 +117,12 @@ function useItemsManager(initialItems: Item[]): {
},
},
})
result.map((item) => {
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})
setallItemsLoaded(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
dispatch({ type: 'ADD_LAYER', layer, items })
}, [])
const setItemsData = useCallback((layer: LayerProps) => {
addLayer(layer)
layer.data?.map((item) => {
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})
setallItemsLoaded(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
if (!layer.data) return
dispatch({ type: 'ADD_LAYER', layer, items: layer.data })
}, [])
const addItem = useCallback(async (item: Item) => {
@ -129,21 +155,24 @@ function useItemsManager(initialItems: Item[]): {
return {
items,
layers,
updateItem,
addItem,
removeItem,
resetItems,
setItemsApi,
setItemsData,
allItemsLoaded,
}
}
export const ItemsProvider: React.FunctionComponent<{
initialItems: Item[]
initialLayers: LayerState
children?: React.ReactNode
}> = ({ initialItems, children }) => (
<ItemContext.Provider value={useItemsManager(initialItems)}>{children}</ItemContext.Provider>
}> = ({ initialItems, initialLayers, children }) => (
<ItemContext.Provider value={useItemsManager(initialItems, initialLayers)}>
{children}
</ItemContext.Provider>
)
export const useItems = (): Item[] => {
@ -181,7 +210,12 @@ export const useSetItemsData = (): UseItemManagerResult['setItemsData'] => {
return setItemsData
}
export const useAllItemsLoaded = (): UseItemManagerResult['allItemsLoaded'] => {
const { allItemsLoaded } = useContext(ItemContext)
return allItemsLoaded
export const useLayers = (): LayerProps[] => {
const { layers } = useContext(ItemContext)
return layers.map((layer) => layer.props)
}
export const useLayerState = (): LayerState => {
const { layers } = useContext(ItemContext)
return layers
}

View File

@ -1,60 +0,0 @@
import { useCallback, useReducer, createContext, useContext } from 'react'
import type { LayerProps } from '#types/LayerProps'
interface ActionType {
type: 'ADD LAYER'
layer: LayerProps
}
type UseItemManagerResult = ReturnType<typeof useLayerManager>
const LayerContext = createContext<UseItemManagerResult>({
layers: [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
addLayer: () => {},
})
function useLayerManager(initialLayers: LayerProps[]): {
layers: LayerProps[]
addLayer: (layer: LayerProps) => void
} {
const [layers, dispatch] = useReducer((state: LayerProps[], action: ActionType) => {
switch (action.type) {
case 'ADD LAYER':
// eslint-disable-next-line no-case-declarations
const exist = state.find((layer) => layer.name === action.layer.name)
if (!exist) {
return [...state, action.layer]
} else return state
default:
throw new Error()
}
}, initialLayers)
const addLayer = useCallback((layer: LayerProps) => {
dispatch({
type: 'ADD LAYER',
layer,
})
}, [])
return { layers, addLayer }
}
export const LayersProvider: React.FunctionComponent<{
initialLayers: LayerProps[]
children?: React.ReactNode
}> = ({ initialLayers, children }: { initialLayers: LayerProps[]; children?: React.ReactNode }) => (
<LayerContext.Provider value={useLayerManager(initialLayers)}>{children}</LayerContext.Provider>
)
export const useLayers = (): LayerProps[] => {
const { layers } = useContext(LayerContext)
return layers
}
export const useAddLayer = (): UseItemManagerResult['addLayer'] => {
const { addLayer } = useContext(LayerContext)
return addLayer
}

View File

@ -1,15 +1,18 @@
import { useAuth } from '#components/Auth/useAuth'
import { useItems, useAllItemsLoaded } from './useItems'
import { useItems, useLayerState } from './useItems'
export const useMyProfile = () => {
const items = useItems()
const allItemsLoaded = useAllItemsLoaded()
const layers = useLayerState()
const user = useAuth().user
// allItemsLoaded is not reliable, so we check if items.length > 0
const isMyProfileLoaded = allItemsLoaded && items.length > 0 && !!user
const isUserProfileLayerLoaded = layers.some(
(layer) => layer.props.userProfileLayer && layer.isInitialized,
)
const isMyProfileLoaded = isUserProfileLayerLoaded && !!user
// Find the user's profile item
const myProfile = items.find(

View File

@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '#components/Auth/useAuth'
import { useItems, useUpdateItem, useAddItem } from '#components/Map/hooks/useItems'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useLayers } from '#components/Map/hooks/useItems'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useAddTag, useGetItemTags, useTags } from '#components/Map/hooks/useTags'
import { MapOverlayPage } from '#components/Templates'

View File

@ -14,7 +14,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useClusterRef } from '#components/Map/hooks/useClusterRef'
import { useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useLayers } from '#components/Map/hooks/useItems'
import { useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useSelectPosition, useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'

View File

@ -11,7 +11,7 @@ import { useAuth } from '#components/Auth/useAuth'
import { TextInput } from '#components/Input'
import { useFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem } from '#components/Map/hooks/useItems'
import { useLayers } from '#components/Map/hooks/useLayers'
import { useLayers } from '#components/Map/hooks/useItems'
import { useAddTag, useGetItemTags, useTags } from '#components/Map/hooks/useTags'
import { Control } from '#components/Map/Subcomponents/Controls/Control'
import { SearchControl } from '#components/Map/Subcomponents/Controls/SearchControl'