dynamic router and context loading only if needed

This commit is contained in:
Anton Tranelis 2024-10-13 16:27:32 +02:00
parent da7c3b3734
commit 30d96bfd91
4 changed files with 247 additions and 214 deletions

View File

@ -1,71 +1,21 @@
import * as React from 'react' import * as React from 'react'
import NavBar from './NavBar' import NavBar from './NavBar'
import { BrowserRouter } from 'react-router-dom'
import { ToastContainer } from 'react-toastify'
import { QuestsProvider } from '../Gaming/hooks/useQuests'
import { AssetsProvider } from './hooks/useAssets'
import { SetAssetsApi } from './SetAssetsApi' import { SetAssetsApi } from './SetAssetsApi'
import { AssetsApi } from '../../types' import { AssetsApi } from '../../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { PermissionsProvider } from '../Map/hooks/usePermissions'
import { TagsProvider } from '../Map/hooks/useTags'
import { FilterProvider } from '../Map/hooks/useFilter'
import { ItemsProvider } from '../Map/hooks/useItems'
import { LayersProvider } from '../Map/hooks/useLayers'
import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
import { ClusterRefProvider } from '../Map/hooks/useClusterRef'
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { ContextWrapper } from './ContextWrapper';
export function AppShell({ appName, children, assetsApi, userType }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi, userType: string }) { export function AppShell({ appName, children, assetsApi, userType }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi, userType: string }) {
// Create a client
const queryClient = new QueryClient()
return ( return (
<BrowserRouter> <ContextWrapper>
<PermissionsProvider initialPermissions={[]}> <div className='tw-flex tw-flex-col tw-h-full'>
<TagsProvider initialTags={[]}> <SetAssetsApi assetsApi={assetsApi} />
<LayersProvider initialLayers={[]}> <NavBar userType={userType} appName={appName}></NavBar>
<FilterProvider initialTags={[]}> <div id="app-content" className="tw-flex-grow">
<ItemsProvider initialItems={[]}> {children}
<SelectPositionProvider> </div>
<LeafletRefsProvider initialLeafletRefs={{}}> </div>
<QueryClientProvider client={queryClient}> </ContextWrapper>
<AssetsProvider>
<ClusterRefProvider>
<SetAssetsApi assetsApi={assetsApi}></SetAssetsApi>
<QuestsProvider initialOpen={true}>
<ToastContainer position="top-right"
autoClose={2000}
hideProgressBar
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light" />
<div className='tw-flex tw-flex-col tw-h-full'>
<NavBar userType={userType} appName={appName}></NavBar>
<div id="app-content" className="tw-flex-grow">
{children}
</div>
</div>
</QuestsProvider>
</ClusterRefProvider>
</AssetsProvider>
</QueryClientProvider>
</LeafletRefsProvider>
</SelectPositionProvider>
</ItemsProvider>
</FilterProvider>
</LayersProvider>
</TagsProvider>
</PermissionsProvider>
</BrowserRouter>
) )
} }

View File

