Skip to content

Commit

Permalink
Frontend refactor (#211)
Browse files Browse the repository at this point in the history
* Refactor fetches and create a reusable fetch method

* Refactor fetching data for filters and getting unique cities values

* Debounce window resizing on list page

* Add .idea directory to gitignore

* Fetch cities and locations data together and resolve the promise after both promises has been returned

* Extract Helmet metadata component to a separate one

* Move setting loading indicators after the promise has been resolved

* Add .env.example file
  • Loading branch information
sSwiergosz authored Oct 6, 2024
1 parent 0b1f0f7 commit d5fda0b
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 199 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
secrets.txt
*.env
.idea/
6 changes: 6 additions & 0 deletions src/nursery-nav/.env.example
Original file line number Diff line number Diff line change
@@ -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=
15 changes: 6 additions & 9 deletions src/nursery-nav/src/api/CitiesFetcher.ts
Original file line number Diff line number Diff line change
@@ -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<getCitiesResponse[]> => {
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<getCitiesResponse[]>(url);
}

export { }
export { }
64 changes: 22 additions & 42 deletions src/nursery-nav/src/api/InstitutionsFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from "axios";
import { Institution, InstitutionListItem } from "../shared/nursery.interface";
import { fetchFromApi } from './fetcher';

export interface getInstitutionsResponse {
items: InstitutionListItem[],
Expand All @@ -13,55 +13,35 @@ export interface getInstitutionsAutocompleteResponse {
name: string
}

export const getInstitutions = async (searchParams: URLSearchParams, pageNum?: number | null): Promise<getInstitutionsResponse> => {
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<getInstitutionsResponse> => {
const url = `${API_URL}/institutions`;
const params = { page: pageNum || undefined, ...Object.fromEntries(searchParams) };

const data = res.data as getInstitutionsResponse;
return data;
}
return fetchFromApi<getInstitutionsResponse>(url, params);
};

export const getInstitutionDetails = async (id: number): Promise<Institution> => {
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<Institution>(url);
};

export const getInstitutionsDetails = async (ids: number[]): Promise<Institution[]> => {
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<Institution[]>(url, params);
};

export const getInstitutionAutocomplete = async (search: string): Promise<getInstitutionsAutocompleteResponse[]> => {
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<getInstitutionsAutocompleteResponse[]>(url, params);
};

export { }
export { }
15 changes: 6 additions & 9 deletions src/nursery-nav/src/api/LocationsFetcher.ts
Original file line number Diff line number Diff line change
@@ -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<LocationResponse[]> => {
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<LocationResponse[]>(url);
}

export { }
11 changes: 11 additions & 0 deletions src/nursery-nav/src/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from "axios";

export const fetchFromApi = async <T>(url: string, params?: object): Promise<T> => {
const { data, status } = await axios.get<T>(url, { params });

if (status !== 200) {
throw new Error(`Failed to fetch data from ${url}. Status: ${status}`);
}

return data;
};
83 changes: 43 additions & 40 deletions src/nursery-nav/src/components/Filters/Filters.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,71 @@
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;
isMobile?: boolean;
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<InstitutionAutocomplete[]>([]);
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<City[]>((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);
setInstitutionsAutocomplete(institutions.map(institution => ({ name: institution.name, id: institution.id })));
}

const onInputChange = (_event: any, value: string | null) => {
if (value) {
getAutocompleteData(value);
}
else {
setInstitutionsAutocomplete([]);
}
value ? getAutocompleteData(value) : setInstitutionsAutocomplete([]);
}

const goToDetails = (_event: any, value: InstitutionAutocomplete | null) => {
Expand All @@ -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);
}

Expand Down
25 changes: 25 additions & 0 deletions src/nursery-nav/src/components/Metadata/Metadata.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
)
};
18 changes: 4 additions & 14 deletions src/nursery-nav/src/pages/AboutPage.tsx
Original file line number Diff line number Diff line change
@@ -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}`;
Expand All @@ -8,18 +9,7 @@ export default function AboutPage() {

return (
<>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={window.location.href} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
<Metadata title={title} description={description} image={image} url={window.location.href} />
<Container sx={{ padding: '1rem' }}>
<Paper elevation={3} sx={{ padding: '1rem' }}>
<Stack direction="column" spacing={2}>
Expand Down Expand Up @@ -69,4 +59,4 @@ export default function AboutPage() {
</Container>
</>
);
}
}
20 changes: 5 additions & 15 deletions src/nursery-nav/src/pages/ComparisonPage.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -18,18 +19,7 @@ export default function ComparisonPage() {

return (
<>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={title} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
<Metadata title={title} description={description} image={image} url={title} />
<Grid item xs={12}>
{displayError &&
<Container fixed>
Expand All @@ -50,4 +40,4 @@ export default function ComparisonPage() {
</Grid>
</>
);
}
}
Loading

0 comments on commit d5fda0b

Please sign in to comment.