mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
dynamic router and context loading only if needed
This commit is contained in:
parent
da7c3b3734
commit
30d96bfd91
@ -1,71 +1,21 @@
|
||||
import * as React from 'react'
|
||||
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 { 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 { ContextWrapper } from './ContextWrapper';
|
||||
|
||||
|
||||
export function AppShell({ appName, children, assetsApi, userType }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi, userType: string }) {
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<PermissionsProvider initialPermissions={[]}>
|
||||
<TagsProvider initialTags={[]}>
|
||||
<LayersProvider initialLayers={[]}>
|
||||
<FilterProvider initialTags={[]}>
|
||||
<ItemsProvider initialItems={[]}>
|
||||
<SelectPositionProvider>
|
||||
<LeafletRefsProvider initialLeafletRefs={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<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>
|
||||
|
||||
<ContextWrapper>
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<SetAssetsApi assetsApi={assetsApi} />
|
||||
<NavBar userType={userType} appName={appName}></NavBar>
|
||||
<div id="app-content" className="tw-flex-grow">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ContextWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
98
src/Components/AppShell/ContextWrapper.tsx
Normal file
98
src/Components/AppShell/ContextWrapper.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -1,159 +1,13 @@
|
||||
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 { 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);
|
||||
}
|
||||
}
|
||||
import { UtopiaMapProps } from "../../types";
|
||||
import { ContextWrapper } from "../AppShell/ContextWrapper";
|
||||
import { UtopiaMapInner } from "./UtopiaMapInner";
|
||||
|
||||
function UtopiaMap(props: UtopiaMapProps) {
|
||||
return (
|
||||
<Router>
|
||||
<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></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='© <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>
|
||||
<ContextWrapper>
|
||||
<UtopiaMapInner {...props} />
|
||||
</ContextWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { UtopiaMap };
|
||||
export { UtopiaMap };
|
||||
|
||||
131
src/Components/Map/UtopiaMapInner.tsx
Normal file
131
src/Components/Map/UtopiaMapInner.tsx
Normal 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='© <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>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user