update item position, item parents, focus item on profile load an open cluster

This commit is contained in:
Anton Tranelis 2024-03-25 01:06:47 +01:00
parent c77972a3be
commit 8484379113
13 changed files with 262 additions and 141 deletions

View File

@ -13,7 +13,8 @@ import { FilterProvider } from '../Map/hooks/useFilter'
import { ItemsProvider } from '../Map/hooks/useItems' import { ItemsProvider } from '../Map/hooks/useItems'
import { LayersProvider } from '../Map/hooks/useLayers' import { LayersProvider } from '../Map/hooks/useLayers'
import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs' import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
import { SelectPositionProvider } from '../Map/hooks/useSetItemPosition' import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
import { ClusterRefProvider } from '../Map/hooks/useClusterRef'
export function AppShell({ appName, nameWidth, children, assetsApi }: { appName: string, nameWidth?: number, children: React.ReactNode, assetsApi: AssetsApi }) { export function AppShell({ appName, nameWidth, children, assetsApi }: { appName: string, nameWidth?: number, children: React.ReactNode, assetsApi: AssetsApi }) {
@ -32,23 +33,25 @@ export function AppShell({ appName, nameWidth, children, assetsApi }: { appName:
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<AssetsProvider> <AssetsProvider>
<SetAssetsApi assetsApi={assetsApi}></SetAssetsApi> <ClusterRefProvider>
<QuestsProvider initialOpen={true}> <SetAssetsApi assetsApi={assetsApi}></SetAssetsApi>
<ToastContainer position="top-right" <QuestsProvider initialOpen={true}>
autoClose={2000} <ToastContainer position="top-right"
hideProgressBar autoClose={2000}
newestOnTop={false} hideProgressBar
closeOnClick newestOnTop={false}
rtl={false} closeOnClick
pauseOnFocusLoss rtl={false}
draggable pauseOnFocusLoss
pauseOnHover draggable
theme="light" /> pauseOnHover
<NavBar appName={appName} nameWidth={nameWidth}></NavBar> theme="light" />
<div id="app-content" className="tw-flex tw-!pl-[77px]"> <NavBar appName={appName} nameWidth={nameWidth}></NavBar>
{children} <div id="app-content" className="tw-flex tw-!pl-[77px]">
</div> {children}
</QuestsProvider> </div>
</QuestsProvider>
</ClusterRefProvider>
</AssetsProvider> </AssetsProvider>
</BrowserRouter> </BrowserRouter>
</QueryClientProvider> </QueryClientProvider>

View File

@ -6,7 +6,7 @@ import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems' import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ItemFormPopup } from './Subcomponents/ItemFormPopup' import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { useFilterTags, useIsLayerVisible } from './hooks/useFilter' import { useFilterTags, useIsLayerVisible, useResetFilterTags } from './hooks/useFilter'
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags' import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs' import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
import { Popup } from 'leaflet' import { Popup } from 'leaflet'
@ -15,6 +15,7 @@ import { getValue } from '../../Utils/GetValue'
import { hashTagRegex } from '../../Utils/HashTagRegex' import { hashTagRegex } from '../../Utils/HashTagRegex'
import { randomColor } from '../../Utils/RandomColor' import { randomColor } from '../../Utils/RandomColor'
import { encodeTag } from '../../Utils/FormatTags' import { encodeTag } from '../../Utils/FormatTags'
import { useSetMarkerClicked } from './hooks/useSelectPosition'
export const Layer = ({ export const Layer = ({
data, data,
@ -27,6 +28,7 @@ export const Layer = ({
markerShape = 'circle', markerShape = 'circle',
markerDefaultColor = '#777', markerDefaultColor = '#777',
api, api,
itemType,
itemNameField = 'name', itemNameField = 'name',
itemTextField = 'text', itemTextField = 'text',
itemAvatarField, itemAvatarField,
@ -53,12 +55,15 @@ export const Layer = ({
const addMarker = useAddMarker(); const addMarker = useAddMarker();
const addPopup = useAddPopup(); const addPopup = useAddPopup();
const leafletRefs = useLeafletRefs(); const leafletRefs = useLeafletRefs();
const resetFilterTags = useResetFilterTags();
let location = useLocation(); const location = useLocation();
const allTagsLoaded = useAllTagsLoaded(); const allTagsLoaded = useAllTagsLoaded();
const allItemsLoaded = useAllItemsLoaded(); const allItemsLoaded = useAllItemsLoaded();
const setMarkerClicked = useSetMarkerClicked();
const tags = useTags(); const tags = useTags();
const addTag = useAddTag(); const addTag = useAddTag();
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([]); const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([]);
@ -71,16 +76,16 @@ export const Layer = ({
useEffect(() => { useEffect(() => {
data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef }); data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemType, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef });
api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef }); api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemType, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef });
}, [data, api]) }, [data, api])
useMapEvents({ useMapEvents({
popupopen: (e) => { popupopen: (e) => {
const item = Object.entries(leafletRefs).find(r => r[1].popup == e.popup)?.[1].item; const item = Object.entries(leafletRefs).find(r => r[1].popup == e.popup)?.[1].item;
if (item?.layer?.name == name && window.location.pathname.split("/")[2] != item.id) { if (item?.layer?.name == name && window.location.pathname.split("/")[2] != item.id) {
let params = new URLSearchParams(window.location.search); let params = new URLSearchParams(window.location.search);
window.history.pushState({}, "", `/${name}/${item.id}`+ `${params.toString() !== "" ? `?${params}` : ""}`) window.history.pushState({}, "", `/${name}/${item.id}` + `${params.toString() !== "" ? `?${params}` : ""}`)
let title = ""; let title = "";
if (item.name) title = item.name; if (item.name) title = item.name;
else if (item.layer?.itemNameField) title = getValue(item, item.layer.itemNameField); else if (item.layer?.itemNameField) title = getValue(item, item.layer.itemNameField);
@ -98,8 +103,9 @@ export const Layer = ({
if (window.location.pathname.split("/")[2]) { if (window.location.pathname.split("/")[2]) {
const id = window.location.pathname.split("/")[2] const id = window.location.pathname.split("/")[2]
const marker = leafletRefs[id]?.marker; const marker = leafletRefs[id]?.marker;
if (marker && marker != null) { resetFilterTags();
marker !== null && clusterRef?.current?.zoomToShowLayer(marker, () => { if (marker && filterTags.length == 0) {
marker !== null && clusterRef?.zoomToShowLayer(marker, () => {
marker.openPopup(); marker.openPopup();
}); });
const item = leafletRefs[id]?.item; const item = leafletRefs[id]?.item;
@ -113,7 +119,6 @@ export const Layer = ({
} }
} }
} }
} }
useEffect(() => { useEffect(() => {
@ -157,7 +162,7 @@ export const Layer = ({
} }
if (allTagsLoaded && allItemsLoaded) { if (allTagsLoaded && allItemsLoaded) {
item[itemTextField].match(hashTagRegex)?.map(tag => { item[itemTextField].match(hashTagRegex)?.map(tag => {
if ((!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) && !newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) { if ((!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) && !newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
const newTag = { id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() }; const newTag = { id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() };
@ -188,7 +193,13 @@ export const Layer = ({
<Marker ref={(r) => { <Marker ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].marker == r)) if (!(item.id in leafletRefs && leafletRefs[item.id].marker == r))
r && addMarker(item, r); r && addMarker(item, r);
}} icon={MarkerIconFactory(markerShape, color1, color2, markerIcon)} key={item.id} position={[latitude, longitude]}> }}
eventHandlers={{
click: () => {
setMarkerClicked(item)
},
}}
icon={MarkerIconFactory(markerShape, color1, color2, markerIcon)} key={item.id} position={[latitude, longitude]}>
{ {
(children && React.Children.toArray(children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ? (children && React.Children.toArray(children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ?
React.Children.toArray(children).map((child) => React.Children.toArray(children).map((child) =>

View File

@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { useAddFilterTag, useFilterTags, useResetFilterTags, useSetSearchPhrase } from '../../hooks/useFilter' import { useAddFilterTag, useFilterTags, useResetFilterTags } from '../../hooks/useFilter'
import useWindowDimensions from '../../hooks/useWindowDimension'; import useWindowDimensions from '../../hooks/useWindowDimension';
import axios from 'axios'; import axios from 'axios';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
@ -15,10 +15,11 @@ import * as L from 'leaflet';
import MarkerIconFactory from '../../../../Utils/MarkerIconFactory'; import MarkerIconFactory from '../../../../Utils/MarkerIconFactory';
import { decodeTag } from '../../../../Utils/FormatTags'; import { decodeTag } from '../../../../Utils/FormatTags';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useClusterRef } from '../../hooks/useClusterRef';
export const SearchControl = ({ clusterRef }) => { export const SearchControl = () => {
const windowDimensions = useWindowDimensions(); const windowDimensions = useWindowDimensions();
const [popupOpen, setPopupOpen] = useState(false); const [popupOpen, setPopupOpen] = useState(false);
@ -36,6 +37,7 @@ export const SearchControl = ({ clusterRef }) => {
const addFilterTag = useAddFilterTag(); const addFilterTag = useAddFilterTag();
const resetFilterTags = useResetFilterTags(); const resetFilterTags = useResetFilterTags();
const filterTags = useFilterTags(); const filterTags = useFilterTags();
const clusterRef = useClusterRef();
useMapEvents({ useMapEvents({
popupopen: () => { popupopen: () => {
@ -115,17 +117,9 @@ export const SearchControl = ({ clusterRef }) => {
<div key={item.id} className='tw-cursor-pointer hover:tw-font-bold' onClick={() => { <div key={item.id} className='tw-cursor-pointer hover:tw-font-bold' onClick={() => {
const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker; const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker;
if(marker){ if(marker){
if (filterTags.length > 0) { navigate(`/${item.layer.name}/${item.id}`)
marker !== null && window.history.pushState({}, "", `/${item.layer.name}/${item.id}`)
resetFilterTags();
hide();
}
else {
marker !== null && clusterRef?.current?.zoomToShowLayer(marker, () => {
marker?.openPopup();
hide();
});
}
} }
else { else {
navigate("item/"+item.id) navigate("item/"+item.id)

View File

@ -96,7 +96,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const item = items.find(item => item.layer == props.layer && item.user_created?.id == user?.id); const item = items.find(item => item.layer == props.layer && item.user_created?.id == user?.id);
item && removeItem(item); item && removeItem(item);
} }
addItem({...formItem, id: uuid, layer: props.layer, user_created: user}); addItem({...formItem, id: uuid, layer: props.layer, user_created: user, type: props.layer.itemType });
toast.success("New item created"); toast.success("New item created");
resetFilterTags(); resetFilterTags();
} }

View File

@ -5,16 +5,17 @@ import { getValue } from "../../../../Utils/GetValue";
import { useAssetApi } from '../../../AppShell/hooks/useAssets' import { useAssetApi } from '../../../AppShell/hooks/useAssets'
import DialogModal from "../../../Templates/DialogModal"; import DialogModal from "../../../Templates/DialogModal";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSetSelectPosition } from "../../hooks/useSetItemPosition"; import { useMap } from "react-leaflet";
export function HeaderView({ item, api, editCallback, deleteCallback, itemNameField, itemAvatarField, loading, hideMenu = false, big = false, updatePosition = false }: { export function HeaderView({ item, api, editCallback, deleteCallback, setPositionCallback, itemNameField, itemAvatarField, loading, hideMenu = false, big = false }: {
item: Item, item: Item,
api?: ItemsApi<any>, api?: ItemsApi<any>,
editCallback?: any, editCallback?: any,
deleteCallback?: any, deleteCallback?: any,
setPositionCallback?: any,
itemNameField?: string, itemNameField?: string,
itemAvatarField?: string, itemAvatarField?: string,
loading?: boolean, loading?: boolean,
@ -29,7 +30,6 @@ export function HeaderView({ item, api, editCallback, deleteCallback, itemNameFi
const hasUserPermission = useHasUserPermission(); const hasUserPermission = useHasUserPermission();
const navigate = useNavigate(); const navigate = useNavigate();
const assetsApi = useAssetApi(); const assetsApi = useAssetApi();
const setSelectPosition = useSetSelectPosition();
const avatar = itemAvatarField && getValue(item, itemAvatarField) ? assetsApi.url + getValue(item, itemAvatarField) + `${big ? "?width=160&heigth=160": "?width=80&heigth=80"}` : item.layer?.itemAvatarField && item && getValue(item, item.layer?.itemAvatarField) && assetsApi.url + getValue(item, item.layer?.itemAvatarField) + `${big ? "?width=160&heigth=160": "?width=80&heigth=80"}`; const avatar = itemAvatarField && getValue(item, itemAvatarField) ? assetsApi.url + getValue(item, itemAvatarField) + `${big ? "?width=160&heigth=160": "?width=80&heigth=80"}` : item.layer?.itemAvatarField && item && getValue(item, item.layer?.itemAvatarField) && assetsApi.url + getValue(item, item.layer?.itemAvatarField) + `${big ? "?width=160&heigth=160": "?width=80&heigth=80"}`;
const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField); const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField);
@ -63,21 +63,21 @@ export function HeaderView({ item, api, editCallback, deleteCallback, itemNameFi
</svg> </svg>
</label> </label>
<ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000"> <ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000">
{((api?.updateItem && hasUserPermission(api.collectionName!, "update", item)) || item.layer?.customEditLink) && <li> {((api?.updateItem && hasUserPermission(api.collectionName!, "update", item)) || item.layer?.customEditLink) && editCallback && <li>
<a className="!tw-text-base-content tw-cursor-pointer" onClick={() => item.layer?.customEditLink? navigate(item.layer.customEditLink) : editCallback}> <a className="!tw-text-base-content tw-cursor-pointer" onClick={() => item.layer?.customEditLink? navigate(item.layer.customEditLink) : editCallback()}>
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /> <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg> </svg>
</a> </a>
</li>} </li>}
{((api?.updateItem && hasUserPermission(api.collectionName!, "update", item))) && updatePosition &&<li> {((api?.updateItem && hasUserPermission(api.collectionName!, "update", item))) && setPositionCallback &&<li>
<a className="!tw-text-base-content tw-cursor-pointer" onClick={() => {setSelectPosition(item), navigate("/")}}> <a className="!tw-text-base-content tw-cursor-pointer" onClick={setPositionCallback}>
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" className="tw-w-5 tw-h-5" xmlns="http://www.w3.org/2000/svg"> <svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" className="tw-w-5 tw-h-5" xmlns="http://www.w3.org/2000/svg">
<path d="M256 0c17.7 0 32 14.3 32 32V42.4c93.7 13.9 167.7 88 181.6 181.6H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H469.6c-13.9 93.7-88 167.7-181.6 181.6V480c0 17.7-14.3 32-32 32s-32-14.3-32-32V469.6C130.3 455.7 56.3 381.7 42.4 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H42.4C56.3 130.3 130.3 56.3 224 42.4V32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6V384c0-17.7 14.3-32 32-32s32 14.3 32 32v20.6c58.3-12.5 104.1-58.4 116.6-116.6H384c-17.7 0-32-14.3-32-32s14.3-32 32-32h20.6C392.1 165.7 346.3 119.9 288 107.4V128c0 17.7-14.3 32-32 32s-32-14.3-32-32V107.4C165.7 119.9 119.9 165.7 107.4 224H128c17.7 0 32 14.3 32 32s-14.3 32-32 32H107.4zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path> <path d="M256 0c17.7 0 32 14.3 32 32V42.4c93.7 13.9 167.7 88 181.6 181.6H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H469.6c-13.9 93.7-88 167.7-181.6 181.6V480c0 17.7-14.3 32-32 32s-32-14.3-32-32V469.6C130.3 455.7 56.3 381.7 42.4 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H42.4C56.3 130.3 130.3 56.3 224 42.4V32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6V384c0-17.7 14.3-32 32-32s32 14.3 32 32v20.6c58.3-12.5 104.1-58.4 116.6-116.6H384c-17.7 0-32-14.3-32-32s14.3-32 32-32h20.6C392.1 165.7 346.3 119.9 288 107.4V128c0 17.7-14.3 32-32 32s-32-14.3-32-32V107.4C165.7 119.9 119.9 165.7 107.4 224H128c17.7 0 32 14.3 32 32s-14.3 32-32 32H107.4zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path>
</svg> </svg>
</a> </a>
</li>} </li>}
{api?.deleteItem && hasUserPermission(api.collectionName!, "delete", item) && <li> {api?.deleteItem && hasUserPermission(api.collectionName!, "delete", item) && deleteCallback &&<li>
<a className='tw-cursor-pointer !tw-text-error' onClick={openDeleteModal}> <a className='tw-cursor-pointer !tw-text-error' onClick={openDeleteModal}>
{loading ? <span className="tw-loading tw-loading-spinner tw-loading-sm"></span> {loading ? <span className="tw-loading tw-loading-spinner tw-loading-sm"></span>
: :

View File

@ -4,8 +4,11 @@ import { getValue } from '../../../../Utils/GetValue'
import { Item } from '../../../../types' import { Item } from '../../../../types'
export const PopupButton = ({url, parameterField, text, colorField, item} : {url: string, parameterField?: string, text: string, colorField?: string, item? : Item}) => { export const PopupButton = ({url, parameterField, text, colorField, item} : {url: string, parameterField?: string, text: string, colorField?: string, item? : Item}) => {
let params = new URLSearchParams(window.location.search);
return ( return (
<Link to={`${url}/${parameterField? getValue(item,parameterField):``}`}><button style={{backgroundColor: `${colorField && getValue(item,colorField)? getValue(item,colorField) : item?.layer?.markerDefaultColor}`}} className="tw-btn tw-text-white tw-btn-sm tw-float-right tw-mt-1">{text}</button></Link> <Link to={`${url}/${parameterField? getValue(item,parameterField):``}?${params}`}><button style={{backgroundColor: `${colorField && getValue(item,colorField)? getValue(item,colorField) : item?.layer?.markerDefaultColor}`}} className="tw-btn tw-text-white tw-btn-sm tw-float-right tw-mt-1">{text}</button></Link>
) )
} }

View File

@ -10,6 +10,7 @@ import { LatLng } from 'leaflet'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useRemoveItem } from '../hooks/useItems' import { useRemoveItem } from '../hooks/useItems'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useSetSelectPosition } from '../hooks/useSelectPosition'
export interface ItemViewPopupProps { export interface ItemViewPopupProps {
@ -26,6 +27,7 @@ export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: a
const [loading, setLoading] = React.useState<boolean>(false); const [loading, setLoading] = React.useState<boolean>(false);
const removeItem = useRemoveItem(); const removeItem = useRemoveItem();
const navigate = useNavigate(); const navigate = useNavigate();
const setSelectPosition = useSetSelectPosition();
const [infoExpanded, setInfoExpanded] = useState<Boolean>(false); const [infoExpanded, setInfoExpanded] = useState<Boolean>(false);
@ -61,7 +63,7 @@ export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: a
return ( return (
<LeafletPopup ref={ref} 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 api={props.item.layer?.api} item={props.item} editCallback={handleEdit} deleteCallback={handleDelete} updatePosition/> <HeaderView api={props.item.layer?.api} item={props.item} editCallback={handleEdit} deleteCallback={handleDelete} setPositionCallback={()=>{map.closePopup();setSelectPosition(props.item); navigate("/")}} loading={loading} />
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'> <div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{props.children ? {props.children ?

View File

@ -14,9 +14,10 @@ import { QuestControl } from "./Subcomponents/Controls/QuestControl";
import { Control } from "./Subcomponents/Controls/Control"; import { Control } from "./Subcomponents/Controls/Control";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { TagsControl } from "./Subcomponents/Controls/TagsControl"; import { TagsControl } from "./Subcomponents/Controls/TagsControl";
import { useSelectPosition, useSetSelectPosition } from "./hooks/useSetItemPosition"; import { useSelectPosition, useSetSelectPosition } from "./hooks/useSelectPosition";
import { useUpdateItem } from "./hooks/useItems"; import { useUpdateItem } from "./hooks/useItems";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
export interface MapEventListenerProps { export interface MapEventListenerProps {
@ -44,15 +45,14 @@ function UtopiaMap({
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`) window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
document.title = document.title.split("-")[0]; document.title = document.title.split("-")[0];
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title); 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")}`); document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
console.log(e.latlng.lat + ',' + e.latlng.lng); console.log(e.latlng.lat + ',' + e.latlng.lng);
if (selectNewItemPosition != null) { if (selectNewItemPosition != null) {
if ('menuIcon' in selectNewItemPosition) { if ('menuIcon' in selectNewItemPosition) {
props.setItemFormPopup({ layer: props.selectNewItemPosition, position: e.latlng }) props.setItemFormPopup({ layer: props.selectNewItemPosition, position: e.latlng })
props.setSelectNewItemPosition(null) props.setSelectNewItemPosition(null)
} }
if ('position' in selectNewItemPosition) { if ('text' in selectNewItemPosition) {
const position = new Geometry(e.latlng.lng,e.latlng.lat); const position = new Geometry(e.latlng.lng,e.latlng.lat);
itemUpdate({...selectNewItemPosition as Item, position: position }) itemUpdate({...selectNewItemPosition as Item, position: position })
setSelectNewItemPosition(null); setSelectNewItemPosition(null);
@ -61,14 +61,13 @@ function UtopiaMap({
}, },
moveend: (e) => { moveend: (e) => {
console.log(e); console.log(e);
} },
}) })
return null return null
} }
const itemUpdate = async (updatedItem: Item) => { const itemUpdate = async (updatedItem: Item) => {
console.log(updatedItem);
console.log(updatedItem?.layer?.api?.updateItem!);
let success = false; let success = false;
try { try {
await updatedItem?.layer?.api?.updateItem!({id: updatedItem.id, position: updatedItem.position }) await updatedItem?.layer?.api?.updateItem!({id: updatedItem.id, position: updatedItem.position })
@ -85,10 +84,11 @@ function UtopiaMap({
const selectNewItemPosition = useSelectPosition(); const selectNewItemPosition = useSelectPosition();
const setSelectNewItemPosition = useSetSelectPosition(); const setSelectNewItemPosition = useSetSelectPosition();
const clusterRef = React.useRef();
const location = useLocation(); const location = useLocation();
const updateItem = useUpdateItem(); const updateItem = useUpdateItem();
const navigate = useNavigate(); const navigate = useNavigate();
const setClusterRef = useSetClusterRef();
const clusterRef = useClusterRef();
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null); const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
@ -100,13 +100,11 @@ function UtopiaMap({
return ( return (
<> <>
<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={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}> <MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}>
<Outlet></Outlet> <Outlet></Outlet>
<Control position='topLeft' zIndex="1000"> <Control position='topLeft' zIndex="1000">
<SearchControl clusterRef={clusterRef} /> <SearchControl />
<TagsControl /> <TagsControl />
</Control> </Control>
<Control position='bottomLeft' zIndex="999"> <Control position='bottomLeft' zIndex="999">
@ -117,7 +115,7 @@ function UtopiaMap({
maxZoom={19} maxZoom={19}
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 ref={clusterRef} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}> <MarkerClusterGroup ref={(r)=> setClusterRef(r)} 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, clusterRef: React.MutableRefObject<undefined> }>(child) ? React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null, clusterRef: React.MutableRefObject<undefined> }>(child) ?

View File

@ -0,0 +1,40 @@
import { createContext, useContext, useState } from "react";
type UseClusterRefManagerResult = ReturnType<typeof useClusterRefManager>;
const ClusterRefContext = createContext<UseClusterRefManagerResult>({
clusterRef: {} as React.MutableRefObject<undefined>,
setClusterRef: () => { },
});
function useClusterRefManager(): {
clusterRef: any
setClusterRef: React.Dispatch<React.SetStateAction<React.MutableRefObject<undefined>>>;
} {
const [clusterRef, setClusterRef] = useState<React.MutableRefObject<undefined>>({} as React.MutableRefObject<undefined>);
return { clusterRef, setClusterRef };
}
export const ClusterRefProvider: React.FunctionComponent<{
children?: React.ReactNode
}> = ({ children }) => (
<ClusterRefContext.Provider value={useClusterRefManager()}>
{children}
</ClusterRefContext.Provider>
);
export const useClusterRef = (): any=> {
const { clusterRef } = useContext(ClusterRefContext);
return clusterRef;
};
export const useSetClusterRef = (): UseClusterRefManagerResult["setClusterRef"] => {
const { setClusterRef } = useContext(ClusterRefContext);
return setClusterRef;
}

View File

@ -0,0 +1,107 @@
import { createContext, useContext, useEffect, useState } from "react";
import { Item, LayerProps } from '../../../types';
import { useUpdateItem } from "./useItems";
import { toast } from "react-toastify";
import { useHasUserPermission } from "./usePermissions";
type UseSelectPositionManagerResult = ReturnType<typeof useSelectPositionManager>;
const SelectPositionContext = createContext<UseSelectPositionManagerResult>({
selectPosition: null,
setSelectPosition: () => { },
setMarkerClicked: () => { },
});
function useSelectPositionManager(): {
selectPosition: Item | LayerProps | null;
setSelectPosition: React.Dispatch<React.SetStateAction<Item | LayerProps | null>>;
setMarkerClicked: React.Dispatch<React.SetStateAction<Item>>;
} {
const [selectPosition, setSelectPosition] = useState<LayerProps | null | Item>(null);
const [markerClicked, setMarkerClicked] = useState<Item>();
const updateItem = useUpdateItem();
const hasUserPermission = useHasUserPermission();
useEffect(() => {
if (selectPosition && markerClicked && 'text' in selectPosition) {
itemUpdate({ ...selectPosition, parent: markerClicked.id })
}
}, [markerClicked])
const itemUpdate = async (updatedItem: Item) => {
if (markerClicked?.layer?.api?.collectionName && hasUserPermission(markerClicked?.layer?.api?.collectionName, "update", markerClicked)) {
let success = false;
try {
await updatedItem?.layer?.api?.updateItem!({ id: updatedItem.id, parent: updatedItem.parent, position: null })
success = true;
} catch (error) {
toast.error(error.toString());
}
if (success) {
await updateItem({ ...updatedItem, parent: updatedItem.parent, position: undefined })
await linkItem(updatedItem.id);
toast.success("Item position updated");
setSelectPosition(null);
}
}
else {
setSelectPosition(null);
toast.error("you don't have permission to add items to " + markerClicked?.name);
}
}
const linkItem = async (id: string) => {
if (markerClicked) {
let new_relations = markerClicked.relations || [];
console.log(new_relations);
console.log(id);
if (!new_relations.some(r => r.related_items_id == id)) {
new_relations?.push({ items_id: markerClicked.id, related_items_id: id })
const updatedItem = { id: markerClicked.id, relations: new_relations }
let success = false;
try {
await markerClicked?.layer?.api?.updateItem!(updatedItem)
success = true;
} catch (error) {
toast.error(error.toString());
}
if (success) {
updateItem({ ...markerClicked, relations: new_relations })
toast.success("Item linked");
}
}
}
}
return { selectPosition, setSelectPosition, setMarkerClicked };
}
export const SelectPositionProvider: React.FunctionComponent<{
children?: React.ReactNode
}> = ({ children }) => (
<SelectPositionContext.Provider value={useSelectPositionManager()}>
{children}
</SelectPositionContext.Provider>
);
export const useSelectPosition = (): Item | LayerProps | null => {
const { selectPosition } = useContext(SelectPositionContext);
return selectPosition;
};
export const useSetSelectPosition = (): UseSelectPositionManagerResult["setSelectPosition"] => {
const { setSelectPosition } = useContext(SelectPositionContext);
return setSelectPosition;
}
export const useSetMarkerClicked = (): UseSelectPositionManagerResult["setMarkerClicked"] => {
const { setMarkerClicked } = useContext(SelectPositionContext);
return setMarkerClicked;
}

View File

@ -1,35 +0,0 @@
import { createContext, useContext, useState } from "react";
import { Item, LayerProps } from '../../../types';
type UseSelectPositionManagerResult = ReturnType<typeof useSelectPositionManager>;
const SelectPositionContext = createContext<UseSelectPositionManagerResult>({
selectPosition: null,
setSelectPosition: () => { },
});
function useSelectPositionManager(): {
selectPosition: Item | LayerProps | null;
setSelectPosition: React.Dispatch<React.SetStateAction< Item | LayerProps | null>>;
} {
const [selectPosition, setSelectPosition] = useState<LayerProps | null | Item>(null);
return { selectPosition, setSelectPosition };
}
export const SelectPositionProvider: React.FunctionComponent<{
children?: React.ReactNode
}> = ({ children }) => (
<SelectPositionContext.Provider value={useSelectPositionManager()}>
{children}
</SelectPositionContext.Provider>
);
export const useSelectPosition = (): Item | LayerProps | null => {
const { selectPosition } = useContext(SelectPositionContext);
return selectPosition;
};
export const useSetSelectPosition = (): UseSelectPositionManagerResult["setSelectPosition"] => {
const { setSelectPosition } = useContext(SelectPositionContext);
return setSelectPosition;
}

View File

@ -8,7 +8,7 @@ import { LatLng } from 'leaflet';
import { PopupStartEndInput, StartEndView, TextView } from '../Map'; import { PopupStartEndInput, StartEndView, TextView } from '../Map';
import useWindowDimensions from '../Map/hooks/useWindowDimension'; import useWindowDimensions from '../Map/hooks/useWindowDimension';
import { useAddTag, useTags } from '../Map/hooks/useTags'; import { useAddTag, useTags } from '../Map/hooks/useTags';
import { useResetFilterTags } from '../Map/hooks/useFilter'; import { useFilterTags, useResetFilterTags } from '../Map/hooks/useFilter';
import { useHasUserPermission } from '../Map/hooks/usePermissions'; import { useHasUserPermission } from '../Map/hooks/usePermissions';
import { TextAreaInput, TextInput } from '../Input'; import { TextAreaInput, TextInput } from '../Input';
import { hashTagRegex } from '../../Utils/HashTagRegex'; import { hashTagRegex } from '../../Utils/HashTagRegex';
@ -19,44 +19,38 @@ import { useLayers } from '../Map/hooks/useLayers';
import { ActionButton } from './ActionsButton'; import { ActionButton } from './ActionsButton';
import { LinkedItemsHeaderView } from './LinkedItemsHeaderView'; import { LinkedItemsHeaderView } from './LinkedItemsHeaderView';
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView'; import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
import { useSelectPosition } from '../Map/hooks/useSetItemPosition'; import { useSelectPosition, useSetSelectPosition } from '../Map/hooks/useSelectPosition';
import { useClusterRef } from '../Map/hooks/useClusterRef';
import { useLeafletRefs } from '../Map/hooks/useLeafletRefs'; import { useLeafletRefs } from '../Map/hooks/useLeafletRefs';
export function OverlayItemProfile() { export function OverlayItemProfile() {
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
const [relations, setRelations] = useState<Array<Item>>([]);
const [activeTab, setActiveTab] = useState<number>(1);
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const location = useLocation(); const location = useLocation();
const items = useItems(); const items = useItems();
const updateItem = useUpdateItem(); const updateItem = useUpdateItem();
const [item, setItem] = useState<Item>({} as Item) const [item, setItem] = useState<Item>({} as Item)
const map = useMap(); const map = useMap();
const windowDimension = useWindowDimensions(); const windowDimension = useWindowDimensions();
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
const layers = useLayers(); const layers = useLayers();
const selectPosition = useSelectPosition(); const selectPosition = useSelectPosition();
const removeItem = useRemoveItem(); const removeItem = useRemoveItem();
const tags = useTags(); const tags = useTags();
const navigate = useNavigate(); const navigate = useNavigate();
const [relations, setRelations] = useState<Array<Item>>([]);
const [activeTab, setActiveTab] = useState<number>(1);
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const addTag = useAddTag(); const addTag = useAddTag();
const resetFilterTags = useResetFilterTags(); const resetFilterTags = useResetFilterTags();
const filterTags = useFilterTags();
const addItem = useAddItem(); const addItem = useAddItem();
const { user } = useAuth(); const { user } = useAuth();
const hasUserPermission = useHasUserPermission(); const hasUserPermission = useHasUserPermission();
const setSelectPosition = useSetSelectPosition();
const clusterRef = useClusterRef();
const leafletRefs = useLeafletRefs();
const tabRef = useRef<HTMLFormElement>(null); const tabRef = useRef<HTMLFormElement>(null);
@ -68,11 +62,6 @@ export function OverlayItemProfile() {
scroll(); scroll();
}, [addItemPopupType]) }, [addItemPopupType])
useEffect(() => {
console.log(addItemPopupType);
}, [addItemPopupType])
const updateActiveTab = (id: number) => { const updateActiveTab = (id: number) => {
setActiveTab(id); setActiveTab(id);
@ -85,18 +74,31 @@ export function OverlayItemProfile() {
} }
useEffect(() => { useEffect(() => {
const itemId = location.pathname.split("/")[2]; const itemId = location.pathname.split("/")[2];
const item = items.find(i => i.id === itemId); const item = items.find(i => i.id === itemId);
item && setItem(item); item && setItem(item);
const bounds = map.getBounds(); resetFilterTags();
const x = bounds.getEast() - bounds.getWest() if (item && filterTags.length == 0) {
if (windowDimension.width > 768) if(item.position) {
if (item?.position && item?.position.coordinates[0]) const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker;
map.setView(new LatLng(item?.position.coordinates[1]!, item?.position.coordinates[0]! + x / 4)) marker && clusterRef?.zoomToShowLayer(marker, () => {
const bounds = map.getBounds();
const x = bounds.getEast() - bounds.getWest()
map.setView(new LatLng(item?.position?.coordinates[1]!, item?.position?.coordinates[0]! + x / 4), undefined, {duration: 1})}
);
}
else {
const parent = items.find(i => i.id == item.parent);
const marker = Object.entries(leafletRefs).find(r => r[1].item == parent)?.[1].marker;
marker && clusterRef?.zoomToShowLayer(marker, () => {
const bounds = map.getBounds();
const x = bounds.getEast() - bounds.getWest()
map.setView(new LatLng(parent?.position?.coordinates[1]!, parent?.position?.coordinates[0]! + x / 4), undefined, {duration: 1})}
);
}
}
}, [items, activeTab, leafletRefs])
}, [location, items, activeTab])
useEffect(() => { useEffect(() => {
@ -121,8 +123,6 @@ export function OverlayItemProfile() {
}, [item]) }, [item])
const [selecting, setSelecting] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
selectPosition && map.closePopup(); selectPosition && map.closePopup();
}, [selectPosition]) }, [selectPosition])
@ -233,17 +233,13 @@ export function OverlayItemProfile() {
return ( return (
<> <>
{item && {item &&
<MapOverlayPage className={`tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-[calc(100dvh-96px)] md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}> <MapOverlayPage className={`tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-[calc(100dvh-96px)] md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}>
<> <>
<HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} big updatePosition/> <HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={()=>{map.closePopup();setSelectPosition(item); navigate("/")}} big />
<div className='tw-h-full'> <div className='tw-h-full'>
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2"> <div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2">
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} /> <input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto fade tw-pt-2 tw-pb-4 tw-mb-4 tw-overflow-x-hidden"> <div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto fade tw-pt-2 tw-pb-4 tw-mb-4 tw-overflow-x-hidden">

View File

@ -21,6 +21,7 @@ export interface LayerProps {
markerShape: string, markerShape: string,
markerDefaultColor: string, markerDefaultColor: string,
api?: ItemsApi<any>, api?: ItemsApi<any>,
itemType: string,
itemNameField?: string, itemNameField?: string,
itemTextField?: string, itemTextField?: string,
itemAvatarField?: string, itemAvatarField?: string,
@ -35,7 +36,7 @@ export interface LayerProps {
customEditLink?: string, customEditLink?: string,
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>, setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>,
itemFormPopup?: ItemFormPopupProps | null, itemFormPopup?: ItemFormPopupProps | null,
clusterRef?: React.MutableRefObject<any> clusterRef?: any
} }
export class Item { export class Item {
@ -51,6 +52,7 @@ export class Item {
tags?: string[]; tags?: string[];
layer?: LayerProps; layer?: LayerProps;
relations?: Relation[]; relations?: Relation[];
parent?:string;
[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;