turash/bugulma/frontend/hooks/map/useSitesByBounds.ts
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- Remove nested git repository from bugulma/frontend/.git
- Add all frontend files to main repository tracking
- Convert from separate frontend/backend repos to unified monorepo
- Preserve all frontend code and development history as tracked files
- Eliminate nested repository complexity for simpler development workflow

This creates a proper monorepo structure with frontend and backend
coexisting in the same repository for easier development and deployment.
2025-11-25 06:02:57 +01:00

101 lines
3.0 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { LatLngBounds } from 'leaflet';
import { useMemo } from 'react';
import type { BackendSite } from '@/schemas/backend/site';
import { getNearbySites } from '@/services/sites-api';
import { calculateDistance } from '@/utils/coordinates.ts';
/**
* Calculate center and radius from map bounds
*/
function boundsToCenterAndRadius(bounds: LatLngBounds): {
lat: number;
lng: number;
radius: number;
} {
const center = bounds.getCenter();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
// Calculate radius as the distance from center to corner
const radius = Math.max(
calculateDistance(center.lat, center.lng, ne.lat, ne.lng),
calculateDistance(center.lat, center.lng, sw.lat, sw.lng)
);
// Add 20% buffer to ensure we get all sites near the edges
return {
lat: center.lat,
lng: center.lng,
radius: radius * 1.2,
};
}
/**
* Hook to fetch sites within current map bounds
* This enables viewport-based lazy loading of organizations
* Optimized with better caching and query key stability
*/
export const useSitesByBounds = (bounds: LatLngBounds | null, enabled: boolean = true) => {
const queryParams = useMemo(() => {
if (!bounds) return null;
const params = boundsToCenterAndRadius(bounds);
// Round coordinates to reduce query key churn (0.001 degrees ≈ 111m)
return {
lat: Math.round(params.lat * 1000) / 1000,
lng: Math.round(params.lng * 1000) / 1000,
radius: Math.round(params.radius * 10) / 10, // Round to 0.1km
};
}, [bounds]);
return useQuery({
queryKey: ['sites', 'nearby', queryParams?.lat, queryParams?.lng, queryParams?.radius],
queryFn: () => {
if (!queryParams) return Promise.resolve([]);
return getNearbySites({
lat: queryParams.lat,
lng: queryParams.lng,
radius: queryParams.radius,
});
},
enabled: enabled && !!queryParams,
placeholderData: [], // Render immediately with empty array - don't block on API
staleTime: 60 * 1000, // 1 minute - sites don't change often
gcTime: 10 * 60 * 1000, // 10 minutes - keep in cache longer
refetchOnWindowFocus: false, // Don't refetch on window focus
refetchOnMount: false, // Don't refetch if data exists
retry: 1, // Only retry once on failure
});
};
/**
* Create a map of organization ID -> sites for organizations in viewport
*/
export const useOrganizationSitesByBounds = (
bounds: LatLngBounds | null,
enabled: boolean = true
) => {
const { data: sites, isLoading } = useSitesByBounds(bounds, enabled);
const orgSitesMap = useMemo(() => {
const map = new Map<string, BackendSite[]>();
if (!sites) return map;
sites.forEach((site) => {
const orgId = site.OwnerOrganizationID;
if (!orgId) return;
const existing = map.get(orgId) || [];
map.set(orgId, [...existing, site]);
});
return map;
}, [sites]);
return {
orgSitesMap,
sites: sites || [],
isLoading,
};
};