implemented searchbar and reorganized map controls

This commit is contained in:
Anton 2024-01-13 17:54:23 +01:00
parent 00daae6b57
commit 25ec60b3a1
13 changed files with 325 additions and 110 deletions

91
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 (
<Marker ref={(r) => {
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" ?
<ItemViewPopup ref={(r) => {
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 = ( {
:
<>
<ItemViewPopup key={item.id + item.name} ref={(r) => {
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}

View File

@ -1,7 +0,0 @@
import * as React from 'react'
export const Control = ({children}) => {
return (
<div className='tw-absolute tw-bottom-4 tw-left-4 tw-z-[999] tw-flex-col'>{children}</div>
)
}

View File

@ -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<HTMLDivElement>()
React.useEffect(() => {
if (controlContainerRef.current !== null) {
L.DomEvent.disableClickPropagation(controlContainerRef.current)
L.DomEvent.disableScrollPropagation(controlContainerRef.current)
}
}, [controlContainerRef])
return (
<div ref={controlContainerRef} className={`tw-absolute tw-z-[999] tw-flex-col ${position === 'topLeft' && "tw-top-4 tw-left-4"} ${position === 'bottomLeft' && "tw-bottom-4 tw-left-4"} ${position === 'topRight' && "tw-bottom-4 tw-right-4"} ${position === 'bottomRight' && "tw-bottom-4 tw-right-4"}`}>
{children}
</div>
)
}

View File

@ -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<HTMLDivElement>()
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 (
<div ref={controlContainerRef} className="tw-card tw-bg-base-100 tw-shadow-xl " onClick={e => e.stopPropagation()}>
<div className="tw-card tw-bg-base-100 tw-shadow-xl " onClick={e => e.stopPropagation()}>
{
open ?
<div className="tw-card-body tw-p-2 tw-w-32 tw-transition-all tw-duration-300" onClick={e => e.stopPropagation()}>

View File

@ -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() {

View File

@ -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<Array<any>>([]);
const [tagsResults, setTagsResults] = useState<Array<any>>([]);
const [itemsResults, setItemsResults] = useState<Array<any>>([]);
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<HTMLInputElement>(null);
return (<>
{!(windowDimensions.height < 500) &&
<div className='tw-w-[calc(100vw-2rem)] tw-max-w-[22rem] '>
<input type="text" placeholder="search ..." className="tw-input tw-input-bordered tw-w-full tw-shadow-xl tw-rounded-lg"
ref={searchInput}
onChange={(e) => 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 ? "" :
<div className='tw-card tw-card-body tw-bg-base-100 tw-p-4 tw-mt-2 tw-z-1000 tw-shadow-xl'>
{tagsResults.length > 0 &&
<div className='tw-flex tw-flex-wrap tw-max-h-16 tw-overflow-hidden'>
{tagsResults.map(tag => (
<div key={tag.id} className='tw-rounded-2xl tw-text-white tw-p-1 tw-px-4 tw-shadow-md tw-card tw-mr-2 tw-mb-2 tw-cursor-pointer' style={{ backgroundColor: tag.color }} onClick={() => {
addFilterTag(tag)
}}>
<b>#{capitalizeFirstLetter(tag.id)}</b>
</div>
))}
</div>
}
{itemsResults.length > 0 && tagsResults.length > 0 && <hr></hr>}
{itemsResults.slice(0, 10).map(item => (
<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;
marker !== null && clusterRef?.current?.zoomToShowLayer(marker, () => {
marker?.openPopup();
});
}
}>{item.name}</div>
))}
{Array.from(geoResults).length > 0 && (itemsResults.length > 0 || tagsResults.length > 0) && <hr></hr>}
{Array.from(geoResults).map((geo) => (
<div className='tw-cursor-pointer hover:tw-font-bold' key={geo?.properties.osm_id}
onClick={() => {
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}
</div>
))}
</div>}
</div>
}
</>
)
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

@ -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 (
<div className='tw-flex tw-flex-wrap tw-mt-4 tw-w-[calc(100vw-2rem)] tw-max-w-xs'>
{
filterTags.map(tag =>
<div key={tag.id} className='tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mr-2 tw-mb-2' style={{ backgroundColor: tag.color }}>
<div className="tw-card-actions tw-justify-end">
<label className="tw-btn tw-btn-xs tw-btn-circle tw-absolute tw--right-2 tw--top-2 tw-bg-white tw-text-gray-600" onClick={() => (removeFilterTag(tag.id))}></label>
</div><b>#{capitalizeFirstLetter(tag.id)}</b>
</div>
)
}
</div>)
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

@ -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<boolean>(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) &&
<div className='tw-flex tw-flex-col tw-absolute tw-top-4 tw-left-4 tw-z-[699] tw-w-[calc(100vw-2rem)] tw-max-w-sm'>
<input type="text" placeholder="search ..." className="tw-input tw-input-bordered tw-w-full tw-shadow-xl tw-rounded-lg" onChange={(e) => setSearchPhrase(e.target.value)} />
<div className='tw-flex tw-flex-wrap tw-mt-4'>
{
filterTags.map(tag =>
<div key={tag.id} className='tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mr-2 tw-mb-2' style={{ backgroundColor: tag.color }}>
<div className="tw-card-actions tw-justify-end">
<label className="tw-btn tw-btn-xs tw-btn-circle tw-absolute tw--right-2 tw--top-2 tw-bg-white tw-text-gray-600" onClick={() => (removeFilterTag(tag.id))}></label>
</div><b>#{capitalizeFirstLetter(tag.id)}</b>
</div>
)
}
</div>
</div>
}
</>
)
}

View File

@ -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 (
<>
<LayersProvider initialLayers={[]}>
<TagsProvider initialTags={[]}>
<PermissionsProvider initialPermissions={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<LeafletRefsProvider initialLeafletRefs={{}}>
<LayersProvider initialLayers={[]}>
<TagsProvider initialTags={[]}>
<PermissionsProvider initialPermissions={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<LeafletRefsProvider initialLeafletRefs={{}}>
<div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}>
<FilterControl></FilterControl>
<Control>
<Control position='topLeft'>
<SearchControl clusterRef={clusterRef}/>
<TagsControl/>
</Control>
<Control position='bottomLeft'>
<QuestControl></QuestControl>
<LayerControl></LayerControl>
</Control>
@ -110,12 +110,12 @@ function UtopiaMap({
</div>
}
</div>
</LeafletRefsProvider>
</ItemsProvider>
</FilterProvider>
</PermissionsProvider>
</TagsProvider>
</LayersProvider>
</LeafletRefsProvider>
</ItemsProvider>
</FilterProvider>
</PermissionsProvider>
</TagsProvider>
</LayersProvider>
<Outlet></Outlet>
</>
);

View File

@ -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, []);
}

View File

@ -0,0 +1,30 @@
import { useCallback, useEffect, useRef } from 'react';
export const useTimeout = (callback, delay) => {
const callbackRef = useRef(callback);
const timeoutRef = useRef<any>();
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 };
}