Refactor
This commit is contained in:
parent
2c613dc0d1
commit
a677b9331f
28 changed files with 1647 additions and 1498 deletions
13
frontend/src/hooks/useClickOutside.ts
Normal file
13
frontend/src/hooks/useClickOutside.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect, type RefObject } from 'react';
|
||||
|
||||
export function useClickOutside(ref: RefObject<HTMLElement | null>, callback: () => void) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [ref, callback]);
|
||||
}
|
||||
21
frontend/src/hooks/useFadeIn.ts
Normal file
21
frontend/src/hooks/useFadeIn.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useRef, useEffect } from 'react';
|
||||
|
||||
export function useFadeInRef() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
el.classList.add('fade-in-visible');
|
||||
observer.unobserve(el);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
27
frontend/src/hooks/useTheme.ts
Normal file
27
frontend/src/hooks/useTheme.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored === 'light' || stored === 'dark') return stored;
|
||||
return 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
}, []);
|
||||
|
||||
return { theme, toggleTheme } as const;
|
||||
}
|
||||
37
frontend/src/hooks/useUrlSync.ts
Normal file
37
frontend/src/hooks/useUrlSync.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters } from '../types';
|
||||
import { stateToParams } from '../lib/url-state';
|
||||
|
||||
const URL_DEBOUNCE_MS = 300;
|
||||
|
||||
export function useUrlSync(
|
||||
currentView: { latitude: number; longitude: number; zoom: number } | null,
|
||||
filters: FeatureFilters,
|
||||
features: FeatureMeta[],
|
||||
selectedPOICategories: Set<string>,
|
||||
rightPaneTab: 'pois' | 'properties' | 'area'
|
||||
) {
|
||||
const urlDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlDebounceRef.current) {
|
||||
clearTimeout(urlDebounceRef.current);
|
||||
}
|
||||
urlDebounceRef.current = setTimeout(() => {
|
||||
const params = stateToParams(
|
||||
currentView,
|
||||
filters,
|
||||
features,
|
||||
selectedPOICategories,
|
||||
rightPaneTab
|
||||
);
|
||||
const search = params.toString();
|
||||
const newUrl = search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
||||
window.history.replaceState({ ...window.history.state }, '', newUrl);
|
||||
}, URL_DEBOUNCE_MS);
|
||||
|
||||
return () => {
|
||||
if (urlDebounceRef.current) clearTimeout(urlDebounceRef.current);
|
||||
};
|
||||
}, [currentView, filters, features, selectedPOICategories, rightPaneTab]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue