Skip to content

Commit

Permalink
Frontend improvements for the map (#210)
Browse files Browse the repository at this point in the history
* Avoid content jumping for the loading spinner

* Remove console.logs

* Get rid of Autocomplete warnings

* Update map view only if necessery and add aria-label to each marker

* Improve performance while rendering markers on a map
  • Loading branch information
sSwiergosz authored Sep 24, 2024
1 parent d65a4af commit b7c6ffe
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 52 deletions.
8 changes: 3 additions & 5 deletions src/nursery-nav/src/components/Filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export default function Filters({ defaultVoivodeship, defaultCity, isMobile, cit
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();

console.log(citiesResponse);

const [institutionsAutocomplete, setInstitutionsAutocomplete] = useState<InstitutionAutocomplete[]>([]);
const voivodeships = [
'DOLNOŚLĄSKIE',
Expand Down Expand Up @@ -103,7 +101,7 @@ export default function Filters({ defaultVoivodeship, defaultCity, isMobile, cit
options={cities.filter(city =>
(defaultVoivodeship && city.voivodeship === defaultVoivodeship) ||
(!defaultVoivodeship && (!searchParams.get('voivodeship') || city.voivodeship === searchParams.get('voivodeship'))))?.map(city => city.city) || []}
value={searchParams.get('city') || ''}
value={searchParams.get('city') || null}
onChange={(_event, value) => {
if (!value && searchParams.has('city')) {
searchParams.delete('city');
Expand All @@ -121,10 +119,10 @@ export default function Filters({ defaultVoivodeship, defaultCity, isMobile, cit
}

{!defaultVoivodeship &&
< Autocomplete
<Autocomplete
id="voivodeshipFilter"
options={voivodeships || []}
value={searchParams.get('voivodeship') || ''}
value={searchParams.get('voivodeship') || null}
onChange={(_event, value) => {
if (!value && searchParams.has('voivodeship')) {
searchParams.delete('voivodeship');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function ListComponent({ defaultVoivodeship, defaultCity }: ListC
setTotalPages(data.totalPages);
setInstitutionIds(data.ids);
} catch (error) {
console.log(error);
console.error(error);
}
setLoading(false);
};
Expand Down
78 changes: 54 additions & 24 deletions src/nursery-nav/src/components/MapComponent/MapComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
import { MapContainer, TileLayer } from 'react-leaflet';
import { Box, Button, Container } from '@mui/material';
import MapPin from '../MapPin/MapPin';
import { useContext, useEffect, useMemo, useState } from 'react';
import MarkerClusterGroup from 'react-leaflet-cluster'
import './MapComponent.css';
import { LocationResponse } from '../../shared/nursery.interface';
import { InstitutionContext } from '../Layout/Layout';
import { MapContainer, TileLayer } from 'react-leaflet';
import {
Link as RouterLink,
generatePath,
useParams,
} from 'react-router-dom';

import { Box, Button, Container, debounce } from '@mui/material';
import MarkerClusterGroup from 'react-leaflet-cluster'

import MapPin from '../MapPin/MapPin';
import { InstitutionContext } from '../Layout/Layout';
import PathConstants from '../../shared/pathConstants';

import { LocationResponse } from '../../shared/nursery.interface';

import './MapComponent.css';

interface MapComponentProps {
locations: LocationResponse[];
setIsMapLoaded: (isMapLoaded: boolean) => void;
}

export default function MapComponent({ locations }: MapComponentProps) {
const { institutionIds } = useContext(InstitutionContext);
const [locationsFiltered, setLocationsFiltered] = useState<LocationResponse[]>([]);
const attributionText = 'Powered by <a href="https://www.geoapify.com/" target="_blank">Geoapify</a> | <a href="https://openmaptiles.org/" target="_blank">© OpenMapTiles</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap</a> contributors';
const mapUrl = `https://maps.geoapify.com/v1/tile/positron/{z}/{x}/{y}.png?apiKey=${process.env.REACT_APP_GEOAPIFY_API_KEY}`;

const mapUrl = `https://maps.geoapify.com/v1/tile/positron/{z}/{x}/{y}.png?apiKey=${process.env.REACT_APP_GEOAPIFY_API_KEY}`;
const attributionText = 'Powered by <a href="https://www.geoapify.com/" target="_blank">Geoapify</a> | <a href="https://openmaptiles.org/" target="_blank">© OpenMapTiles</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap</a> contributors';
const isXs = window.innerWidth < 600;
const isSm = window.innerWidth < 900;
function useFilteredLocations(locations: LocationResponse[], institutionIds: number[]) {
return useMemo(() => {
if (institutionIds.length === 0) {
return locations;
}

return locations.filter((location) => institutionIds.includes(location.id));
}, [locations, institutionIds]);
}

export default function MapComponent({ locations, setIsMapLoaded }: MapComponentProps) {
const { institutionIds } = useContext(InstitutionContext);
const { id } = useParams();
const selectedLocation = id ? locations.find((location) => location.id === parseInt(id)) : undefined;
const [size, setSize] = useState({
isXs: window.innerWidth < 600,
isSm: window.innerWidth < 900,
});
const locationsFiltered = useFilteredLocations(locations, institutionIds);

const selectedLocation = useMemo(
() => id ? locations.find((location) => location.id === parseInt(id)) : undefined,
[id, locations]
);

const markers = useMemo(() => locationsFiltered.map((location) => (
<MapPin
Expand All @@ -42,13 +62,19 @@ export default function MapComponent({ locations }: MapComponentProps) {
)), [locationsFiltered, selectedLocation]);

useEffect(() => {
if (institutionIds.length === 0) {
setLocationsFiltered(locations);
}
else {
setLocationsFiltered(locations.filter((location) => institutionIds.includes(location.id)));
const handleResize = debounce(() => {
setSize({
isXs: window.innerWidth < 600,
isSm: window.innerWidth < 900,
});
}, 200);

window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
handleResize.clear();
}
}, [institutionIds, locations]);
}, []);

return (
<Box>
Expand All @@ -61,20 +87,24 @@ export default function MapComponent({ locations }: MapComponentProps) {
<MapContainer
preferCanvas={true}
center={[52.5, 19.14]}
zoom={isXs ? 6 : 7}
zoom={size.isXs ? 6 : 7}
scrollWheelZoom={true}
zoomControl={false}
style={{ position: 'fixed', top: 0, bottom: 0, width: isSm ? '100%' : '50%' }}
style={{ position: 'fixed', top: 0, bottom: 0, width: size.isSm ? '100%' : '50%' }}
>
<TileLayer
attribution={attributionText}
url={mapUrl}
maxZoom={20}
eventHandlers={{
load: () => setIsMapLoaded(true),
}}
/>
<MarkerClusterGroup
className="marker-cluster-group"
polygonOptions={{ opacity: 0 }}
chunkedLoading>
chunkedLoading
>
{markers}
</MarkerClusterGroup>
</MapContainer>
Expand Down
24 changes: 15 additions & 9 deletions src/nursery-nav/src/components/MapPin/MapPin.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useEffect } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { useNavigate, generatePath } from 'react-router-dom';
import { Marker, useMap } from 'react-leaflet';
import { Crib } from '@mui/icons-material';

import { divIcon } from 'leaflet';
import { renderToStaticMarkup } from 'react-dom/server';
import { Crib } from '@mui/icons-material';

import { InstitutionType } from '../../shared/nursery.interface';
import './MapPin.css';
import { useNavigate, generatePath } from 'react-router-dom';
import PathConstants from '../../shared/pathConstants';

import './MapPin.css';

export interface MapPinProps {
institutionType: InstitutionType;
id: number;
Expand All @@ -20,18 +24,20 @@ export default function MapPin(props: MapPinProps) {
const map = useMap();
const navigate = useNavigate();

if (props.selectedLocationLat !== undefined && props.selectedLocationLon !== undefined) {
map.setView([props.selectedLocationLat, props.selectedLocationLon], 18, {
animate: true
});
}
// Update map view only if necessary
useEffect(() => {
if (props.selectedLocationLat !== undefined && props.selectedLocationLon !== undefined) {
map.setView([props.selectedLocationLat, props.selectedLocationLon], 18, { animate: true });
}
}, [props.selectedLocationLat, props.selectedLocationLon, map]);

const position: [number, number] = [props.latitude, props.longitude];
const institutionType = props.institutionType === InstitutionType.NURSERY ? 'nursery' : 'childclub';
const iconBackgroundColor = `map-pin-icon map-pin-icon-${institutionType}`;

return (
<Marker
aria-label={`Details for ${institutionType} with ID ${props.id}`}
eventHandlers={{
click: () => {
navigate(generatePath(PathConstants.INSTITUTION_DETAILS, { id: props.id }));
Expand Down
5 changes: 3 additions & 2 deletions src/nursery-nav/src/pages/InstitutionDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function InstitutionDetailsPage() {
const [locations, setLocations] = useState<LocationResponse[]>([]);
const [selectedInstitution, setSelectedInstitution] = useState<Institution | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isMapLoaded, setIsMapLoaded] = useState(false);

useEffect(() => {
const fetchInstitution = async () => {
Expand Down Expand Up @@ -61,8 +62,8 @@ export default function InstitutionDetailsPage() {
{selectedInstitution && <InstitutionDetails {...selectedInstitution} />}
</Grid>
<Grid item display={{ xs: "none", md: "block" }} md={6}>
{isLoading ? <CircularProgress /> : <MapComponent locations={locations} />}
{(isLoading && !isMapLoaded) ? <CircularProgress /> : <MapComponent locations={locations} setIsMapLoaded={setIsMapLoaded} />}
</Grid>
</>
);
}
}
17 changes: 9 additions & 8 deletions src/nursery-nav/src/pages/ListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Grid, CircularProgress } from "@mui/material";
import { Grid, CircularProgress, Box } from "@mui/material";
import ListComponent from "../components/ListComponent/ListComponent";
import MapComponent from "../components/MapComponent/MapComponent";
import FiltersBar from "../components/Filters/FiltersBar";
Expand All @@ -18,6 +18,7 @@ export default function ListPage() {
const [isMobile, setIsMobile] = useState(window.innerWidth < 600);
const [locations, setLocations] = useState<LocationResponse[]>([]);
const [isLoading, setIsLoading] = useState(true); // Add loading state
const [isMapLoaded, setIsMapLoaded] = useState(false);
const [cities, setCities] = useState<getCitiesResponse[]>();

useLayoutEffect(() => {
Expand All @@ -31,13 +32,11 @@ export default function ListPage() {

useEffect(() => {
const fetchData = async () => {
setIsLoading(true); // Set loading state to true before fetching data
const locations = await getLocations();
setLocations(locations);
setIsLoading(false); // Set loading state to false after fetching data
};

fetchData();
fetchData().then(() => setIsLoading(false));
}, []);

useEffect(() => {
Expand Down Expand Up @@ -76,10 +75,12 @@ export default function ListPage() {
</Grid>
{!isMobile && (
<Grid item xs={12} md={6} sx={{ display: { xs: "none", md: "block" } }}>
{isLoading ? (
<CircularProgress /> // Render loading animation when isLoading is true
{(isLoading && !isMapLoaded) ? (
<Box alignItems="center" justifyContent={"center"} display="flex" height="100%" width="100%">
<CircularProgress />
</Box>
) : (
<MapComponent locations={locations} />
<MapComponent locations={locations} setIsMapLoaded={setIsMapLoaded} />
)}
</Grid>
)}
Expand All @@ -95,4 +96,4 @@ function getTitle(voivodeship: string | undefined, city: string | undefined) {
title = `Żłobki ${voivodeship.toLocaleUpperCase()} - ` + title;
}
return title;
}
}
8 changes: 5 additions & 3 deletions src/nursery-nav/src/pages/MapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function MapPage() {

const [locations, setLocations] = useState<LocationResponse[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isMapLoaded, setIsMapLoaded] = useState(false);

useEffect(() => {
const fetchData = async () => {
Expand Down Expand Up @@ -56,11 +57,12 @@ export default function MapPage() {
<ListComponent defaultVoivodeship={voivodeship} defaultCity={city} />
</Grid>
<Grid item xs={12} md={6}>
{isLoading ?
{(isLoading && !isMapLoaded) ?
<CircularProgress />
:
<MapComponent locations={locations} />}
<MapComponent locations={locations} setIsMapLoaded={setIsMapLoaded} />
}
</Grid>
</>
);
}
}

0 comments on commit b7c6ffe

Please sign in to comment.