diff --git a/lib/src/Components/AppShell/ContextWrapper.tsx b/lib/src/Components/AppShell/ContextWrapper.tsx index c4245b52..93fc8429 100644 --- a/lib/src/Components/AppShell/ContextWrapper.tsx +++ b/lib/src/Components/AppShell/ContextWrapper.tsx @@ -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 ( - + - - - - - - - - - - {children} - - - - - - - - + + + + + + + + + {children} + + + + + + + - + ) diff --git a/lib/src/Components/Item/PopupView.tsx b/lib/src/Components/Item/PopupView.tsx index 6eea6e43..5495cfc5 100644 --- a/lib/src/Components/Item/PopupView.tsx +++ b/lib/src/Components/Item/PopupView.tsx @@ -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()) && diff --git a/lib/src/Components/Map/Layer.tsx b/lib/src/Components/Map/Layer.tsx index 6d54af71..5ae163c4 100644 --- a/lib/src/Components/Map/Layer.tsx +++ b/lib/src/Components/Map/Layer.tsx @@ -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([]) const [tagsReady] = useState(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) { diff --git a/lib/src/Components/Map/Subcomponents/AddButton.tsx b/lib/src/Components/Map/Subcomponents/AddButton.tsx index 5781a1ab..c6277b47 100644 --- a/lib/src/Components/Map/Subcomponents/AddButton.tsx +++ b/lib/src/Components/Map/Subcomponents/AddButton.tsx @@ -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({ diff --git a/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx b/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx index 10180de0..016678e3 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/LayerControl.tsx @@ -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) diff --git a/lib/src/Components/Map/UtopiaMapInner.tsx b/lib/src/Components/Map/UtopiaMapInner.tsx index d197f92a..68a49177 100644 --- a/lib/src/Components/Map/UtopiaMapInner.tsx +++ b/lib/src/Components/Map/UtopiaMapInner.tsx @@ -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() diff --git a/lib/src/Components/Map/hooks/useFilter.tsx b/lib/src/Components/Map/hooks/useFilter.tsx index d7733f04..0bfa8a94 100644 --- a/lib/src/Components/Map/hooks/useFilter.tsx +++ b/lib/src/Components/Map/hooks/useFilter.tsx @@ -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 + }) +} diff --git a/lib/src/Components/Map/hooks/useItems.tsx b/lib/src/Components/Map/hooks/useItems.tsx index 0fb1af78..13f8126a 100644 --- a/lib/src/Components/Map/hooks/useItems.tsx +++ b/lib/src/Components/Map/hooks/useItems.tsx @@ -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 const ItemContext = createContext({ + layers: [], items: [], addItem: () => {}, updateItem: () => {}, @@ -29,10 +39,13 @@ const ItemContext = createContext({ 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(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 }) => ( - {children} +}> = ({ initialItems, initialLayers, children }) => ( + + {children} + ) 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 } diff --git a/lib/src/Components/Map/hooks/useLayers.tsx b/lib/src/Components/Map/hooks/useLayers.tsx deleted file mode 100644 index 963063b0..00000000 --- a/lib/src/Components/Map/hooks/useLayers.tsx +++ /dev/null @@ -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 - -const LayerContext = createContext({ - 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 }) => ( - {children} -) - -export const useLayers = (): LayerProps[] => { - const { layers } = useContext(LayerContext) - return layers -} - -export const useAddLayer = (): UseItemManagerResult['addLayer'] => { - const { addLayer } = useContext(LayerContext) - return addLayer -} diff --git a/lib/src/Components/Map/hooks/useMyProfile.ts b/lib/src/Components/Map/hooks/useMyProfile.ts index e94c82ed..f8f2c05a 100644 --- a/lib/src/Components/Map/hooks/useMyProfile.ts +++ b/lib/src/Components/Map/hooks/useMyProfile.ts @@ -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( diff --git a/lib/src/Components/Profile/ProfileForm.tsx b/lib/src/Components/Profile/ProfileForm.tsx index b4d82862..62e5850b 100644 --- a/lib/src/Components/Profile/ProfileForm.tsx +++ b/lib/src/Components/Profile/ProfileForm.tsx @@ -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' diff --git a/lib/src/Components/Profile/ProfileView.tsx b/lib/src/Components/Profile/ProfileView.tsx index 498f0347..7f3eda93 100644 --- a/lib/src/Components/Profile/ProfileView.tsx +++ b/lib/src/Components/Profile/ProfileView.tsx @@ -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' diff --git a/lib/src/Components/Templates/OverlayItemsIndexPage.tsx b/lib/src/Components/Templates/OverlayItemsIndexPage.tsx index 094ec95f..5557c2fb 100644 --- a/lib/src/Components/Templates/OverlayItemsIndexPage.tsx +++ b/lib/src/Components/Templates/OverlayItemsIndexPage.tsx @@ -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'