This commit is contained in:
Andras Schmelczer 2026-02-02 21:56:35 +00:00
parent 2c613dc0d1
commit a677b9331f
28 changed files with 1647 additions and 1498 deletions

View 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]);
}

View 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;
}

View 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;
}

View 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]);
}