From 4454ac73b964e75c1da8c5542bad5cfaea8bbdc2 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 09:42:51 +0200 Subject: [PATCH 1/8] Refactor fetches and create a reusable fetch method --- src/nursery-nav/src/api/CitiesFetcher.ts | 15 ++--- .../src/api/InstitutionsFetcher.ts | 64 +++++++------------ src/nursery-nav/src/api/LocationsFetcher.ts | 15 ++--- src/nursery-nav/src/api/fetcher.ts | 11 ++++ 4 files changed, 45 insertions(+), 60 deletions(-) create mode 100644 src/nursery-nav/src/api/fetcher.ts diff --git a/src/nursery-nav/src/api/CitiesFetcher.ts b/src/nursery-nav/src/api/CitiesFetcher.ts index 10926f5..726f154 100644 --- a/src/nursery-nav/src/api/CitiesFetcher.ts +++ b/src/nursery-nav/src/api/CitiesFetcher.ts @@ -1,19 +1,16 @@ -import axios from "axios"; +import { fetchFromApi } from './fetcher'; export interface getCitiesResponse { city: string; voivodeship: string; } +const API_URL = process.env.REACT_APP_API_URL; + export const getCities = async (): Promise => { - const url = `${process.env.REACT_APP_API_URL}/cities`; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch cities'); - } + const url = `${API_URL}/cities`; - const data = res.data as getCitiesResponse[]; - return data; + return fetchFromApi(url); } -export { } \ No newline at end of file +export { } diff --git a/src/nursery-nav/src/api/InstitutionsFetcher.ts b/src/nursery-nav/src/api/InstitutionsFetcher.ts index 76d2b59..ae520f5 100644 --- a/src/nursery-nav/src/api/InstitutionsFetcher.ts +++ b/src/nursery-nav/src/api/InstitutionsFetcher.ts @@ -1,5 +1,5 @@ -import axios from "axios"; import { Institution, InstitutionListItem } from "../shared/nursery.interface"; +import { fetchFromApi } from './fetcher'; export interface getInstitutionsResponse { items: InstitutionListItem[], @@ -13,55 +13,35 @@ export interface getInstitutionsAutocompleteResponse { name: string } -export const getInstitutions = async (searchParams: URLSearchParams, pageNum?: number | null): Promise => { - let url = `${process.env.REACT_APP_API_URL}/institutions`; - if (pageNum) { - url += `?page=${pageNum}&${searchParams}`; - } - else { - url += `?${searchParams}`; - } +const API_URL = process.env.REACT_APP_API_URL; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch institutions'); - } +export const getInstitutions = async ( + searchParams: URLSearchParams, + pageNum?: number | null +): Promise => { + const url = `${API_URL}/institutions`; + const params = { page: pageNum || undefined, ...Object.fromEntries(searchParams) }; - const data = res.data as getInstitutionsResponse; - return data; -} + return fetchFromApi(url, params); +}; export const getInstitutionDetails = async (id: number): Promise => { - const url = `${process.env.REACT_APP_API_URL}/institutions/details/${id}`; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch institution details'); - } - - const data = res.data as Institution; - return data; -} + const url = `${API_URL}/institutions/details/${id}`; + return fetchFromApi(url); +}; export const getInstitutionsDetails = async (ids: number[]): Promise => { - const url = `${process.env.REACT_APP_API_URL}/institutions/details?id=${ids.join('&id=')}`; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch institution details'); - } + const url = `${API_URL}/institutions/details`; + const params = { id: ids }; - const data = res.data as Institution[]; - return data; -} + return fetchFromApi(url, params); +}; export const getInstitutionAutocomplete = async (search: string): Promise => { - const url = `${process.env.REACT_APP_API_URL}/institutions/autocomplete?search=${search}`; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch institution autocomplete'); - } + const url = `${API_URL}/institutions/autocomplete`; + const params = { search }; - const data = res.data as getInstitutionsAutocompleteResponse[]; - return data; -} + return fetchFromApi(url, params); +}; -export { } \ No newline at end of file +export { } diff --git a/src/nursery-nav/src/api/LocationsFetcher.ts b/src/nursery-nav/src/api/LocationsFetcher.ts index 25394a6..9dd798b 100644 --- a/src/nursery-nav/src/api/LocationsFetcher.ts +++ b/src/nursery-nav/src/api/LocationsFetcher.ts @@ -1,15 +1,12 @@ -import axios from "axios"; +import { fetchFromApi } from './fetcher'; import { LocationResponse } from "../shared/nursery.interface"; +const API_URL = process.env.REACT_APP_API_URL; + export const getLocations = async (): Promise => { - const url = `${process.env.REACT_APP_API_URL}/locations`; - const res = await axios.get(url); - if (res.status !== 200) { - throw new Error('Failed to fetch locations'); - } - - const data = res.data as LocationResponse[]; - return data; + const url = `${API_URL}/locations`; + + return fetchFromApi(url); } export { } diff --git a/src/nursery-nav/src/api/fetcher.ts b/src/nursery-nav/src/api/fetcher.ts new file mode 100644 index 0000000..7aeb4b8 --- /dev/null +++ b/src/nursery-nav/src/api/fetcher.ts @@ -0,0 +1,11 @@ +import axios from "axios"; + +export const fetchFromApi = async (url: string, params?: object): Promise => { + const { data, status } = await axios.get(url, { params }); + + if (status !== 200) { + throw new Error(`Failed to fetch data from ${url}. Status: ${status}`); + } + + return data; +}; From 2018d77da0d5fbf086caa8a1d44e212f63432718 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 09:43:56 +0200 Subject: [PATCH 2/8] Refactor fetching data for filters and getting unique cities values --- .../src/components/Filters/Filters.tsx | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/src/nursery-nav/src/components/Filters/Filters.tsx b/src/nursery-nav/src/components/Filters/Filters.tsx index f664656..126babd 100644 --- a/src/nursery-nav/src/components/Filters/Filters.tsx +++ b/src/nursery-nav/src/components/Filters/Filters.tsx @@ -1,14 +1,17 @@ +import { useState } from "react"; +import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; + import { FormControlLabel, Autocomplete, TextField, debounce, RadioGroup, Stack } from "@mui/material"; import Radio from '@mui/material/Radio'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; -import { useState } from "react"; -import { InstitutionAutocomplete, InstitutionType } from "../../shared/nursery.interface"; -import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; -import PathConstants from "../../shared/pathConstants"; + import { getInstitutionAutocomplete } from "../../api/InstitutionsFetcher"; import { getCitiesResponse } from "../../api/CitiesFetcher"; +import PathConstants from "../../shared/pathConstants"; +import { InstitutionAutocomplete, InstitutionType } from "../../shared/nursery.interface"; + interface FiltersProps { defaultVoivodeship?: string; defaultCity?: string; @@ -16,35 +19,45 @@ interface FiltersProps { citiesResponse?: getCitiesResponse[]; } +interface City { + city: string; + voivodeship: string; +} + +const voivodeships = [ + 'DOLNOŚLĄSKIE', + 'KUJAWSKO-POMORSKIE', + 'LUBELSKIE', + 'LUBUSKIE', + 'ŁÓDZKIE', + 'MAŁOPOLSKIE', + 'MAZOWIECKIE', + 'OPOLSKIE', + 'PODKARPACKIE', + 'PODLASKIE', + 'POMORSKIE', + 'ŚLĄSKIE', + 'ŚWIĘTOKRZYSKIE', + 'WARMIŃSKO-MAZURSKIE', + 'WIELKOPOLSKIE', + 'ZACHODNIOPOMORSKIE', +] as const; + export default function Filters({ defaultVoivodeship, defaultCity, isMobile, citiesResponse }: FiltersProps) { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const [institutionsAutocomplete, setInstitutionsAutocomplete] = useState([]); - const voivodeships = [ - 'DOLNOŚLĄSKIE', - 'KUJAWSKO-POMORSKIE', - 'LUBELSKIE', - 'LUBUSKIE', - 'ŁÓDZKIE', - 'MAŁOPOLSKIE', - 'MAZOWIECKIE', - 'OPOLSKIE', - 'PODKARPACKIE', - 'PODLASKIE', - 'POMORSKIE', - 'ŚLĄSKIE', - 'ŚWIĘTOKRZYSKIE', - 'WARMIŃSKO-MAZURSKIE', - 'WIELKOPOLSKIE', - 'ZACHODNIOPOMORSKIE', - ]; - - - const citiesUnique = (citiesResponse || []) - .map(city => ({ city: city?.city, voivodeship: city?.voivodeship })) - .filter((city, index, self) => self.findIndex(c => c.city === city.city) === index); - const cities = citiesUnique.filter(city => city !== undefined && city !== null); + + const cities = (citiesResponse || []) + .reduce((uniqueCities, { city, voivodeship }) => { + // Check if city exists and if it's already in the uniqueCities array + if (city && !uniqueCities.some(c => c.city === city)) { + // If not, add the city to the uniqueCities array + uniqueCities.push({ city, voivodeship }); + } + return uniqueCities; + }, []); const getAutocompleteData = async (value: string) => { const institutions = await getInstitutionAutocomplete(value); @@ -52,12 +65,7 @@ export default function Filters({ defaultVoivodeship, defaultCity, isMobile, cit } const onInputChange = (_event: any, value: string | null) => { - if (value) { - getAutocompleteData(value); - } - else { - setInstitutionsAutocomplete([]); - } + value ? getAutocompleteData(value) : setInstitutionsAutocomplete([]); } const goToDetails = (_event: any, value: InstitutionAutocomplete | null) => { @@ -67,12 +75,7 @@ export default function Filters({ defaultVoivodeship, defaultCity, isMobile, cit } const handleInstitutionTypeFilter = (value: string) => { - if (value === 'ALL') { - searchParams.delete('insType'); - } - else { - searchParams.set('insType', value); - } + value === 'ALL' ? searchParams.delete('insType') : searchParams.set('insType', value); setSearchParams(searchParams); } From 2971600694434ad29c72056d706d5b423817fa9d Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 09:47:20 +0200 Subject: [PATCH 3/8] Debounce window resizing on list page --- src/nursery-nav/src/pages/ListPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nursery-nav/src/pages/ListPage.tsx b/src/nursery-nav/src/pages/ListPage.tsx index 10b46b1..568477f 100644 --- a/src/nursery-nav/src/pages/ListPage.tsx +++ b/src/nursery-nav/src/pages/ListPage.tsx @@ -1,4 +1,4 @@ -import { Grid, CircularProgress, Box } from "@mui/material"; +import { Grid, CircularProgress, Box, debounce } from "@mui/material"; import ListComponent from "../components/ListComponent/ListComponent"; import MapComponent from "../components/MapComponent/MapComponent"; import FiltersBar from "../components/Filters/FiltersBar"; @@ -22,9 +22,9 @@ export default function ListPage() { const [cities, setCities] = useState(); useLayoutEffect(() => { - const handleResize = () => { + const handleResize = debounce(() => { setIsMobile(window.innerWidth < 600); - }; + }, 150); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); From 62e23ef659c957a559e31d91dd79696f3f9fb051 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 09:47:50 +0200 Subject: [PATCH 4/8] Add .idea directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0ebae5d..94040e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ secrets.txt *.env +.idea/ From 452ae390989da21aa9a3345e3c30d3569f44ef40 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 09:50:49 +0200 Subject: [PATCH 5/8] Fetch cities and locations data together and resolve the promise after both promises has been returned --- src/nursery-nav/src/pages/ListPage.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/nursery-nav/src/pages/ListPage.tsx b/src/nursery-nav/src/pages/ListPage.tsx index 568477f..a2cc47d 100644 --- a/src/nursery-nav/src/pages/ListPage.tsx +++ b/src/nursery-nav/src/pages/ListPage.tsx @@ -31,21 +31,17 @@ export default function ListPage() { }, []); useEffect(() => { - const fetchData = async () => { + const fetchLocations = async () => { const locations = await getLocations(); setLocations(locations); }; - fetchData().then(() => setIsLoading(false)); - }, []); - - useEffect(() => { const fetchCities = async () => { - const response = await getCities(); - setCities(response); + const cities = await getCities(); + setCities(cities); }; - fetchCities(); + Promise.all([fetchLocations(), fetchCities()]).then(() => setIsLoading(false)); }, []); const title = getTitle(voivodeship, city); From 26a62533c5570307431ba0e5d4ea79dbba39f512 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 10:00:33 +0200 Subject: [PATCH 6/8] Extract Helmet metadata component to a separate one --- .../src/components/Metadata/Metadata.tsx | 25 +++++++++++++++++++ src/nursery-nav/src/pages/AboutPage.tsx | 18 +++---------- src/nursery-nav/src/pages/ComparisonPage.tsx | 20 ++++----------- .../src/pages/InstitutionDetailsPage.tsx | 22 ++++++---------- src/nursery-nav/src/pages/ListPage.tsx | 24 ++++++------------ src/nursery-nav/src/pages/MapPage.tsx | 22 ++++++---------- .../src/pages/PageNotFoundPage.tsx | 11 +++----- 7 files changed, 60 insertions(+), 82 deletions(-) create mode 100644 src/nursery-nav/src/components/Metadata/Metadata.tsx diff --git a/src/nursery-nav/src/components/Metadata/Metadata.tsx b/src/nursery-nav/src/components/Metadata/Metadata.tsx new file mode 100644 index 0000000..5deff7b --- /dev/null +++ b/src/nursery-nav/src/components/Metadata/Metadata.tsx @@ -0,0 +1,25 @@ +import { Helmet } from "react-helmet-async"; + +interface MetadataProps { + title: string; + description?: string; + image: string; + url?: string; +} + +export default function Metadata({ title, description, image, url }: MetadataProps) { + return ( + + {title} + + + + + + + + + + + ) +}; diff --git a/src/nursery-nav/src/pages/AboutPage.tsx b/src/nursery-nav/src/pages/AboutPage.tsx index 0ab596d..47b2ba1 100644 --- a/src/nursery-nav/src/pages/AboutPage.tsx +++ b/src/nursery-nav/src/pages/AboutPage.tsx @@ -1,5 +1,6 @@ import { Box, Container, Link, List, ListItem, ListItemText, Paper, Stack, Typography } from "@mui/material"; -import { Helmet } from "react-helmet-async"; + +import Metadata from "../components/Metadata/Metadata"; export default function AboutPage() { const title = `O aplikacji - ${process.env.REACT_APP_NAME}`; @@ -8,18 +9,7 @@ export default function AboutPage() { return ( <> - - {title} - - - - - - - - - - + @@ -69,4 +59,4 @@ export default function AboutPage() { ); -} \ No newline at end of file +} diff --git a/src/nursery-nav/src/pages/ComparisonPage.tsx b/src/nursery-nav/src/pages/ComparisonPage.tsx index 18939c8..c8a590b 100644 --- a/src/nursery-nav/src/pages/ComparisonPage.tsx +++ b/src/nursery-nav/src/pages/ComparisonPage.tsx @@ -1,7 +1,8 @@ +import { useSearchParams } from "react-router-dom"; import { Box, Container, Grid, Stack, Typography } from "@mui/material"; -import { Helmet } from "react-helmet-async"; + import Comparison from "../components/Comparison/Comparison"; -import { useSearchParams } from "react-router-dom"; +import Metadata from "../components/Metadata/Metadata"; export default function ComparisonPage() { @@ -18,18 +19,7 @@ export default function ComparisonPage() { return ( <> - - {title} - - - - - - - - - - + {displayError && @@ -50,4 +40,4 @@ export default function ComparisonPage() { ); -} \ No newline at end of file +} diff --git a/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx b/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx index 6efa6b7..8b9da72 100644 --- a/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx +++ b/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx @@ -1,11 +1,14 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; import { CircularProgress, Grid } from "@mui/material"; + import MapComponent from "../components/MapComponent/MapComponent"; +import Metadata from "../components/Metadata/Metadata"; import InstitutionDetails from "../components/InstitutionDetails/InstitutionDetails"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { Helmet } from "react-helmet-async"; + import { getInstitutionDetails } from "../api/InstitutionsFetcher"; import { getLocations } from "../api/LocationsFetcher"; + import { Institution, LocationResponse } from "../shared/nursery.interface"; export default function InstitutionDetailsPage() { @@ -46,18 +49,7 @@ export default function InstitutionDetailsPage() { return ( <> - - {title} - - - - - - - - - - + {selectedInstitution && } diff --git a/src/nursery-nav/src/pages/ListPage.tsx b/src/nursery-nav/src/pages/ListPage.tsx index a2cc47d..179b835 100644 --- a/src/nursery-nav/src/pages/ListPage.tsx +++ b/src/nursery-nav/src/pages/ListPage.tsx @@ -1,14 +1,17 @@ +import { useEffect, useLayoutEffect, useState } from "react"; +import { useParams } from "react-router-dom"; import { Grid, CircularProgress, Box, debounce } from "@mui/material"; + import ListComponent from "../components/ListComponent/ListComponent"; import MapComponent from "../components/MapComponent/MapComponent"; import FiltersBar from "../components/Filters/FiltersBar"; -import { Helmet } from "react-helmet-async"; -import { useEffect, useLayoutEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import Metadata from "../components/Metadata/Metadata"; + import { getLocations } from "../api/LocationsFetcher"; -import { LocationResponse } from "../shared/nursery.interface"; import { getCities, getCitiesResponse } from "../api/CitiesFetcher"; +import { LocationResponse } from "../shared/nursery.interface"; + export default function ListPage() { const { voivodeship, city } = useParams<{ voivodeship: string | undefined; @@ -51,18 +54,7 @@ export default function ListPage() { return ( <> - - {title} - - - - - - - - - - + diff --git a/src/nursery-nav/src/pages/MapPage.tsx b/src/nursery-nav/src/pages/MapPage.tsx index 63db38a..47ec385 100644 --- a/src/nursery-nav/src/pages/MapPage.tsx +++ b/src/nursery-nav/src/pages/MapPage.tsx @@ -1,11 +1,14 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; import { CircularProgress, Grid } from "@mui/material"; -import { Helmet } from "react-helmet-async"; + import FiltersBar from "../components/Filters/FiltersBar"; import MapComponent from "../components/MapComponent/MapComponent"; import ListComponent from "../components/ListComponent/ListComponent"; -import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import Metadata from "../components/Metadata/Metadata"; + import { getLocations } from "../api/LocationsFetcher"; + import { LocationResponse } from "../shared/nursery.interface"; export default function MapPage() { @@ -38,18 +41,7 @@ export default function MapPage() { return ( <> - - {title} - - - - - - - - - - + diff --git a/src/nursery-nav/src/pages/PageNotFoundPage.tsx b/src/nursery-nav/src/pages/PageNotFoundPage.tsx index 143050f..65068ac 100644 --- a/src/nursery-nav/src/pages/PageNotFoundPage.tsx +++ b/src/nursery-nav/src/pages/PageNotFoundPage.tsx @@ -1,6 +1,7 @@ import { Container, Link, Stack, Typography } from "@mui/material"; -import { Helmet } from "react-helmet-async"; + import PathConstants from "../shared/pathConstants"; +import Metadata from "../components/Metadata/Metadata"; export default function PageNotFoundPage() { const title = `Strona nie istnieje`; @@ -8,11 +9,7 @@ export default function PageNotFoundPage() { return ( <> - - {title} - - - + Strona nie istnieje @@ -26,4 +23,4 @@ export default function PageNotFoundPage() { ); -} \ No newline at end of file +} From a7100f9dc9c72c890332606c7700ab69b764016d Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 10:02:59 +0200 Subject: [PATCH 7/8] Move setting loading indicators after the promise has been resolved --- src/nursery-nav/src/pages/InstitutionDetailsPage.tsx | 4 +--- src/nursery-nav/src/pages/MapPage.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx b/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx index 8b9da72..f95ec68 100644 --- a/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx +++ b/src/nursery-nav/src/pages/InstitutionDetailsPage.tsx @@ -34,13 +34,11 @@ export default function InstitutionDetailsPage() { useEffect(() => { const fetchData = async () => { - setIsLoading(true); const locations = await getLocations(); setLocations(locations); - setIsLoading(false); }; - fetchData(); + fetchData().then(() => setIsLoading(false)); }, []); const title = `${selectedInstitution?.name} - ${selectedInstitution?.address.city} (${selectedInstitution?.address.voivodeship})`; diff --git a/src/nursery-nav/src/pages/MapPage.tsx b/src/nursery-nav/src/pages/MapPage.tsx index 47ec385..e50bad0 100644 --- a/src/nursery-nav/src/pages/MapPage.tsx +++ b/src/nursery-nav/src/pages/MapPage.tsx @@ -30,13 +30,11 @@ export default function MapPage() { useEffect(() => { const fetchData = async () => { - setIsLoading(true); const locations = await getLocations(); setLocations(locations); - setIsLoading(false); }; - fetchData(); + fetchData().then(() => setIsLoading(false)); }, []); return ( From f390f91703ad70beeae8a28681ccf6b3b261b430 Mon Sep 17 00:00:00 2001 From: szymonswiergosz Date: Sun, 6 Oct 2024 10:08:23 +0200 Subject: [PATCH 8/8] Add .env.example file --- src/nursery-nav/.env.example | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/nursery-nav/.env.example diff --git a/src/nursery-nav/.env.example b/src/nursery-nav/.env.example new file mode 100644 index 0000000..7d1a50a --- /dev/null +++ b/src/nursery-nav/.env.example @@ -0,0 +1,6 @@ +REACT_APP_GEOAPIFY_API_KEY= +REACT_APP_API_URL= +REACT_APP_NAME= +REACT_APP_CONTACT_MAIL= +REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID= +REACT_APP_DATA_SOURCE_UPDATE_DATE=