{ - const t = await getTranslations('site'); const content = await getPage(slug); if (content === null) { return; diff --git a/src/components/map-filters.tsx b/src/components/map-filters.tsx index 8e3f3df..5ec513d 100644 --- a/src/components/map-filters.tsx +++ b/src/components/map-filters.tsx @@ -1,8 +1,8 @@ import { useCallback } from 'react'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useMapContext } from '@/context/map'; +import { useTranslations } from 'next-intl'; -import { getUrlSearchParamsForLayers, partition } from '@/lib/utils'; +import { partition } from '@/lib/utils'; import { Accordion, AccordionContent, @@ -14,29 +14,32 @@ import { Label } from './ui/label'; import { Switch } from './ui/switch'; export default function MapFilters() { - const { settings } = useMapContext(); - const router = useRouter(); - const pathname = usePathname(); - const params = useSearchParams(); - const layersFromParams = params.get('layers') ?? ''; + const { settings, layers, toggleLayer } = useMapContext(); + const t = useTranslations(); const handleChange = useCallback( (id: number, isActive: boolean) => { - const nextLayerSearchParams = getUrlSearchParamsForLayers( - layersFromParams, - [id], - isActive, - ); - const textFromParams = params.get('text') - ? `&text=${params.get('text')}` - : ''; - router.push(`${pathname}${nextLayerSearchParams}${textFromParams}`); + toggleLayer(id, isActive); }, - [layersFromParams, params, router, pathname], + [toggleLayer], ); - if (settings === null || settings.layersTree.length === 0) { - return null; + if ( + settings === null || + settings.layersTree.length === 0 || + layers === null + ) { + return ( +
+

{t('site.loading')}

+ {Array.from({ length: 4 }, () => ( +
+
+
+
+ ))} +
+ ); } const [grouped, [notGrouped]] = partition( @@ -44,10 +47,9 @@ export default function MapFilters() { layer => layer.label !== null, ); - const defaultActivatedLayers = layersFromParams - .split(',') - .filter(Boolean) - .map(Number); + const activatedLayers = layers + .filter(({ isActive }) => isActive) + .map(({ id }) => id); return ( <> @@ -75,7 +77,11 @@ export default function MapFilters() { e.id === layer.id)?.geojson && + activatedLayers.includes(layer.id) + } onCheckedChange={isActive => handleChange(layer.id, isActive) } @@ -100,7 +106,11 @@ export default function MapFilters() { e.id === layer.id)?.geojson && + activatedLayers.includes(layer.id) + } onCheckedChange={isActive => handleChange(layer.id, isActive)} /> diff --git a/src/components/metadata-list.tsx b/src/components/metadata-list.tsx index 20ce88b..80c09b5 100644 --- a/src/components/metadata-list.tsx +++ b/src/components/metadata-list.tsx @@ -45,7 +45,7 @@ const MetadataItem = ({
{t(type)} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 107f57e..2521f5c 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -2,28 +2,44 @@ import * as React from 'react'; import * as SwitchPrimitives from '@radix-ui/react-switch'; +import { useTranslations } from 'next-intl'; import { cn } from '@/lib/utils'; +import { Icons } from '../icons'; + const Switch = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - & { + isLoading?: boolean; + } +>(({ className, isLoading, ...props }, ref) => { + const t = useTranslations(); + return ( + - -)); + {...props} + ref={ref} + > + + {isLoading && ( + + )} + + + ); +}); Switch.displayName = SwitchPrimitives.Root.displayName; export { Switch }; diff --git a/src/context/map.tsx b/src/context/map.tsx index 24d30a8..03e62dd 100644 --- a/src/context/map.tsx +++ b/src/context/map.tsx @@ -8,14 +8,12 @@ import { useMemo, useState, } from 'react'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { getGeoJSON } from '@/api/geojson'; -import { Layer, Settings } from '@/api/settings'; +import { Layer, Settings, getMapSettings } from '@/api/settings'; import { FeatureCollection, Point } from 'geojson'; import { Map } from 'leaflet'; -import { getUrlSearchParamsForLayers, partition } from '@/lib/utils'; - type MapContextProps = { layers: Layer[] | null; map: Map | null; @@ -29,7 +27,6 @@ type MapContextProps = { type MapContextProviderProps = { children: React.ReactNode; - defaultSettings: Settings['map'] | null; }; export const MapContext = createContext({ @@ -45,25 +42,47 @@ export const MapContext = createContext({ export const useMapContext = () => useContext(MapContext); -export const MapContextProvider = ({ - defaultSettings, - children, -}: MapContextProviderProps) => { - const router = useRouter(); - const pathname = usePathname(); +export const MapContextProvider = ({ children }: MapContextProviderProps) => { const params = useSearchParams(); const layersFromParams = params.get('layers') ?? ''; + const [defaultSettings, setDefaultSettings] = useState< + Settings['map'] | null + >(null); + + // Fetch settings on first render + useEffect(() => { + (getMapSettings() as Promise).then(settings => + setDefaultSettings(settings), + ); + }, []); + const settings = useMemo( () => defaultSettings?.layersTree.flatMap(group => group.layers) ?? null, - [defaultSettings?.layersTree], + [defaultSettings], ); - const [layers, setLayers] = useState( - () => - settings?.map(item => ({ ...item, isActive: item.defaultActive })) ?? - null, - ); + const [layers, setLayers] = useState(null); + + // When settings are loaded, initialize layers + useEffect(() => { + if (layersFromParams !== '') { + // if there's layers in the URL, those are the active ones + setLayers( + settings?.map(item => ({ + ...item, + isActive: layersFromParams.split(',').map(Number).includes(item.id), + })) ?? null, + ); + } else { + // If not, just use the default ones + setLayers( + settings?.map(item => ({ ...item, isActive: item.defaultActive })) ?? + null, + ); + } + }, [settings, layersFromParams]); + const [map, setMap] = useState(null); const [observationCoordinates, setObservationCoordinates] = useState(null); @@ -82,64 +101,70 @@ export const MapContextProvider = ({ if (currentLayer.isActive === isActive) { return; } + + setLayers( + prevLayers => + prevLayers?.map(layer => { + if (layer.id === currentLayer.id) { + return { ...layer, isActive }; + } + return layer; + }) ?? [], + ); + + if (currentLayer.geojson) return; + const geojson = currentLayer.geojson ?? (await getGeoJSON(currentLayer.geojsonUrl)); - setLayers(prevLayers => { - if (prevLayers === null) { - return prevLayers; - } - return prevLayers.reduce((list: Layer[], layer) => { - const { defaultActive, ...nextLayer } = layer; - if (nextLayer.id !== id) { - list.push(nextLayer); - } else { - list.push({ - ...nextLayer, - isActive, - geojson: geojson as FeatureCollection, - }); - } - return list; - }, []); - }); + setLayers( + prevLayers => + prevLayers?.map(layer => { + if (layer.id === currentLayer.id) { + return { ...layer, geojson: geojson as FeatureCollection }; + } + return layer; + }) ?? [], + ); }, [getLayerById], ); + // When active layers are changed, fetch the geojson useEffect(() => { - if ( - layersFromParams === '' && - layers?.some(item => 'defaultActive' in item) - ) { - const layersID = layers - .filter(item => item.defaultActive) - .map(({ id }) => id); - const nextLayerSearchParams = getUrlSearchParamsForLayers( - layersFromParams, - layersID, - true, - ); - const text = params.get('text'); - router.replace( - `${pathname}${nextLayerSearchParams}${ - text ? `&text=${encodeURIComponent(text)}` : '' - }`, - ); - } - }, [layers, layersFromParams, params, pathname, router]); + layers + ?.filter(({ isActive }) => isActive) + .forEach(async currentLayer => { + if (!currentLayer.geojson) { + const geojson = await getGeoJSON(currentLayer.geojsonUrl); + setLayers( + prevLayers => + prevLayers?.map(layer => { + if (layer.id === currentLayer.id) { + return { ...layer, geojson: geojson as FeatureCollection }; + } + return layer; + }) ?? [], + ); + } + }); + }, [layers]); + // Update URL when active layers are changed useEffect(() => { - const [activatedLayers, disabledLayers] = partition(layers ?? [], item => - layersFromParams.split(',').map(Number).includes(item.id), + const textFromParams = params.get('text') + ? `&text=${params.get('text')}` + : ''; + const nextLayerSearchParams = `?layers=${layers + ?.filter(({ isActive }) => isActive) + .map(({ id }) => id) + .join(',')}`; + window.history.pushState( + null, + '', + `${nextLayerSearchParams}${textFromParams}`, ); - activatedLayers.forEach(item => toggleLayer(item.id, true)); - disabledLayers.forEach(item => toggleLayer(item.id, false)); - }, [layers, layersFromParams, toggleLayer]); - - if (!defaultSettings && !layers) { - return null; - } + }, [layers, params]); return ( .leaflet-tooltip:not(:last-child) { display: none; } + + +.skeleton-animation { + background: linear-gradient(90deg, hsl(var(--muted-foreground)) 0%, hsl(var(--foreground)) 50%, hsl(var(--muted-foreground)) 100%); + background-size: 300% 100%; + opacity: 0.2; + animation: skeletonator 4s ease infinite; +} + +@keyframes skeletonator { + 0% { + background-position-x: 0%; + } + 50% { + background-position-x: 100%; + } + 100% { + background-position-x: 0%; + } +} diff --git a/translations/fr.json b/translations/fr.json index fa58182..02ee7c0 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -86,7 +86,8 @@ "menu": "Menu", "openMenu": "Ouvrir le menu", "prev": "Précédent", - "next": "Suivant" + "next": "Suivant", + "loading": "Chargement" }, "theme": { "toggle": "Thème {theme}",