individual item URLS basics

This commit is contained in:
Anton 2023-09-14 08:31:41 +02:00
parent 1f675ccf3c
commit 58380f6b77
7 changed files with 193 additions and 56 deletions

View File

@ -1,12 +1,15 @@
import * as React from 'react' import * as React from 'react'
import { Marker, Tooltip } from 'react-leaflet' import { Marker, Tooltip, useMap, useMapEvents } from 'react-leaflet'
import { Item, LayerProps } from '../../types' import { Item, LayerProps } from '../../types'
import MarkerIconFactory from '../../Utils/MarkerIconFactory' import MarkerIconFactory from '../../Utils/MarkerIconFactory'
import { ItemViewPopup } from './Subcomponents/ItemViewPopup' import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import { useItems, useResetItems, useSetItemsApi, useSetItemsData } from './hooks/useItems' import { useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup' import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { useFilterTags, useSearchPhrase } from './hooks/useFilter' import { useFilterTags, useSearchPhrase } from './hooks/useFilter'
import { useGetItemTags } from './hooks/useTags'
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
import { Popup } from 'leaflet'
export const Layer = (props: LayerProps) => { export const Layer = (props: LayerProps) => {
@ -18,32 +21,63 @@ export const Layer = (props: LayerProps) => {
const items = useItems(); const items = useItems();
const setItemsApi = useSetItemsApi(); const setItemsApi = useSetItemsApi();
const setItemsData = useSetItemsData(); const setItemsData = useSetItemsData();
const getItemTags = useGetItemTags();
const resetItems = useResetItems(); const addMarker = useAddMarker();
const addPopup = useAddPopup();
const leafletRefs = useLeafletRefs();
const searchPhrase = useSearchPhrase(); const searchPhrase = useSearchPhrase();
const map = useMap();
useEffect(() => { useEffect(() => {
resetItems(props);
props.data && setItemsData(props); props.data && setItemsData(props);
props.api && setItemsApi(props); props.api && setItemsApi(props);
}, [props.data, props.api]) }, [props.data, props.api])
const openPopup = () => {
if (window.location.pathname.split("/")[1] == props.name) {
const id = window.location.pathname.split("/")[2]
const marker = leafletRefs[id]?.marker;
if (marker && marker != null) {
marker !== null && props.clusterRef?.current?.zoomToShowLayer(marker, () => {
marker.openPopup();
});
}
}
}
useMapEvents({
popupopen: (e) => {
const item = Object.entries(leafletRefs).find(r => r[1].popup == e.popup)?.[1].item;
if(item?.layer?.name == props.name )
window.history.pushState({},"",`/${props.name}/${item.id}`)
},
})
useEffect(() => {
openPopup();
}, [leafletRefs])
return ( return (
<> <>
{items && {items &&
items. items.
filter(item => item.layer?.name === props.name)?. filter(item => item.layer?.name === props.name)?.
filter(item => filter(item =>
// filterTags.length == 0 ? item : item.tags?.some(tag => filterTags.some(filterTag => filterTag.id === tag.id)))?. filterTags.length == 0 ? item : filterTags.every(tag => getItemTags(item).some(filterTag => filterTag.id === tag.id)))?.
filterTags.length == 0 ? item : filterTags.every(tag => item.tags?.some(filterTag => filterTag.id === tag.id)))?.
filter(item => { filter(item => {
return searchPhrase === '' return searchPhrase === ''
? item : ? item :
item.name.toLowerCase().includes(searchPhrase.toLowerCase()) item.name.toLowerCase().includes(searchPhrase.toLowerCase())
}). }).
map((item: Item) => { map((item: Item) => {
const tags = item.tags; const tags = getItemTags(item);
let color1 = "#666"; let color1 = "#666";
let color2 = "RGBA(35, 31, 32, 0.2)"; let color2 = "RGBA(35, 31, 32, 0.2)";
@ -54,17 +88,26 @@ export const Layer = (props: LayerProps) => {
color2 = tags[1].color; color2 = tags[1].color;
} }
return ( return (
<Marker icon={MarkerIconFactory(props.markerShape, color1, color2, props.markerIcon)} key={item.id} position={[item.position.coordinates[1], item.position.coordinates[0]]}> <Marker ref={(r) => {
if (!(item.id in leafletRefs))
r && addMarker(item, r);
}} icon={MarkerIconFactory(props.markerShape, color1, color2, props.markerIcon)} key={item.id} position={[item.position.coordinates[1], item.position.coordinates[0]]}>
{ {
(props.children && React.Children.toArray(props.children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ? (props.children && React.Children.toArray(props.children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ?
React.Children.toArray(props.children).map((child) => React.Children.toArray(props.children).map((child) =>
React.isValidElement(child) && child.props.__TYPE === "ItemView" ? React.isValidElement(child) && child.props.__TYPE === "ItemView" ?
<ItemViewPopup key={item.id} item={item} setItemFormPopup={props.setItemFormPopup} >{child}</ItemViewPopup> <ItemViewPopup ref={(r) => {
if (!(item.id in leafletRefs))
r && addPopup(item, r as Popup);
}} key={item.id + item.name} item={item} setItemFormPopup={props.setItemFormPopup} >{child}</ItemViewPopup>
: "" : ""
) )
: :
<> <>
<ItemViewPopup item={item} setItemFormPopup={props.setItemFormPopup} /> <ItemViewPopup key={item.id + item.name} ref={(r) => {
if (!(item.id in leafletRefs))
r && addPopup(item, r as Popup);
}} item={item} setItemFormPopup={props.setItemFormPopup} />
</>) </>)
} }
<Tooltip offset={[0, -38]} direction='top'>{item.name}</Tooltip> <Tooltip offset={[0, -38]} direction='top'>{item.name}</Tooltip>

View File

@ -5,7 +5,7 @@ import { ItemFormPopupProps } from "../ItemFormPopup";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import { Item } from "../../../../types"; import { Item } from "../../../../types";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useHasUserPermission, usePermissions } from "../../hooks/usePermissions"; import { useHasUserPermission } from "../../hooks/usePermissions";
@ -19,7 +19,6 @@ export function HeaderView({ item, setItemFormPopup }: {
const map = useMap(); const map = useMap();
const hasUserPermission = useHasUserPermission(); const hasUserPermission = useHasUserPermission();
const permissions = usePermissions();
const removeItemFromMap = async (event: React.MouseEvent<HTMLElement>) => { const removeItemFromMap = async (event: React.MouseEvent<HTMLElement>) => {
@ -48,11 +47,6 @@ export function HeaderView({ item, setItemFormPopup }: {
setItemFormPopup({ position: new LatLng(item.position.coordinates[1], item.position.coordinates[0]), layer: item.layer!, item: item, setItemFormPopup: setItemFormPopup }) setItemFormPopup({ position: new LatLng(item.position.coordinates[1], item.position.coordinates[0]), layer: item.layer!, item: item, setItemFormPopup: setItemFormPopup })
} }
console.log(item.layer.api.collectionName);
console.log(permissions);
console.log( hasUserPermission(item.api?.collectionName!,"update") );
return ( return (
<div className='tw-grid tw-grid-cols-6 tw-pb-2'> <div className='tw-grid tw-grid-cols-6 tw-pb-2'>

View File

@ -2,9 +2,9 @@ import * as React from 'react'
import { Item } from '../../../../types' import { Item } from '../../../../types'
import { useAddTag, useTags } from '../../hooks/useTags'; import { useAddTag, useTags } from '../../hooks/useTags';
import reactStringReplace from 'react-string-replace'; import reactStringReplace from 'react-string-replace';
import { useAddFilterTag, useResetFilterTags } from '../../hooks/useFilter'; import { useAddFilterTag } from '../../hooks/useFilter';
import { hashTagRegex } from '../../../../Utils/HashTagRegex'; import { hashTagRegex } from '../../../../Utils/HashTagRegex';
import { fixUrls, mailRegex, urlRegex } from '../../../../Utils/ReplaceURLs'; import { fixUrls, mailRegex } from '../../../../Utils/ReplaceURLs';
import { useMap } from 'react-leaflet'; import { useMap } from 'react-leaflet';
import { randomColor } from '../../../../Utils/RandomColor'; import { randomColor } from '../../../../Utils/RandomColor';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
@ -16,7 +16,6 @@ export const TextView = ({ item }: { item?: Item }) => {
const groupRef = useRef(null); const groupRef = useRef(null);
const addFilterTag = useAddFilterTag(); const addFilterTag = useAddFilterTag();
const resetFilterTags = useResetFilterTags();
const map = useMap(); const map = useMap();

View File

@ -1,21 +1,21 @@
import * as React from 'react' import * as React from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet' import { Popup as LeafletPopup} from 'react-leaflet'
import { Item } from '../../../types' import { Item } from '../../../types'
import { ItemFormPopupProps } from './ItemFormPopup' import { ItemFormPopupProps } from './ItemFormPopup'
import { HeaderView } from './ItemPopupComponents/HeaderView' import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView' import { TextView } from './ItemPopupComponents/TextView'
export interface ItemViewPopupProps { export interface ItemViewPopupProps {
item: Item, item: Item,
children?: React.ReactNode; children?: React.ReactNode;
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>> setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
} }
export const ItemViewPopup = (props: ItemViewPopupProps) => { export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: any) => {
const item: Item = props.item;
return ( return (
<LeafletPopup maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}> <LeafletPopup ref={ref} maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
<div className='tw-bg-base-100 tw-text-base-content'> <div className='tw-bg-base-100 tw-text-base-content'>
<HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} /> <HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} />
<div className='tw-overflow-y-auto tw-max-h-72'> <div className='tw-overflow-y-auto tw-max-h-72'>
@ -35,5 +35,5 @@ export const ItemViewPopup = (props: ItemViewPopupProps) => {
</div> </div>
</LeafletPopup> </LeafletPopup>
) )
} })

View File

@ -14,6 +14,7 @@ import { LayersProvider } from "./hooks/useLayers";
import { FilterProvider } from "./hooks/useFilter"; import { FilterProvider } from "./hooks/useFilter";
import { FilterControl } from "./Subcomponents/FilterControl"; import { FilterControl } from "./Subcomponents/FilterControl";
import { PermissionsProvider } from "./hooks/usePermissions"; import { PermissionsProvider } from "./hooks/usePermissions";
import { LeafletRefsProvider } from "./hooks/useLeafletRefs";
export interface MapEventListenerProps { export interface MapEventListenerProps {
@ -25,7 +26,7 @@ export interface MapEventListenerProps {
function MapEventListener(props: MapEventListenerProps) { function MapEventListener(props: MapEventListenerProps) {
useMapEvents({ useMapEvents({
click: (e) => { click: (e) => {
window.history.pushState({}, "", "/");
console.log(e.latlng.lat + ',' + e.latlng.lng); console.log(e.latlng.lat + ',' + e.latlng.lng);
if (props.selectNewItemPosition != null) { if (props.selectNewItemPosition != null) {
props.setItemFormPopup({ layer: props.selectNewItemPosition, position: e.latlng }) props.setItemFormPopup({ layer: props.selectNewItemPosition, position: e.latlng })
@ -34,8 +35,8 @@ function MapEventListener(props: MapEventListenerProps) {
}, },
resize: () => { resize: () => {
console.log("resize"); console.log("resize");
},
}
}) })
return null return null
} }
@ -53,6 +54,7 @@ function UtopiaMap({
const [selectNewItemPosition, setSelectNewItemPosition] = useState<LayerProps | null>(null); const [selectNewItemPosition, setSelectNewItemPosition] = useState<LayerProps | null>(null);
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null); const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
const clusterRef = React.useRef();
return ( return (
@ -61,17 +63,18 @@ function UtopiaMap({
<PermissionsProvider initialPermissions={[]}> <PermissionsProvider initialPermissions={[]}>
<FilterProvider initialTags={[]}> <FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}> <ItemsProvider initialItems={[]}>
<LeafletRefsProvider initialLeafletRefs={{}}>
<div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}> <div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}> <MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}>
<FilterControl></FilterControl> <FilterControl></FilterControl>
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" /> url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
<MarkerClusterGroup showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}> <MarkerClusterGroup ref={clusterRef} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{ {
React.Children.toArray(children).map((child) => React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null }>(child) ? React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null, clusterRef: React.MutableRefObject<undefined> }>(child) ?
React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup }) : child React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup, clusterRef: clusterRef }) : child
) )
} }
</MarkerClusterGroup> </MarkerClusterGroup>
@ -88,6 +91,7 @@ function UtopiaMap({
</div> </div>
} }
</div> </div>
</LeafletRefsProvider>
</ItemsProvider> </ItemsProvider>
</FilterProvider> </FilterProvider>
</PermissionsProvider> </PermissionsProvider>

View File

@ -0,0 +1,95 @@
import { useCallback, useReducer, createContext, useContext, useEffect } from "react";
import * as React from "react";
import { Item } from "../../../types";
import { Marker, Popup } from "leaflet";
type LeafletRef = {
item: Item,
marker: Marker,
popup: Popup
}
type ActionType =
| { type: "ADD_MARKER"; item: Item, marker: Marker }
| { type: "ADD_POPUP"; item: Item, popup: Popup }
;
type UseLeafletRefsManagerResult = ReturnType<typeof useLeafletRefsManager>;
const LeafletRefsContext = createContext<UseLeafletRefsManagerResult>({
leafletRefs: {},
addMarker: () => { },
addPopup: () => { },
});
function useLeafletRefsManager(initialLeafletRefs: {}): {
leafletRefs: Record<string,LeafletRef>;
addMarker: (item: Item, marker: Marker) => void;
addPopup: (item: Item, popup: Popup) => void;
} {
const [leafletRefs, dispatch] = useReducer((state: Record<string,LeafletRef>, action: ActionType) => {
switch (action.type) {
case "ADD_MARKER":
return {
...state,
[action.item.id]: { ...state[action.item.id], marker: action.marker, item: action.item }}
case "ADD_POPUP":
return {
...state,
[action.item.id]: { ...state[action.item.id], popup: action.popup, item: action.item }}
default:
throw new Error();
}
}, initialLeafletRefs);
const addMarker = useCallback((item: Item, marker: Marker) => {
dispatch({
type: "ADD_MARKER",
item,
marker
});
}, []);
const addPopup = useCallback((item: Item, popup: Popup) => {
dispatch({
type: "ADD_POPUP",
item,
popup
});
}, []);
return { leafletRefs, addMarker, addPopup };
}
export const LeafletRefsProvider: React.FunctionComponent<{
initialLeafletRefs: {}, children?: React.ReactNode
}> = ({ initialLeafletRefs, children }) => (
<LeafletRefsContext.Provider value={useLeafletRefsManager(initialLeafletRefs)}>
{children}
</LeafletRefsContext.Provider>
);
export const useLeafletRefs = (): Record<string,LeafletRef> => {
const { leafletRefs } = useContext(LeafletRefsContext);
return leafletRefs;
};
export const useAddMarker = (): UseLeafletRefsManagerResult["addMarker"] => {
const { addMarker } = useContext(LeafletRefsContext);
return addMarker;
};
export const useAddPopup = (): UseLeafletRefsManagerResult["addPopup"] => {
const { addPopup } = useContext(LeafletRefsContext);
return addPopup;
};

View File

@ -1,4 +1,4 @@
import { LatLng } from "leaflet"; import { LatLng, LatLngTuple, latLng } from "leaflet";
import { ItemFormPopupProps } from "./Components/Map/Subcomponents/ItemFormPopup"; import { ItemFormPopupProps } from "./Components/Map/Subcomponents/ItemFormPopup";
export interface UtopiaMapProps { export interface UtopiaMapProps {
@ -22,7 +22,8 @@ export interface LayerProps {
markerDefaultColor: string, markerDefaultColor: string,
api?: ItemsApi<any>, api?: ItemsApi<any>,
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>, setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>,
itemFormPopup?: ItemFormPopupProps | null itemFormPopup?: ItemFormPopupProps | null,
clusterRef?: React.MutableRefObject<any>
} }
export class Item { export class Item {
@ -36,6 +37,7 @@ export class Item {
end?: string; end?: string;
api?: ItemsApi<any>; api?: ItemsApi<any>;
tags?: Tag[]; tags?: Tag[];
layer?: LayerProps;
[key: string]: any; [key: string]: any;
constructor(id:string,name:string,text:string,position:Geometry, layer?: LayerProps, api?: ItemsApi<any>){ constructor(id:string,name:string,text:string,position:Geometry, layer?: LayerProps, api?: ItemsApi<any>){
this.id = id; this.id = id;