@ -0,0 +1,98 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ToastContainer } from 'react-toastify'
import { QuestsProvider } from '../Gaming/hooks/useQuests'
import { ClusterRefProvider } from '../Map/hooks/useClusterRef'
import { FilterProvider } from '../Map/hooks/useFilter'
import { ItemsProvider } from '../Map/hooks/useItems'
import { LayersProvider } from '../Map/hooks/useLayers'
import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
import { PermissionsProvider } from '../Map/hooks/usePermissions'
import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
import { TagsProvider } from '../Map/hooks/useTags'
import { AssetsProvider } from './hooks/useAssets'
import { useContext, createContext } from 'react';
import { BrowserRouter as Router, useLocation } from 'react-router-dom';
// Helper context to determine if the ContextWrapper is already present.
const ContextCheckContext = createContext(false);
export const ContextWrapper = ({ children }) => {
const isWrapped = useContext(ContextCheckContext);
// Check if we are already inside a Router
let location;
try {
location = useLocation();
} catch (e) {
location = null;
}
// Case 1: Only the Router is missing, but ContextWrapper is already provided
if (!location && isWrapped) {
console.log("Router is not present, but ContextWrapper is already provided. Wrapping with Router only.");
return (
<Router>
{children}
</Router>
);
}
// Case 2: Neither Router nor ContextWrapper is present
if (!location && !isWrapped) {
console.log("Router and ContextWrapper are not present. Wrapping both.");
return (
<Router>
<ContextCheckContext.Provider value={true}>
<Wrappers>
{children}
</Wrappers>
</ContextCheckContext.Provider>
</Router>
);
}
// Case 3: Only ContextWrapper is missing
if (location && !isWrapped) {
console.log("ContextWrapper is not present. Wrapping ContextWrapper.");
return (
<ContextCheckContext.Provider value={true}>
<Wrappers>{children}</Wrappers>
</ContextCheckContext.Provider>
);
}
// Case 4: Both Router and ContextWrapper are already present
console.log("Router and ContextWrapper are already present.");
return children;
};
export const Wrappers = ({ children }) => {
const queryClient = new QueryClient();
return (
<PermissionsProvider initialPermissions={[]}>
<TagsProvider initialTags={[]}>
<LayersProvider initialLayers={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<SelectPositionProvider>
<LeafletRefsProvider initialLeafletRefs={{}}>
<QueryClientProvider client={queryClient}>
<AssetsProvider>
<ClusterRefProvider>
<QuestsProvider initialOpen={true}>
{children}
</QuestsProvider>
</ClusterRefProvider>
</AssetsProvider>
</QueryClientProvider>
</LeafletRefsProvider>
</SelectPositionProvider>
</ItemsProvider>
</FilterProvider>
</LayersProvider>
</TagsProvider>
</PermissionsProvider>
);
};

View File

@ -1,158 +1,12 @@
import { TileLayer, MapContainer, useMapEvents, GeoJSON } from "react-leaflet"; import { UtopiaMapProps } from "../../types";
import "leaflet/dist/leaflet.css"; import { ContextWrapper } from "../AppShell/ContextWrapper";
import * as React from "react"; import { UtopiaMapInner } from "./UtopiaMapInner";
import { UtopiaMapProps } from "../../types"
import "./UtopiaMap.css"
import { LatLng } from "leaflet";
import MarkerClusterGroup from 'react-leaflet-cluster'
import AddButton from "./Subcomponents/AddButton";
import { useEffect, useState } from "react";
import { ItemFormPopupProps } from "./Subcomponents/ItemFormPopup";
import { SearchControl } from "./Subcomponents/Controls/SearchControl";
import { Control } from "./Subcomponents/Controls/Control";
import { BrowserRouter, Outlet, useLocation } from "react-router-dom";
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
import { useSelectPosition, useSetMapClicked, useSetSelectPosition } from "./hooks/useSelectPosition";
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
import { FilterControl } from "./Subcomponents/Controls/FilterControl";
import { LayerControl } from "./Subcomponents/Controls/LayerControl";
import { useLayers } from "./hooks/useLayers";
import { useAddVisibleLayer } from "./hooks/useFilter";
import { GratitudeControl } from "./Subcomponents/Controls/GratitudeControl";
import { SelectPosition } from "./Subcomponents/SelectPosition";
const mapDivRef = React.createRef();
export const Router = ({ children }) => {
let location;
try {
location = useLocation();
} catch (e) {
location = null;
}
if (!location) {
return (
<BrowserRouter>
{children}
</BrowserRouter>
);
}
return children;
};
function UtopiaMap({
height = "500px",
width = "100%",
center = [50.6, 9.5],
zoom = 10,
children,
geo,
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true
}
: UtopiaMapProps) {
function MapEventListener() {
useMapEvents({
click: (e) => {
resetMetaTags();
console.log(e.latlng.lat + ',' + e.latlng.lng);
selectNewItemPosition && setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup })
},
moveend: () => {
}
})
return null
}
const resetMetaTags = () => {
let params = new URLSearchParams(window.location.search);
if (!location.pathname.includes("/item/")) {
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
}
document.title = document.title.split("-")[0];
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
}
const selectNewItemPosition = useSelectPosition();
const setSelectNewItemPosition = useSetSelectPosition();
const setClusterRef = useSetClusterRef();
const clusterRef = useClusterRef();
const setMapClicked = useSetMapClicked();
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
const layers = useLayers();
const addVisibleLayer = useAddVisibleLayer();
useEffect(() => {
layers.map(l => addVisibleLayer(l))
}, [layers])
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {
if (feature.properties) {
layer.bindPopup(feature.properties.name);
}
}
function UtopiaMap(props: UtopiaMapProps) {
return ( return (
<Router> <ContextWrapper>
<div className={`tw-h-full ${(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}`}> <UtopiaMapInner {...props} />
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}> </ContextWrapper>
<Outlet></Outlet>
<Control position='topLeft' zIndex="1000" absolute>
<SearchControl />
<TagsControl />
</Control>
<Control position='bottomLeft' zIndex="999" absolute>
{/*{!embedded && (*/}
{/* <QuestControl></QuestControl>*/}
{/*)}*/}
{showFilterControl && <FilterControl />}
{/*todo: needed layer handling is located LayerControl*/}
{showLayerControl && <LayerControl></LayerControl>}
{showGratitudeControl && <GratitudeControl/>}
</Control>
<TileLayer
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
<MarkerClusterGroup ref={(r) => setClusterRef(r)} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{
React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null, clusterRef: React.MutableRefObject<undefined> }>(child) ?
React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup, clusterRef: clusterRef }) : child
)
}
</MarkerClusterGroup>
{geo && <GeoJSON data={geo} onEachFeature={onEachFeature} eventHandlers={{
click: (e) => {
selectNewItemPosition && e.layer!.closePopup();
selectNewItemPosition && setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup })
},
}} />}
<MapEventListener />
</MapContainer>
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
{selectNewItemPosition != null &&
<SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />
}
</div>
</Router>
); );
} }

