diff --git a/package-lock.json b/package-lock.json index 582857c1..1d9a899d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@heroicons/react": "^2.0.17", "@tanstack/react-query": "^5.17.8", "@types/offscreencanvas": "^2019.7.1", + "axios": "^1.6.5", "leaflet": "^1.9.4", "prop-types": "^15.8.1", "react-colorful": "^5.6.1", @@ -741,6 +742,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -774,6 +780,16 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -1027,6 +1043,17 @@ "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1337,6 +1364,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2085,6 +2120,38 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -3554,6 +3621,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4634,6 +4720,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", diff --git a/package.json b/package.json index df3046ae..e4dbad09 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@heroicons/react": "^2.0.17", "@tanstack/react-query": "^5.17.8", "@types/offscreencanvas": "^2019.7.1", + "axios": "^1.6.5", "leaflet": "^1.9.4", "prop-types": "^15.8.1", "react-colorful": "^5.6.1", diff --git a/src/Components/Map/Layer.tsx b/src/Components/Map/Layer.tsx index 9d5c73a1..d3434e05 100644 --- a/src/Components/Map/Layer.tsx +++ b/src/Components/Map/Layer.tsx @@ -6,7 +6,7 @@ import { ItemViewPopup } from './Subcomponents/ItemViewPopup' import { useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems' import { useEffect } from 'react' import { ItemFormPopup } from './Subcomponents/ItemFormPopup' -import { useFilterTags, useIsLayerVisible, useSearchPhrase } from './hooks/useFilter' +import { useFilterTags, useIsLayerVisible } from './hooks/useFilter' import { useGetItemTags } from './hooks/useTags' import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs' import { Popup } from 'leaflet' @@ -47,7 +47,6 @@ export const Layer = ( { let location = useLocation(); - const searchPhrase = useSearchPhrase(); const map = useMap(); @@ -64,7 +63,7 @@ export const Layer = ( { useMapEvents({ 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) { window.history.pushState({}, "", `/${name}/${item.id}`) let title = ""; @@ -116,11 +115,6 @@ export const Layer = ( { filter(item => item.layer?.name === name)?. filter(item => filterTags.length == 0 ? item : filterTags.every(tag => getItemTags(item).some(filterTag => filterTag.id === tag.id)))?. - filter(item => { - return searchPhrase === '' - ? item : - item.name?.toLowerCase().includes(searchPhrase.toLowerCase()) || item.text?.toLowerCase().includes(searchPhrase.toLowerCase()) - }). filter(item => item.layer && isLayerVisible(item.layer)). map((item: Item) => { const tags = getItemTags(item); @@ -139,7 +133,7 @@ export const Layer = ( { } return ( { - if (!(item.id in leafletRefs)) + if (!(item.id in leafletRefs && leafletRefs[item.id].marker == r)) r && addMarker(item, r); }} icon={MarkerIconFactory(markerShape, color1, color2, markerIcon)} key={item.id} position={[item.position.coordinates[1], item.position.coordinates[0]]}> { @@ -147,7 +141,7 @@ export const Layer = ( { React.Children.toArray(children).map((child) => React.isValidElement(child) && child.props.__TYPE === "ItemView" ? { - if (!(item.id in leafletRefs)) + if (!(item.id in leafletRefs && leafletRefs[item.id].popup == r)) r && addPopup(item, r as Popup); }} key={item.id + item.name} title={itemTitleField && item ? getValue(item, itemTitleField) : undefined} @@ -162,7 +156,7 @@ export const Layer = ( { : <> { - if (!(item.id in leafletRefs)) + if (!(item.id in leafletRefs && leafletRefs[item.id].popup == r)) r && addPopup(item, r as Popup); }} title={itemTitleField && item ? getValue(item, itemTitleField) : undefined} avatar={itemAvatarField && item && getValue(item, itemAvatarField)? assetsApi.url + getValue(item, itemAvatarField) : undefined} diff --git a/src/Components/Map/Subcomponents/Control.tsx b/src/Components/Map/Subcomponents/Control.tsx deleted file mode 100644 index d6d564a6..00000000 --- a/src/Components/Map/Subcomponents/Control.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react' - -export const Control = ({children}) => { - return ( -
{children}
- ) -} diff --git a/src/Components/Map/Subcomponents/Controls/Control.tsx b/src/Components/Map/Subcomponents/Controls/Control.tsx new file mode 100644 index 00000000..63ab78b8 --- /dev/null +++ b/src/Components/Map/Subcomponents/Controls/Control.tsx @@ -0,0 +1,25 @@ +import * as L from 'leaflet' +import * as React from 'react' + + + +export const Control = ({ position, children }: { position: "topLeft" | "topRight" | "bottomLeft" | "bottomRight", children: React.ReactNode }) => { + + + const controlContainerRef = React.createRef() + + + React.useEffect(() => { + if (controlContainerRef.current !== null) { + L.DomEvent.disableClickPropagation(controlContainerRef.current) + L.DomEvent.disableScrollPropagation(controlContainerRef.current) + } + }, [controlContainerRef]) + + return ( +
+ + {children} +
+ ) +} diff --git a/src/Components/Map/Subcomponents/LayerControl.tsx b/src/Components/Map/Subcomponents/Controls/LayerControl.tsx similarity index 84% rename from src/Components/Map/Subcomponents/LayerControl.tsx rename to src/Components/Map/Subcomponents/Controls/LayerControl.tsx index 146352ef..4458ef78 100644 --- a/src/Components/Map/Subcomponents/LayerControl.tsx +++ b/src/Components/Map/Subcomponents/Controls/LayerControl.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import * as L from 'leaflet' -import { useLayers } from '../hooks/useLayers'; -import { useAddVisibleLayer, useIsLayerVisible, useToggleVisibleLayer } from '../hooks/useFilter'; +import { useLayers } from '../../hooks/useLayers'; +import { useAddVisibleLayer, useIsLayerVisible, useToggleVisibleLayer } from '../../hooks/useFilter'; import { useEffect } from 'react'; export function LayerControl() { @@ -9,14 +9,6 @@ export function LayerControl() { const [open, setOpen] = React.useState(false); const layers = useLayers(); - const controlContainerRef = React.createRef() - - React.useEffect(() => { - if (controlContainerRef.current !== null) { - L.DomEvent.disableClickPropagation(controlContainerRef.current) - L.DomEvent.disableScrollPropagation(controlContainerRef.current) - } - }, [controlContainerRef]) useEffect(() => { layers.map(layer => @@ -30,7 +22,7 @@ export function LayerControl() { const addVisibleLayer = useAddVisibleLayer(); return ( -
e.stopPropagation()}> +
e.stopPropagation()}> { open ?
e.stopPropagation()}> diff --git a/src/Components/Map/Subcomponents/QuestControl.tsx b/src/Components/Map/Subcomponents/Controls/QuestControl.tsx similarity index 94% rename from src/Components/Map/Subcomponents/QuestControl.tsx rename to src/Components/Map/Subcomponents/Controls/QuestControl.tsx index 9ef83afd..c6b8ba98 100644 --- a/src/Components/Map/Subcomponents/QuestControl.tsx +++ b/src/Components/Map/Subcomponents/Controls/QuestControl.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useQuestsOpen, useSetQuestOpen } from '../../Gaming/hooks/useQuests'; +import { useQuestsOpen, useSetQuestOpen } from '../../../Gaming/hooks/useQuests'; export function QuestControl() { diff --git a/src/Components/Map/Subcomponents/Controls/SearchControl.tsx b/src/Components/Map/Subcomponents/Controls/SearchControl.tsx new file mode 100644 index 00000000..9ee2c2cb --- /dev/null +++ b/src/Components/Map/Subcomponents/Controls/SearchControl.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import { useAddFilterTag, useSetSearchPhrase } from '../../hooks/useFilter' +import useWindowDimensions from '../../hooks/useWindowDimension'; +import axios from 'axios'; +import { useRef, useState } from 'react'; +import { useMap } from 'react-leaflet'; +import { LatLng, LatLngBounds } from 'leaflet'; +import { useDebounce } from '../../hooks/useDebounce'; +import { useTags } from '../../hooks/useTags'; +import { useItems } from '../../hooks/useItems'; +import { useLeafletRefs } from '../../hooks/useLeafletRefs'; + + + +export const SearchControl = ({ clusterRef }) => { + + const windowDimensions = useWindowDimensions(); + const [value, setValue] = useState(''); + const [geoResults, setGeoResults] = useState>([]); + const [tagsResults, setTagsResults] = useState>([]); + const [itemsResults, setItemsResults] = useState>([]); + const [hideSuggestions, setHideSuggestions] = useState(true); + + const map = useMap(); + const tags = useTags(); + const items = useItems(); + const leafletRefs = useLeafletRefs(); + const addFilterTag = useAddFilterTag(); + + useDebounce(() => { + const searchGeo = async () => { + try { + const { data } = await axios.get( + `https://photon.komoot.io/api/?q=${value}&limit=5` + ); + + setGeoResults(data.features); + } catch (error) { + console.log(error); + } + }; + searchGeo(); + setItemsResults(items.filter(item => item.name?.toLowerCase().includes(value.toLowerCase()) || item.text?.toLowerCase().includes(value.toLowerCase()))) + setTagsResults(tags.filter(tag => tag.id?.toLowerCase().includes(value.toLowerCase()))) + + + + }, 500, [value]); + + const searchInput = useRef(null); + + return (<> + {!(windowDimensions.height < 500) && +
+ setValue(e.target.value)} + onFocus={() => setHideSuggestions(false)} + onBlur={async () => { + setTimeout(() => { + setHideSuggestions(true); + }, 200); + }} /> + {hideSuggestions || Array.from(geoResults).length == 0 && itemsResults.length == 0 && tagsResults.length == 0 || value.length==0 ? "" : +
+ {tagsResults.length > 0 && +
+ {tagsResults.map(tag => ( +
{ + addFilterTag(tag) + }}> + #{capitalizeFirstLetter(tag.id)} +
+ ))} +
+ } + + {itemsResults.length > 0 && tagsResults.length > 0 &&
} + {itemsResults.slice(0, 10).map(item => ( +
{ + const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker; + marker !== null && clusterRef?.current?.zoomToShowLayer(marker, () => { + marker?.openPopup(); + }); + } + }>{item.name}
+ ))} + {Array.from(geoResults).length > 0 && (itemsResults.length > 0 || tagsResults.length > 0) &&
} + {Array.from(geoResults).map((geo) => ( +
{ + searchInput.current?.blur(); + if (geo.properties.extent) map.fitBounds(new LatLngBounds(new LatLng(geo.properties.extent[1], geo.properties.extent[0]), new LatLng(geo.properties.extent[3], geo.properties.extent[2]))); + else map.setView(new LatLng(geo.geometry.coordinates[1], geo.geometry.coordinates[0]), 15, { duration: 1 }) + }}> + {geo?.properties.name} +
+ ))} +
} +
+ } + + + ) +} + + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} \ No newline at end of file diff --git a/src/Components/Map/Subcomponents/Controls/TagsControl.tsx b/src/Components/Map/Subcomponents/Controls/TagsControl.tsx new file mode 100644 index 00000000..c3d8cc0f --- /dev/null +++ b/src/Components/Map/Subcomponents/Controls/TagsControl.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import { useFilterTags, useRemoveFilterTag } from '../../hooks/useFilter'; + +export const TagsControl = () => { + + const filterTags = useFilterTags(); + const removeFilterTag = useRemoveFilterTag(); + + return ( +
+ { + filterTags.map(tag => +
+
+ +
#{capitalizeFirstLetter(tag.id)} +
+ ) + } + + +
) +} + + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} \ No newline at end of file diff --git a/src/Components/Map/Subcomponents/FilterControl.tsx b/src/Components/Map/Subcomponents/FilterControl.tsx deleted file mode 100644 index 735f4e0e..00000000 --- a/src/Components/Map/Subcomponents/FilterControl.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' -import { useFilterTags, useRemoveFilterTag, useSetSearchPhrase } from '../hooks/useFilter' -import useWindowDimensions from '../hooks/useWindowDimension'; - - - - - - -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -export const FilterControl = () => { - - const windowDimensions = useWindowDimensions(); - /** - const [popupOpen, setPopupOpen] = useState(false); - - useMapEvents({ - popupopen: (e) => { - console.log(e); - - setPopupOpen(true); - }, - popupclose: () => { - setPopupOpen(false); - } - }) - */ - const filterTags = useFilterTags(); - const removeFilterTag = useRemoveFilterTag(); - const setSearchPhrase = useSetSearchPhrase(); - return (<> - {!( - //popupOpen && - windowDimensions.height < 500) && -
- setSearchPhrase(e.target.value)} /> -
- { - filterTags.map(tag => -
-
- -
#{capitalizeFirstLetter(tag.id)} -
- ) - } - - -
-
- } - - - ) -} diff --git a/src/Components/Map/UtopiaMap.tsx b/src/Components/Map/UtopiaMap.tsx index 7c1f440e..3cb121d9 100644 --- a/src/Components/Map/UtopiaMap.tsx +++ b/src/Components/Map/UtopiaMap.tsx @@ -12,13 +12,14 @@ import { ItemsProvider } from "./hooks/useItems"; import { TagsProvider } from "./hooks/useTags"; import { LayersProvider } from "./hooks/useLayers"; import { FilterProvider } from "./hooks/useFilter"; -import { FilterControl } from "./Subcomponents/FilterControl"; +import { SearchControl } from "./Subcomponents/Controls/SearchControl"; import { PermissionsProvider } from "./hooks/usePermissions"; import { LeafletRefsProvider } from "./hooks/useLeafletRefs"; -import { LayerControl } from "./Subcomponents/LayerControl"; -import { QuestControl } from "./Subcomponents/QuestControl"; -import { Control } from "./Subcomponents/Control"; +import { LayerControl } from "./Subcomponents/Controls/LayerControl"; +import { QuestControl } from "./Subcomponents/Controls/QuestControl"; +import { Control } from "./Subcomponents/Controls/Control"; import { Outlet } from "react-router-dom"; +import { TagsControl } from "./Subcomponents/Controls/TagsControl"; export interface MapEventListenerProps { @@ -57,10 +58,6 @@ function UtopiaMap({ props.setSelectNewItemPosition(null) } }, - resize: () => { - console.log("resize"); - }, - }) return null } @@ -73,16 +70,19 @@ function UtopiaMap({ return ( <> - - - - - - + + + + + +
- - + + + + + @@ -110,12 +110,12 @@ function UtopiaMap({
}
- - - - - - + + + + + + ); diff --git a/src/Components/Map/hooks/useDebounce.tsx b/src/Components/Map/hooks/useDebounce.tsx new file mode 100644 index 00000000..5541e925 --- /dev/null +++ b/src/Components/Map/hooks/useDebounce.tsx @@ -0,0 +1,9 @@ +import { useEffect } from 'react'; +import { useTimeout } from './useTimeout'; + +export const useDebounce = (callback, delay, deps) => { + const { reset, clear } = useTimeout(callback, delay); + + useEffect(reset, [...deps, reset]); + useEffect(clear, []); +} \ No newline at end of file diff --git a/src/Components/Map/hooks/useTimeout.tsx b/src/Components/Map/hooks/useTimeout.tsx new file mode 100644 index 00000000..ae31835c --- /dev/null +++ b/src/Components/Map/hooks/useTimeout.tsx @@ -0,0 +1,30 @@ +import { useCallback, useEffect, useRef } from 'react'; + +export const useTimeout = (callback, delay) => { + const callbackRef = useRef(callback); + const timeoutRef = useRef(); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + const set = useCallback(() => { + timeoutRef.current = setTimeout(() => callbackRef.current(), delay); + }, [delay]); + + const clear = useCallback(() => { + timeoutRef.current && clearTimeout(timeoutRef.current); + }, []); + + useEffect(() => { + set(); + return clear; + }, [delay, set, clear]); + + const reset = useCallback(() => { + clear(); + set(); + }, [clear, set]); + + return { reset, clear }; +} \ No newline at end of file