View File

@ -0,0 +1,131 @@
import { TileLayer, MapContainer, useMapEvents, GeoJSON } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import * as React from "react";
import { UtopiaMapProps } from "../../types";
import "./UtopiaMap.css";
import { LatLng } from "leaflet";
import MarkerClusterGroup from 'react-leaflet-cluster';
import AddButton from "./Subcomponents/AddButton";
import { useEffect, useState } from "react";
import { ItemFormPopupProps } from "./Subcomponents/ItemFormPopup";
import { SearchControl } from "./Subcomponents/Controls/SearchControl";
import { Control } from "./Subcomponents/Controls/Control";
import { Outlet } from "react-router-dom";
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
import { useSelectPosition, useSetMapClicked, useSetSelectPosition } from "./hooks/useSelectPosition";
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
import { FilterControl } from "./Subcomponents/Controls/FilterControl";
import { LayerControl } from "./Subcomponents/Controls/LayerControl";
import { useLayers } from "./hooks/useLayers";
import { useAddVisibleLayer } from "./hooks/useFilter";
import { GratitudeControl } from "./Subcomponents/Controls/GratitudeControl";
import { SelectPosition } from "./Subcomponents/SelectPosition";
const mapDivRef = React.createRef();
export function UtopiaMapInner({
height = "500px",
width = "100%",
center = [50.6, 9.5],
zoom = 10,
children,
geo,
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true
}: UtopiaMapProps) {
// Hooks that rely on contexts, called after ContextWrapper is provided
const selectNewItemPosition = useSelectPosition();
const setSelectNewItemPosition = useSetSelectPosition();
const setClusterRef = useSetClusterRef();
const clusterRef = useClusterRef();
const setMapClicked = useSetMapClicked();
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
const layers = useLayers();
const addVisibleLayer = useAddVisibleLayer();
useEffect(() => {
layers.forEach(layer => addVisibleLayer(layer));
}, [layers]);
function MapEventListener() {
useMapEvents({
click: (e) => {
resetMetaTags();
console.log(e.latlng.lat + ',' + e.latlng.lng);
if (selectNewItemPosition) {
setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup });
}
},
moveend: () => { }
});
return null;
}
const resetMetaTags = () => {
let params = new URLSearchParams(window.location.search);
if (!window.location.pathname.includes("/item/")) {
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`);
}
document.title = document.title.split("-")[0];
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
};
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {
if (feature.properties) {
layer.bindPopup(feature.properties.name);
}
};
return (
<div className={`tw-h-full ${selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined}`}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}>
<Outlet />
<Control position="topLeft" zIndex="1000" absolute>
<SearchControl />
<TagsControl />
</Control>
<Control position="bottomLeft" zIndex="999" absolute>
{showFilterControl && <FilterControl />}
{showLayerControl && <LayerControl />}
{showGratitudeControl && <GratitudeControl />}
</Control>
<TileLayer
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png"
/>
<MarkerClusterGroup ref={(r) => setClusterRef(r)} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{
React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null, clusterRef: React.MutableRefObject<undefined> }>(child)
? React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup, clusterRef: clusterRef })
: child
)
}
</MarkerClusterGroup>
{geo && (
<GeoJSON
data={geo}
onEachFeature={onEachFeature}
eventHandlers={{
click: (e) => {
if (selectNewItemPosition) {
e.layer!.closePopup();
setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup });
}
},
}}
/>
)}
<MapEventListener />
</MapContainer>
<AddButton triggerAction={setSelectNewItemPosition} />
{selectNewItemPosition != null && <SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />}
</div>
);
}