From 338605a566fe7e974560717d75b7a983ac907f2a Mon Sep 17 00:00:00 2001 From: ddeme <5726983+DDeme@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:02:59 +0100 Subject: [PATCH 01/10] [User Story 1] Increase table font size --- .../EstablishmentsTableRow.tsx | 15 +++++++++++++++ src/components/EstablishmentTableRow/index.css | 3 +++ src/components/EstablishmentTableRow/index.ts | 1 + src/components/EstablishmentsTable.tsx | 2 +- src/components/EstablishmentsTableRow.tsx | 10 ---------- 5 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx create mode 100644 src/components/EstablishmentTableRow/index.css create mode 100644 src/components/EstablishmentTableRow/index.ts delete mode 100644 src/components/EstablishmentsTableRow.tsx diff --git a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx new file mode 100644 index 0000000..191f22c --- /dev/null +++ b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx @@ -0,0 +1,15 @@ +import "./index.css"; + +type EstablishmentsTableRowProps = { + establishment: { [key: string]: string } | null | undefined; +}; +export const EstablishmentsTableRow = ({ + establishment, +}: EstablishmentsTableRowProps) => { + return ( + + {establishment?.BusinessName} + {establishment?.RatingValue} + + ); +}; diff --git a/src/components/EstablishmentTableRow/index.css b/src/components/EstablishmentTableRow/index.css new file mode 100644 index 0000000..d648750 --- /dev/null +++ b/src/components/EstablishmentTableRow/index.css @@ -0,0 +1,3 @@ +.establishment-table__row { + font-size: 20px; +} diff --git a/src/components/EstablishmentTableRow/index.ts b/src/components/EstablishmentTableRow/index.ts new file mode 100644 index 0000000..4b9c916 --- /dev/null +++ b/src/components/EstablishmentTableRow/index.ts @@ -0,0 +1 @@ +export * from "./EstablishmentsTableRow"; diff --git a/src/components/EstablishmentsTable.tsx b/src/components/EstablishmentsTable.tsx index 56df19f..208497a 100644 --- a/src/components/EstablishmentsTable.tsx +++ b/src/components/EstablishmentsTable.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { EstablishmentsTableRow } from "./EstablishmentsTableRow"; +import { EstablishmentsTableRow } from "./EstablishmentTableRow"; import PropTypes from "prop-types"; const headerStyle: { [key: string]: string | number } = { diff --git a/src/components/EstablishmentsTableRow.tsx b/src/components/EstablishmentsTableRow.tsx deleted file mode 100644 index c4cf644..0000000 --- a/src/components/EstablishmentsTableRow.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const EstablishmentsTableRow: React.FC<{ - establishment: { [key: string]: string } | null | undefined; -}> = ({ establishment }) => { - return ( - - {establishment?.BusinessName} - {establishment?.RatingValue} - - ); -}; From 70356fcd4df2987d2750bec7304c7103e54c820c Mon Sep 17 00:00:00 2001 From: ddeme <5726983+DDeme@users.noreply.github.com> Date: Tue, 12 Mar 2024 00:34:04 +0100 Subject: [PATCH 02/10] [User Story 2] Show some loading text when waiting loading the next page of data --- src/api/ratingsAPI.test.tsx | 27 ------- .../EstablishmentsTable.tsx | 33 ++++++++ src/components/EstablishmentTable/index.css | 8 ++ src/components/EstablishmentTable/index.ts | 1 + .../PaginatedEstablishmentsTable.tsx | 43 +++++++++++ .../EstablishmentTablePaginated/index.css | 7 ++ .../EstablishmentsTablePagination.tsx | 43 +++++++++++ .../EstablishmentTablePagination/index.css | 3 + .../EstablishmentTablePagination/index.ts | 1 + .../EstablishmentsTableRow.tsx | 5 +- .../EstablishmentTableRow/index.css | 3 - src/components/EstablishmentsTable.tsx | 40 ---------- .../EstablishmentsTableNavigation.tsx | 41 ---------- src/components/HomePage.tsx | 2 +- .../PaginatedEstablishmentsTable.tsx | 75 ------------------- src/hooks/useFetch.ts | 36 +++++++++ .../useFetchRatings.ts} | 16 ++-- src/types/EstablishmentDto.d.ts | 14 ++++ 18 files changed, 200 insertions(+), 198 deletions(-) delete mode 100644 src/api/ratingsAPI.test.tsx create mode 100644 src/components/EstablishmentTable/EstablishmentsTable.tsx create mode 100644 src/components/EstablishmentTable/index.css create mode 100644 src/components/EstablishmentTable/index.ts create mode 100644 src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx create mode 100644 src/components/EstablishmentTablePaginated/index.css create mode 100644 src/components/EstablishmentTablePagination/EstablishmentsTablePagination.tsx create mode 100644 src/components/EstablishmentTablePagination/index.css create mode 100644 src/components/EstablishmentTablePagination/index.ts delete mode 100644 src/components/EstablishmentTableRow/index.css delete mode 100644 src/components/EstablishmentsTable.tsx delete mode 100644 src/components/EstablishmentsTableNavigation.tsx delete mode 100644 src/components/PaginatedEstablishmentsTable.tsx create mode 100644 src/hooks/useFetch.ts rename src/{api/ratingsAPI.ts => hooks/useFetchRatings.ts} (66%) create mode 100644 src/types/EstablishmentDto.d.ts diff --git a/src/api/ratingsAPI.test.tsx b/src/api/ratingsAPI.test.tsx deleted file mode 100644 index eb9bfeb..0000000 --- a/src/api/ratingsAPI.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { enableFetchMocks } from "jest-fetch-mock"; -import { getEstablishmentRatings } from "./ratingsAPI"; -import fetch from "jest-fetch-mock"; - -enableFetchMocks(); - -describe("Ratings API", () => { - beforeEach(() => { - fetch.resetMocks(); - }); - - it("call the ratings api with the provided page number and returns the data", async () => { - // Given - let pageNum = 1; - let expected = { testing: "test" }; - fetch.mockResponseOnce(JSON.stringify(expected)); - // When - let actual = await getEstablishmentRatings(pageNum); - - // Then - expect(actual).toEqual(expected); - expect(fetch.mock.calls.length).toEqual(1); - expect(fetch.mock.calls[0][0]).toEqual( - `http://api.ratings.food.gov.uk/Establishments/basic/${pageNum}/10` - ); - }); -}); diff --git a/src/components/EstablishmentTable/EstablishmentsTable.tsx b/src/components/EstablishmentTable/EstablishmentsTable.tsx new file mode 100644 index 0000000..32fc3ce --- /dev/null +++ b/src/components/EstablishmentTable/EstablishmentsTable.tsx @@ -0,0 +1,33 @@ +import { EstablishmentsTableRow } from "../EstablishmentTableRow"; +import "./index.css"; + +type EstablishmentsTableProps = { + establishments: EstablishmentDto[]; + isLoading: boolean; +}; + +export const EstablishmentsTable = ({ + establishments, + isLoading, +}: EstablishmentsTableProps) => { + return ( + + + + + + + {isLoading && ( + + + + )} + + + {establishments.map((establishment, index) => ( + + ))} + +
Business NameRating Value
Loading ...
+ ); +}; diff --git a/src/components/EstablishmentTable/index.css b/src/components/EstablishmentTable/index.css new file mode 100644 index 0000000..f82d16e --- /dev/null +++ b/src/components/EstablishmentTable/index.css @@ -0,0 +1,8 @@ +.establishment-table { + font-size: 20px; +} + +.establishment-table thead { + padding-bottom: 10px; + text-align: left; +} diff --git a/src/components/EstablishmentTable/index.ts b/src/components/EstablishmentTable/index.ts new file mode 100644 index 0000000..9786db2 --- /dev/null +++ b/src/components/EstablishmentTable/index.ts @@ -0,0 +1 @@ +export * from "./EstablishmentsTable"; diff --git a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx new file mode 100644 index 0000000..d6eab24 --- /dev/null +++ b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from "react"; +import { EstablishmentsTable } from "../EstablishmentTable"; +import { EstablishmentsTablePagination } from "../EstablishmentTablePagination"; +import { useFetchRatings } from "../../hooks/useFetchRatings"; + +import "./index.css"; + +export const PaginatedEstablishmentsTable = () => { + const [pageNum, setPageNum] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const { error, data, loading, refetch } = useFetchRatings(pageNum); + + useEffect(() => { + setTotalPages(data?.meta?.totalCount ?? 1); + }, [data]); + + if (error) { + return
Error: {error}
; + } + + return ( +
+

Food Hygiene Ratings

+ + { + setPageNum(pageNum - 1); + refetch(); + }} + onNextPage={() => { + setPageNum(pageNum + 1); + refetch(); + }} + isDisabled={loading} + /> +
+ ); +}; diff --git a/src/components/EstablishmentTablePaginated/index.css b/src/components/EstablishmentTablePaginated/index.css new file mode 100644 index 0000000..2ccf37f --- /dev/null +++ b/src/components/EstablishmentTablePaginated/index.css @@ -0,0 +1,7 @@ +.establishment-container { + background: rgba(51, 51, 51, 0.9); + padding: 10px; + width: max-content; + margin-left: 50px; + color: white; +} diff --git a/src/components/EstablishmentTablePagination/EstablishmentsTablePagination.tsx b/src/components/EstablishmentTablePagination/EstablishmentsTablePagination.tsx new file mode 100644 index 0000000..4b679c3 --- /dev/null +++ b/src/components/EstablishmentTablePagination/EstablishmentsTablePagination.tsx @@ -0,0 +1,43 @@ +import "./index.css"; + +type EstablishmentsTablePaginationType = { + pageNum: number; + pageCount: number; + isDisabled: boolean; + onPreviousPage: () => void; + onNextPage: () => void; +}; + +export const EstablishmentsTablePagination = ({ + pageNum, + pageCount, + onPreviousPage, + onNextPage, + isDisabled, +}: EstablishmentsTablePaginationType) => { + return ( + + ); +}; diff --git a/src/components/EstablishmentTablePagination/index.css b/src/components/EstablishmentTablePagination/index.css new file mode 100644 index 0000000..d22928c --- /dev/null +++ b/src/components/EstablishmentTablePagination/index.css @@ -0,0 +1,3 @@ +.pagination-button { + margin: 0 5px; +} diff --git a/src/components/EstablishmentTablePagination/index.ts b/src/components/EstablishmentTablePagination/index.ts new file mode 100644 index 0000000..04f5a55 --- /dev/null +++ b/src/components/EstablishmentTablePagination/index.ts @@ -0,0 +1 @@ +export * from "./EstablishmentsTablePagination"; diff --git a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx index 191f22c..1647839 100644 --- a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx +++ b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx @@ -1,8 +1,7 @@ -import "./index.css"; - type EstablishmentsTableRowProps = { - establishment: { [key: string]: string } | null | undefined; + establishment: EstablishmentDto; }; + export const EstablishmentsTableRow = ({ establishment, }: EstablishmentsTableRowProps) => { diff --git a/src/components/EstablishmentTableRow/index.css b/src/components/EstablishmentTableRow/index.css deleted file mode 100644 index d648750..0000000 --- a/src/components/EstablishmentTableRow/index.css +++ /dev/null @@ -1,3 +0,0 @@ -.establishment-table__row { - font-size: 20px; -} diff --git a/src/components/EstablishmentsTable.tsx b/src/components/EstablishmentsTable.tsx deleted file mode 100644 index 208497a..0000000 --- a/src/components/EstablishmentsTable.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import { EstablishmentsTableRow } from "./EstablishmentTableRow"; -import PropTypes from "prop-types"; - -const headerStyle: { [key: string]: string | number } = { - paddingBottom: "10px", - textAlign: "left", - fontSize: "20px", -}; - -export const EstablishmentsTable: React.FC<{ - establishments: { [key: string]: string }[] | null | undefined; -}> = ({ establishments }) => { - return ( - - - - - - - {establishments && - establishments?.map( - ( - establishment: { [key: string]: string } | null | undefined, - index: React.Key | null | undefined - ) => ( - - ) - )} - -
Business NameRating Value
- ); -}; - -EstablishmentsTable.propTypes = { - establishments: PropTypes.array, -}; diff --git a/src/components/EstablishmentsTableNavigation.tsx b/src/components/EstablishmentsTableNavigation.tsx deleted file mode 100644 index 805b6ca..0000000 --- a/src/components/EstablishmentsTableNavigation.tsx +++ /dev/null @@ -1,41 +0,0 @@ -const buttonStyle = { - margin: "0 5px", -}; - -type EstablishmentsTableNavigationType = { - pageNum: number; - pageCount: number; - onPreviousPage: () => void; - onNextPage: () => void; -}; - -export const EstablishmentsTableNavigation = ( - props: EstablishmentsTableNavigationType -) => { - const { pageNum, pageCount, onPreviousPage, onNextPage } = props; - return ( - - ); -}; diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx index 24e4a1f..349fcff 100644 --- a/src/components/HomePage.tsx +++ b/src/components/HomePage.tsx @@ -1,4 +1,4 @@ -import { PaginatedEstablishmentsTable } from "./PaginatedEstablishmentsTable"; +import { PaginatedEstablishmentsTable } from "./EstablishmentTablePaginated/PaginatedEstablishmentsTable"; import Background from "../static/logo.svg"; const logoStyle: { [key: string]: string | number } = { diff --git a/src/components/PaginatedEstablishmentsTable.tsx b/src/components/PaginatedEstablishmentsTable.tsx deleted file mode 100644 index af6d64a..0000000 --- a/src/components/PaginatedEstablishmentsTable.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useState, useEffect } from "react"; -import { EstablishmentsTable } from "./EstablishmentsTable"; -import { EstablishmentsTableNavigation } from "./EstablishmentsTableNavigation"; -import { getEstablishmentRatings } from "../api/ratingsAPI"; - -const tableStyle = { - background: "rgba(51, 51, 51, 0.9)", - padding: "10px", - width: "max-content", - marginLeft: "50px", - color: "white", -}; - -export const PaginatedEstablishmentsTable = () => { - const [error, setError] = - useState<{ message: string; [key: string]: string }>(); - const [establishments, setEstablishments] = useState< - { [key: string]: string }[] - >([]); - const [pageNum, setPageNum] = useState(1); - const [pageCount] = useState(100); - - useEffect(() => { - getEstablishmentRatings(pageNum).then( - (result) => { - setEstablishments(result?.establishments); - }, - (error) => { - setError(error); - } - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - async function handlePreviousPage() { - pageNum > 1 && setPageNum(pageNum - 1); - getEstablishmentRatings(pageNum).then( - (result) => { - setEstablishments(result.establishments); - }, - (error) => { - setError(error); - } - ); - } - - async function handleNextPage() { - pageNum < pageCount && setPageNum(pageNum + 1); - getEstablishmentRatings(pageNum).then( - (result) => { - setEstablishments(result.establishments); - }, - (error) => { - setError(error); - } - ); - } - - if (error) { - return
Error: {error.message}
; - } else { - return ( -
-

Food Hygiene Ratings

- - -
- ); - } -}; diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts new file mode 100644 index 0000000..14af37d --- /dev/null +++ b/src/hooks/useFetch.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +export const useFetch = ( + url: string, + options?: RequestInit | undefined +) => { + const [data, setData] = useState(); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [input] = useState(url); + const [init] = useState(options); + const [refetchIndex, setRefetchIndex] = useState(0); + + const refetch = () => + setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1); + useEffect(() => { + (async function () { + try { + setLoading(true); + setData(undefined); + const response = await fetch(input, init); + if (!response.ok) { + throw new Error(response.statusText); + } + const data = (await response.json()) as Data; + setData(data); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + })(); + }, [input, init, refetchIndex]); + + return { data, error, loading, refetch }; +}; diff --git a/src/api/ratingsAPI.ts b/src/hooks/useFetchRatings.ts similarity index 66% rename from src/api/ratingsAPI.ts rename to src/hooks/useFetchRatings.ts index 1d4080c..c7f48aa 100644 --- a/src/api/ratingsAPI.ts +++ b/src/hooks/useFetchRatings.ts @@ -1,5 +1,7 @@ -export type EstablishmentsType = { - establishments: {}[]; +import { useFetch } from "./useFetch"; + +type ResponseType = { + establishments: EstablishmentDto[]; meta: { dataSource: string; extractDate: string; @@ -18,11 +20,9 @@ export type EstablishmentsType = { ]; }; -export function getEstablishmentRatings( - pageNum: number -): Promise { - return fetch( +export const useFetchRatings = (pageNum: number) => { + return useFetch( `http://api.ratings.food.gov.uk/Establishments/basic/${pageNum}/10`, { headers: { "x-api-version": "2" } } - ).then((res) => res.json()); -} + ); +}; diff --git a/src/types/EstablishmentDto.d.ts b/src/types/EstablishmentDto.d.ts new file mode 100644 index 0000000..262ab7c --- /dev/null +++ b/src/types/EstablishmentDto.d.ts @@ -0,0 +1,14 @@ +interface EstablishmentDto { + FHRSID: number; + LocalAuthorityBusinessID: string; + BusinessName: string; + BusinessType: string; + RatingValue: string; + RatingDate: Date; + links: Link[]; +} + +interface Link { + rel: string; + href: string; +} From b101472330d9c3092ee8a3cfbb60317243c6710d Mon Sep 17 00:00:00 2001 From: ddeme <5726983+DDeme@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:23:52 +0100 Subject: [PATCH 03/10] [User Story 3] Filter Establishments by Country or Authority --- .../EstablishmentsTable.tsx | 5 +- .../PaginatedEstablishmentsTable.tsx | 46 +++++----------- .../TableBasic.tsx | 43 +++++++++++++++ .../TableWithFilters.tsx | 55 +++++++++++++++++++ .../EstablishmentsTableRow.tsx | 2 +- .../SelectAuthorities/SelectAuthorities.tsx | 46 ++++++++++++++++ src/components/SelectAuthorities/index.ts | 1 + src/hooks/useFetch.ts | 18 ++++-- src/hooks/useFetchAuthorities.ts | 12 ++++ src/hooks/useFetchRatingsByAuthority.ts | 37 +++++++++++++ src/types/ApiResponse.d.ts | 4 ++ src/types/AuthorityDto.d.ts | 8 +++ src/types/EstablishmentDto.d.ts | 7 +-- src/types/EstablishmentsByAuthorityDto.d.ts | 38 +++++++++++++ src/types/LinkDto.d.ts | 4 ++ src/types/MetaDto.d.ts | 10 ++++ 16 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 src/components/EstablishmentTablePaginated/TableBasic.tsx create mode 100644 src/components/EstablishmentTablePaginated/TableWithFilters.tsx create mode 100644 src/components/SelectAuthorities/SelectAuthorities.tsx create mode 100644 src/components/SelectAuthorities/index.ts create mode 100644 src/hooks/useFetchAuthorities.ts create mode 100644 src/hooks/useFetchRatingsByAuthority.ts create mode 100644 src/types/ApiResponse.d.ts create mode 100644 src/types/AuthorityDto.d.ts create mode 100644 src/types/EstablishmentsByAuthorityDto.d.ts create mode 100644 src/types/LinkDto.d.ts create mode 100644 src/types/MetaDto.d.ts diff --git a/src/components/EstablishmentTable/EstablishmentsTable.tsx b/src/components/EstablishmentTable/EstablishmentsTable.tsx index 32fc3ce..d803c9c 100644 --- a/src/components/EstablishmentTable/EstablishmentsTable.tsx +++ b/src/components/EstablishmentTable/EstablishmentsTable.tsx @@ -1,8 +1,11 @@ +import { ComponentProps } from "react"; import { EstablishmentsTableRow } from "../EstablishmentTableRow"; import "./index.css"; type EstablishmentsTableProps = { - establishments: EstablishmentDto[]; + establishments: ComponentProps< + typeof EstablishmentsTableRow + >["establishment"][]; isLoading: boolean; }; diff --git a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx index d6eab24..7a8b91d 100644 --- a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx +++ b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx @@ -1,43 +1,27 @@ -import { useState, useEffect } from "react"; -import { EstablishmentsTable } from "../EstablishmentTable"; -import { EstablishmentsTablePagination } from "../EstablishmentTablePagination"; -import { useFetchRatings } from "../../hooks/useFetchRatings"; - +import { useState } from "react"; +import { SelectAuthorities } from "../SelectAuthorities"; +import { TableWithFilters } from "./TableWithFilters"; +import { TableBasic } from "./TableBasic"; import "./index.css"; export const PaginatedEstablishmentsTable = () => { - const [pageNum, setPageNum] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const { error, data, loading, refetch } = useFetchRatings(pageNum); - - useEffect(() => { - setTotalPages(data?.meta?.totalCount ?? 1); - }, [data]); - - if (error) { - return
Error: {error}
; - } + const [localAuthorityId, setLocalAuthorityId] = useState< + string | undefined + >(); return (

Food Hygiene Ratings

- - { - setPageNum(pageNum - 1); - refetch(); - }} - onNextPage={() => { - setPageNum(pageNum + 1); - refetch(); + { + setLocalAuthorityId(val); }} - isDisabled={loading} /> + {localAuthorityId ? ( + + ) : ( + + )}
); }; diff --git a/src/components/EstablishmentTablePaginated/TableBasic.tsx b/src/components/EstablishmentTablePaginated/TableBasic.tsx new file mode 100644 index 0000000..aa90aa6 --- /dev/null +++ b/src/components/EstablishmentTablePaginated/TableBasic.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { EstablishmentsTable } from "../EstablishmentTable"; +import { EstablishmentsTablePagination } from "../EstablishmentTablePagination"; +import { useFetchRatings } from "../../hooks/useFetchRatings"; + +export const TableBasic = () => { + const [pageNum, setPageNum] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const { data, error, loading, refetch } = useFetchRatings(pageNum); + + useEffect(() => { + setTotalPages(data?.meta?.totalCount ?? 1); + }, [data]); + + useEffect(() => { + refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageNum]); + + if (error) { + return
Error: {error?.message}
; + } + + return ( + <> + + { + setPageNum(pageNum - 1); + }} + onNextPage={() => { + setPageNum(pageNum + 1); + }} + isDisabled={loading} + /> + + ); +}; diff --git a/src/components/EstablishmentTablePaginated/TableWithFilters.tsx b/src/components/EstablishmentTablePaginated/TableWithFilters.tsx new file mode 100644 index 0000000..c3a3bc2 --- /dev/null +++ b/src/components/EstablishmentTablePaginated/TableWithFilters.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { useFetchRatingsByAuthority } from "../../hooks/useFetchRatingsByAuthority"; +import { EstablishmentsTable } from "../EstablishmentTable"; +import { EstablishmentsTablePagination } from "../EstablishmentTablePagination"; + +type TableWithFiltersProps = { + localAuthorityId: string; +}; + +export const TableWithFilters = ({ + localAuthorityId: initLocalAuthorityId, +}: TableWithFiltersProps) => { + const [pageNum, setPageNum] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [localAuthorityId] = useState(initLocalAuthorityId); + + const { data, error, loading, refetch } = useFetchRatingsByAuthority({ + pageNumber: pageNum, + localAuthorityId: localAuthorityId ?? "", + pageSize: 10, + }); + + useEffect(() => { + setTotalPages(data?.meta?.totalCount ?? 1); + }, [data]); + + useEffect(() => { + refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageNum]); + + if (error) { + return
Error: {error?.message}
; + } + + return ( + <> + + { + setPageNum(pageNum - 1); + }} + onNextPage={() => { + setPageNum(pageNum + 1); + }} + isDisabled={loading} + /> + + ); +}; diff --git a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx index 1647839..2375eed 100644 --- a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx +++ b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx @@ -1,5 +1,5 @@ type EstablishmentsTableRowProps = { - establishment: EstablishmentDto; + establishment: Pick; }; export const EstablishmentsTableRow = ({ diff --git a/src/components/SelectAuthorities/SelectAuthorities.tsx b/src/components/SelectAuthorities/SelectAuthorities.tsx new file mode 100644 index 0000000..1a6dc32 --- /dev/null +++ b/src/components/SelectAuthorities/SelectAuthorities.tsx @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { useFetchAuthorities } from "../../hooks/useFetchAuthorities"; + +type SelectAuthoritiesProps = { + onChange: (value: string) => void; +}; + +export const SelectAuthorities = ({ onChange }: SelectAuthoritiesProps) => { + const { data, error, loading } = useFetchAuthorities(); + const [value, setValue] = useState(undefined); + return ( + + ); +}; diff --git a/src/components/SelectAuthorities/index.ts b/src/components/SelectAuthorities/index.ts new file mode 100644 index 0000000..016b728 --- /dev/null +++ b/src/components/SelectAuthorities/index.ts @@ -0,0 +1 @@ +export * from "./SelectAuthorities"; diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts index 14af37d..e6fad55 100644 --- a/src/hooks/useFetch.ts +++ b/src/hooks/useFetch.ts @@ -4,20 +4,26 @@ export const useFetch = ( url: string, options?: RequestInit | undefined ) => { + console.log(url, "url"); const [data, setData] = useState(); - const [error, setError] = useState(null); + const [error, setError] = useState(); const [loading, setLoading] = useState(false); - const [input] = useState(url); - const [init] = useState(options); + const [input, setInput] = useState(url); + const [init, setInit] = useState(options); const [refetchIndex, setRefetchIndex] = useState(0); - const refetch = () => + const refetch = () => { + setInput(url); + setInit(options); setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1); + }; + useEffect(() => { (async function () { try { - setLoading(true); + setError(undefined); setData(undefined); + setLoading(true); const response = await fetch(input, init); if (!response.ok) { throw new Error(response.statusText); @@ -25,7 +31,7 @@ export const useFetch = ( const data = (await response.json()) as Data; setData(data); } catch (err) { - setError(err); + setError(err as Error); } finally { setLoading(false); } diff --git a/src/hooks/useFetchAuthorities.ts b/src/hooks/useFetchAuthorities.ts new file mode 100644 index 0000000..efd9fa5 --- /dev/null +++ b/src/hooks/useFetchAuthorities.ts @@ -0,0 +1,12 @@ +import { useFetch } from "./useFetch"; + +interface AuthoritiesResponse extends ApiResponse { + authorities: AuthorityDto[]; +} + +export const useFetchAuthorities = () => { + return useFetch( + `http://api.ratings.food.gov.uk/Authorities/basic/`, + { headers: { "x-api-version": "2" } } + ); +}; diff --git a/src/hooks/useFetchRatingsByAuthority.ts b/src/hooks/useFetchRatingsByAuthority.ts new file mode 100644 index 0000000..31b7b14 --- /dev/null +++ b/src/hooks/useFetchRatingsByAuthority.ts @@ -0,0 +1,37 @@ +import { useFetch } from "./useFetch"; + +interface RatingsResponse extends ApiResponse { + establishments: EstablishmentsByAuthorityDto[]; +} + +interface Params { + name?: string; + address?: string; + longitude?: string; + latitude?: string; + maxDistanceLimit?: number; + businessTypeId?: string; + schemeTypeKey?: string; + ratingKey?: string; + ratingOperatorKey?: string; + localAuthorityId: string; + countryId?: string; + sortOptionKey?: string; + pageNumber: number; + pageSize: number; +} + +export const useFetchRatingsByAuthority = (params: Params) => { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value) { + searchParams.append(key, value); + } + } + const urlParams = searchParams.toString(); + console.log(urlParams); + return useFetch( + `http://api.ratings.food.gov.uk/Establishments?${urlParams}`, + { headers: { "x-api-version": "2" } } + ); +}; diff --git a/src/types/ApiResponse.d.ts b/src/types/ApiResponse.d.ts new file mode 100644 index 0000000..37e78fb --- /dev/null +++ b/src/types/ApiResponse.d.ts @@ -0,0 +1,4 @@ +interface ApiResponse { + meta: MetaDto; + link: LinkDto[]; +} diff --git a/src/types/AuthorityDto.d.ts b/src/types/AuthorityDto.d.ts new file mode 100644 index 0000000..9a89e4d --- /dev/null +++ b/src/types/AuthorityDto.d.ts @@ -0,0 +1,8 @@ +interface AuthorityDto { + LocalAuthorityId: number; + LocalAuthorityIdCode: string; + Name: string; + EstablishmentCount: number; + SchemeType: number; + links: LinkDto[]; +} diff --git a/src/types/EstablishmentDto.d.ts b/src/types/EstablishmentDto.d.ts index 262ab7c..ea5404f 100644 --- a/src/types/EstablishmentDto.d.ts +++ b/src/types/EstablishmentDto.d.ts @@ -5,10 +5,5 @@ interface EstablishmentDto { BusinessType: string; RatingValue: string; RatingDate: Date; - links: Link[]; -} - -interface Link { - rel: string; - href: string; + links: LinkDto[]; } diff --git a/src/types/EstablishmentsByAuthorityDto.d.ts b/src/types/EstablishmentsByAuthorityDto.d.ts new file mode 100644 index 0000000..89cad33 --- /dev/null +++ b/src/types/EstablishmentsByAuthorityDto.d.ts @@ -0,0 +1,38 @@ +interface EstablishmentsByAuthorityDto { + AddressLine1: string; + AddressLine2: string; + AddressLine3: string; + AddressLine4: string; + BusinessName: string; + BusinessType: string; + BusinessTypeID: number; + ChangesByServerID: number; + Distance: null; + FHRSID: number; + LocalAuthorityBusinessID: string; + LocalAuthorityCode: string; + LocalAuthorityEmailAddress: string; + LocalAuthorityName: string; + LocalAuthorityWebSite: string; + NewRatingPending: boolean; + Phone: string; + PostCode: string; + RatingDate: Date; + RatingKey: string; + RatingValue: string; + RightToReply: string; + SchemeType: string; + geocode: Geocode; + scores: Scores; +} + +interface Geocode { + longitude: string; + latitude: string; +} + +interface Scores { + Hygiene: null; + Structural: null; + ConfidenceInManagement: null; +} diff --git a/src/types/LinkDto.d.ts b/src/types/LinkDto.d.ts new file mode 100644 index 0000000..da40146 --- /dev/null +++ b/src/types/LinkDto.d.ts @@ -0,0 +1,4 @@ +interface LinkDto { + rel: string; + href: string; +} diff --git a/src/types/MetaDto.d.ts b/src/types/MetaDto.d.ts new file mode 100644 index 0000000..86bf81e --- /dev/null +++ b/src/types/MetaDto.d.ts @@ -0,0 +1,10 @@ +interface MetaDto { + dataSource: string; + extractDate: string; + itemCount: number; + returncode: string; + totalCount: number; + totalPages: number; + pageSize: number; + pageNumber: number; +} From 18da6fc02034d57f6bc2cee3bbe4720ccd0c5f26 Mon Sep 17 00:00:00 2001 From: ddeme <5726983+DDeme@users.noreply.github.com> Date: Wed, 13 Mar 2024 12:25:04 +0100 Subject: [PATCH 04/10] [User Story 4] Establishments link to their detail page --- package.json | 1 + src/App.css | 38 ----------- src/App.test.tsx | 6 -- src/App.tsx | 10 --- src/components/Container/Container.tsx | 6 ++ .../index.css | 0 src/components/Container/index.ts | 1 + .../PaginatedEstablishmentsTable.tsx | 6 +- .../EstablishmentsTableRow.tsx | 27 ++++++-- .../EstablishmentTableRow/index.css | 12 ++++ src/hooks/useFetchEstablishment.ts | 8 +++ src/hooks/useFetchRatings.ts | 22 +----- src/hooks/useFetchRatingsByAuthority.ts | 2 +- src/index.css | 5 -- src/index.tsx | 27 ++++++-- src/routes/Establishment.tsx | 68 +++++++++++++++++++ src/{components => routes}/HomePage.tsx | 2 +- ...tyDto.d.ts => EstablishmentDetailDto.d.ts} | 4 +- yarn.lock | 20 ++++++ 19 files changed, 169 insertions(+), 96 deletions(-) delete mode 100644 src/App.css delete mode 100644 src/App.test.tsx delete mode 100644 src/App.tsx create mode 100644 src/components/Container/Container.tsx rename src/components/{EstablishmentTablePaginated => Container}/index.css (100%) create mode 100644 src/components/Container/index.ts create mode 100644 src/components/EstablishmentTableRow/index.css create mode 100644 src/hooks/useFetchEstablishment.ts create mode 100644 src/routes/Establishment.tsx rename src/{components => routes}/HomePage.tsx (76%) rename src/types/{EstablishmentsByAuthorityDto.d.ts => EstablishmentDetailDto.d.ts} (92%) diff --git a/package.json b/package.json index 185bbd5..1c939ad 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/react-dom": "^17.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-router-dom": "^6.22.3", "react-scripts": "4.0.3", "typescript": "^4.1.2", "web-vitals": "^1.0.1" diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index ba6a502..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { render } from "@testing-library/react"; -import App from "./App"; - -test("renders learn react link", () => { - render(); -}); diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 6e1f19b..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { Component } from "react"; -import HomePage from "./components/HomePage"; - -class App extends Component { - render() { - return ; - } -} - -export default App; diff --git a/src/components/Container/Container.tsx b/src/components/Container/Container.tsx new file mode 100644 index 0000000..dcc6a2b --- /dev/null +++ b/src/components/Container/Container.tsx @@ -0,0 +1,6 @@ +import { PropsWithChildren } from "react"; +import "./index.css"; + +export const Container = ({ children }: PropsWithChildren<{}>) => { + return
{children}
; +}; diff --git a/src/components/EstablishmentTablePaginated/index.css b/src/components/Container/index.css similarity index 100% rename from src/components/EstablishmentTablePaginated/index.css rename to src/components/Container/index.css diff --git a/src/components/Container/index.ts b/src/components/Container/index.ts new file mode 100644 index 0000000..a75e925 --- /dev/null +++ b/src/components/Container/index.ts @@ -0,0 +1 @@ +export * from "./Container"; diff --git a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx index 7a8b91d..fd8d728 100644 --- a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx +++ b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { SelectAuthorities } from "../SelectAuthorities"; import { TableWithFilters } from "./TableWithFilters"; import { TableBasic } from "./TableBasic"; -import "./index.css"; +import { Container } from "../Container"; export const PaginatedEstablishmentsTable = () => { const [localAuthorityId, setLocalAuthorityId] = useState< @@ -10,7 +10,7 @@ export const PaginatedEstablishmentsTable = () => { >(); return ( -
+

Food Hygiene Ratings

{ @@ -22,6 +22,6 @@ export const PaginatedEstablishmentsTable = () => { ) : ( )} -
+ ); }; diff --git a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx index 2375eed..d63231a 100644 --- a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx +++ b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx @@ -1,14 +1,33 @@ +import { useNavigate } from "react-router-dom"; +import "./index.css"; + type EstablishmentsTableRowProps = { - establishment: Pick; + establishment: Pick< + EstablishmentDto, + "BusinessName" | "RatingValue" | "FHRSID" + >; }; export const EstablishmentsTableRow = ({ establishment, }: EstablishmentsTableRowProps) => { + const navigate = useNavigate(); + const linkToEstablishment = `/establishment/${establishment.FHRSID}`; return ( - - {establishment?.BusinessName} - {establishment?.RatingValue} + + + { + event.preventDefault(); + navigate(linkToEstablishment); + }} + className="establishment__link" + > + {establishment.BusinessName} + + + {establishment.RatingValue} ); }; diff --git a/src/components/EstablishmentTableRow/index.css b/src/components/EstablishmentTableRow/index.css new file mode 100644 index 0000000..c3281bf --- /dev/null +++ b/src/components/EstablishmentTableRow/index.css @@ -0,0 +1,12 @@ +.establishment__link { + color: #fff; + text-decoration: none; +} + +.establishment__link:visited { + color: #fff; +} + +.establishment__link:hover { + opacity: 70%; +} diff --git a/src/hooks/useFetchEstablishment.ts b/src/hooks/useFetchEstablishment.ts new file mode 100644 index 0000000..5bdb464 --- /dev/null +++ b/src/hooks/useFetchEstablishment.ts @@ -0,0 +1,8 @@ +import { useFetch } from "./useFetch"; + +export const useFetchEstablishment = (id: string) => { + return useFetch( + `http://api.ratings.food.gov.uk/Establishments/${id}`, + { headers: { "x-api-version": "2" } } + ); +}; diff --git a/src/hooks/useFetchRatings.ts b/src/hooks/useFetchRatings.ts index c7f48aa..bb6f39b 100644 --- a/src/hooks/useFetchRatings.ts +++ b/src/hooks/useFetchRatings.ts @@ -1,27 +1,11 @@ import { useFetch } from "./useFetch"; -type ResponseType = { +interface RatingsResponse extends ApiResponse { establishments: EstablishmentDto[]; - meta: { - dataSource: string; - extractDate: string; - itemCount: number; - returncode: string; - totalCount: number; - totalPages: number; - pageSize: number; - pageNumber: number; - }; - links: [ - { - rel: string; - href: string; - } - ]; -}; +} export const useFetchRatings = (pageNum: number) => { - return useFetch( + return useFetch( `http://api.ratings.food.gov.uk/Establishments/basic/${pageNum}/10`, { headers: { "x-api-version": "2" } } ); diff --git a/src/hooks/useFetchRatingsByAuthority.ts b/src/hooks/useFetchRatingsByAuthority.ts index 31b7b14..bdfb57b 100644 --- a/src/hooks/useFetchRatingsByAuthority.ts +++ b/src/hooks/useFetchRatingsByAuthority.ts @@ -1,7 +1,7 @@ import { useFetch } from "./useFetch"; interface RatingsResponse extends ApiResponse { - establishments: EstablishmentsByAuthorityDto[]; + establishments: EstablishmentDetailDto[]; } interface Params { diff --git a/src/index.css b/src/index.css index 8238453..95e3a34 100644 --- a/src/index.css +++ b/src/index.css @@ -13,8 +13,3 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} diff --git a/src/index.tsx b/src/index.tsx index ef2edf8..9ed0cff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,27 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom"; +import "./index.css"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import reportWebVitals from "./reportWebVitals"; +import HomePage from "./routes/HomePage"; +import Establishment from "./routes/Establishment"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/establishment/:establishmentId", + element: , + }, +]); ReactDOM.render( - + , - document.getElementById('root') + document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function diff --git a/src/routes/Establishment.tsx b/src/routes/Establishment.tsx new file mode 100644 index 0000000..17c28d2 --- /dev/null +++ b/src/routes/Establishment.tsx @@ -0,0 +1,68 @@ +import { useNavigate, useParams } from "react-router-dom"; +import Background from "../static/logo.svg"; +import { Container } from "../components/Container"; +import { useFetchEstablishment } from "../hooks/useFetchEstablishment"; +import { useEffect } from "react"; + +const logoStyle: { [key: string]: string | number } = { + width: "640px", + height: "25px", + background: `transparent url(${Background}) no-repeat center`, + margin: "20px auto", +}; + +const formatDate = (isoString: string) => + new Intl.DateTimeFormat("in", { dateStyle: "short" }).format( + new Date(isoString) + ); + +const Establishment = () => { + const { establishmentId } = useParams(); + const navigate = useNavigate(); + const { error, data, loading } = useFetchEstablishment(establishmentId ?? ""); + + useEffect(() => { + if (!establishmentId) { + navigate("/", { replace: true }); + } + }, [establishmentId, navigate]); + + if (!establishmentId) { + return <>; + } + + return ( +
+
+ + + {error && <>Error: {error?.message} } + {loading && <>Loading ...} + {data && ( + <> +

{data?.BusinessName}

+
+ {data?.AddressLine1} +
+ {data?.AddressLine2} +
+ {data?.AddressLine3} + {data?.AddressLine4} +
+
+
+
Rating: {data?.RatingValue}
+
+ Rating Date: {data?.RatingDate && formatDate(data.RatingDate)} +
+ + )} + {!loading && !error && !data && <>There is no data.} +
+
+ ); +}; + +export default Establishment; diff --git a/src/components/HomePage.tsx b/src/routes/HomePage.tsx similarity index 76% rename from src/components/HomePage.tsx rename to src/routes/HomePage.tsx index 349fcff..ef90c6d 100644 --- a/src/components/HomePage.tsx +++ b/src/routes/HomePage.tsx @@ -1,4 +1,4 @@ -import { PaginatedEstablishmentsTable } from "./EstablishmentTablePaginated/PaginatedEstablishmentsTable"; +import { PaginatedEstablishmentsTable } from "../components/EstablishmentTablePaginated/PaginatedEstablishmentsTable"; import Background from "../static/logo.svg"; const logoStyle: { [key: string]: string | number } = { diff --git a/src/types/EstablishmentsByAuthorityDto.d.ts b/src/types/EstablishmentDetailDto.d.ts similarity index 92% rename from src/types/EstablishmentsByAuthorityDto.d.ts rename to src/types/EstablishmentDetailDto.d.ts index 89cad33..53830b8 100644 --- a/src/types/EstablishmentsByAuthorityDto.d.ts +++ b/src/types/EstablishmentDetailDto.d.ts @@ -1,4 +1,4 @@ -interface EstablishmentsByAuthorityDto { +interface EstablishmentDetailDto { AddressLine1: string; AddressLine2: string; AddressLine3: string; @@ -17,7 +17,7 @@ interface EstablishmentsByAuthorityDto { NewRatingPending: boolean; Phone: string; PostCode: string; - RatingDate: Date; + RatingDate: string; RatingKey: string; RatingValue: string; RightToReply: string; diff --git a/yarn.lock b/yarn.lock index 9508d03..d1a72eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1448,6 +1448,11 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== + "@rollup/plugin-node-resolve@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca" @@ -9107,6 +9112,21 @@ react-refresh@^0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-router-dom@^6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== + dependencies: + "@remix-run/router" "1.15.3" + react-router "6.22.3" + +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== + dependencies: + "@remix-run/router" "1.15.3" + react-scripts@4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-4.0.3.tgz#b1cafed7c3fa603e7628ba0f187787964cb5d345" From 8dce7e66548095aeb3d2d6c4bdecba78afc20074 Mon Sep 17 00:00:00 2001 From: ddeme <5726983+DDeme@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:54:58 +0100 Subject: [PATCH 05/10] [User Story 5] Favourites table --- .../EstablishmentFavouriteProvider.tsx | 70 +++++++++++++++++++ .../EstablishmentFavouriteTable.tsx | 47 +++++++++++++ .../favouriteStorage.ts | 20 ++++++ .../EstablishmentFavoriteTable/index.ts | 3 + .../EstablishmentLink/EstablishmentLink.tsx | 24 +++++++ .../index.css | 0 .../EstablishmentsTable.tsx | 3 +- .../PaginatedEstablishmentsTable.tsx | 27 +++---- .../EstablishmentTablePaginated/index.ts | 1 + .../EstablishmentsTableRow.tsx | 34 +++++---- src/components/Logo/Logo.tsx | 5 ++ src/components/Logo/index.css | 0 src/components/Logo/index.ts | 1 + src/components/Logo/logo.svg | 1 + src/components/PageTemplate/PageTemplate.tsx | 21 ++++++ src/components/PageTemplate/index.css | 13 ++++ src/components/PageTemplate/index.ts | 1 + .../SelectAuthorities/SelectAuthorities.tsx | 19 +++-- src/hooks/useFetch.ts | 1 - src/hooks/useFetchEstablishmentList.ts | 19 +++++ src/hooks/useFetchRatingsByAuthority.ts | 1 - src/index.css | 9 ++- src/index.tsx | 5 +- src/routes/Establishment.tsx | 59 +++++++--------- src/routes/HomePage.tsx | 16 ++--- src/static/logo.svg | 1 - 26 files changed, 316 insertions(+), 85 deletions(-) create mode 100644 src/components/EstablishmentFavoriteTable/EstablishmentFavouriteProvider.tsx create mode 100644 src/components/EstablishmentFavoriteTable/EstablishmentFavouriteTable.tsx create mode 100644 src/components/EstablishmentFavoriteTable/favouriteStorage.ts create mode 100644 src/components/EstablishmentFavoriteTable/index.ts create mode 100644 src/components/EstablishmentLink/EstablishmentLink.tsx rename src/components/{EstablishmentTableRow => EstablishmentLink}/index.css (100%) create mode 100644 src/components/EstablishmentTablePaginated/index.ts create mode 100644 src/components/Logo/Logo.tsx create mode 100644 src/components/Logo/index.css create mode 100644 src/components/Logo/index.ts create mode 100644 src/components/Logo/logo.svg create mode 100644 src/components/PageTemplate/PageTemplate.tsx create mode 100644 src/components/PageTemplate/index.css create mode 100644 src/components/PageTemplate/index.ts create mode 100644 src/hooks/useFetchEstablishmentList.ts delete mode 100644 src/static/logo.svg diff --git a/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteProvider.tsx b/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteProvider.tsx new file mode 100644 index 0000000..b6752cf --- /dev/null +++ b/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteProvider.tsx @@ -0,0 +1,70 @@ +import { PropsWithChildren, createContext, useEffect, useState } from "react"; +import { useFetchEstablishmentList } from "../../hooks/useFetchEstablishmentList"; +import { + getFavouriteItemsStorage, + removeFavouriteItemStorage, + setFavouriteItemStorage, +} from "./favouriteStorage"; + +export type EstablishmentFavourite = Pick< + EstablishmentDetailDto, + "FHRSID" | "BusinessName" | "RatingValue" +>; + +export const EstablishmentFavouriteContext = createContext<{ + items: EstablishmentFavourite[]; + loading: boolean; + getIsInFavourite: (id: EstablishmentFavourite["FHRSID"]) => boolean; + addFavouriteItem: (item: EstablishmentFavourite) => void; + removeFavouriteItem: (id: EstablishmentFavourite["FHRSID"]) => void; +}>({ + items: [], + loading: true, + getIsInFavourite: () => false, + addFavouriteItem: () => {}, + removeFavouriteItem: () => {}, +}); + +export const removeFromFavourite = (id: EstablishmentFavourite["FHRSID"]) => {}; + +export const EstablishmentFavouriteProvider = ({ + children, +}: PropsWithChildren<{}>) => { + const { data, loading } = useFetchEstablishmentList( + getFavouriteItemsStorage() + ); + const [items, setItems] = useState([]); + + useEffect(() => { + setItems(data?.establishments ?? []); + }, [data]); + + const getIsInFavourite = (id: EstablishmentFavourite["FHRSID"]) => { + return items.some((item) => item.FHRSID === id); + }; + + const addFavouriteItem = (item: EstablishmentFavourite) => { + setFavouriteItemStorage(item.FHRSID); + setItems([...items, item]); + }; + + const removeFavouriteItem = (id: EstablishmentFavourite["FHRSID"]) => { + const filtered = items.filter((item) => item.FHRSID !== id); + removeFavouriteItemStorage(id); + setItems(filtered); + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteTable.tsx b/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteTable.tsx new file mode 100644 index 0000000..e180c9a --- /dev/null +++ b/src/components/EstablishmentFavoriteTable/EstablishmentFavouriteTable.tsx @@ -0,0 +1,47 @@ +import { useContext } from "react"; +import { EstablishmentFavouriteContext } from "./EstablishmentFavouriteProvider"; +import { EstablishmentLink } from "../EstablishmentLink/EstablishmentLink"; + +export const EstablishmentFavouriteTable = () => { + const { items, loading, removeFavouriteItem } = useContext( + EstablishmentFavouriteContext + ); + return ( + + + + + + + + {loading && ( + + + + )} + + + {items.map((favourite) => ( + + + + + + ))} + +
Business NameRating Value
Loading ...
+ + {favourite.BusinessName} + + {favourite.RatingValue} + +
+ ); +}; diff --git a/src/components/EstablishmentFavoriteTable/favouriteStorage.ts b/src/components/EstablishmentFavoriteTable/favouriteStorage.ts new file mode 100644 index 0000000..2e8b0c9 --- /dev/null +++ b/src/components/EstablishmentFavoriteTable/favouriteStorage.ts @@ -0,0 +1,20 @@ +const LOCAL_STORAGE_KEY = "favEstablishments"; + +type FavouriteEstablishments = number[]; + +export const getFavouriteItemsStorage = (): FavouriteEstablishments => { + const val = localStorage.getItem(LOCAL_STORAGE_KEY); + return val !== null ? JSON.parse(val) : []; +}; + +export const setFavouriteItemStorage = (key: number) => { + const newValue = [...getFavouriteItemsStorage(), key]; + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newValue)); +}; + +export const removeFavouriteItemStorage = (key: number) => { + const newValue = getFavouriteItemsStorage().filter( + (favourite) => favourite !== key + ); + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newValue)); +}; diff --git a/src/components/EstablishmentFavoriteTable/index.ts b/src/components/EstablishmentFavoriteTable/index.ts new file mode 100644 index 0000000..25415e6 --- /dev/null +++ b/src/components/EstablishmentFavoriteTable/index.ts @@ -0,0 +1,3 @@ +export * from "./EstablishmentFavouriteProvider"; +export * from "./EstablishmentFavouriteTable"; +export * from "./favouriteStorage"; diff --git a/src/components/EstablishmentLink/EstablishmentLink.tsx b/src/components/EstablishmentLink/EstablishmentLink.tsx new file mode 100644 index 0000000..025d222 --- /dev/null +++ b/src/components/EstablishmentLink/EstablishmentLink.tsx @@ -0,0 +1,24 @@ +import { PropsWithChildren } from "react"; +import { useNavigate } from "react-router-dom"; +import "./index.css"; + +type EstablishmentLinkProps = PropsWithChildren<{ + id: number; +}>; + +export const EstablishmentLink = ({ id, children }: EstablishmentLinkProps) => { + const navigate = useNavigate(); + const linkToEstablishment = `/establishment/${id}`; + return ( + { + event.preventDefault(); + navigate(linkToEstablishment); + }} + className="establishment__link" + > + {children} + + ); +}; diff --git a/src/components/EstablishmentTableRow/index.css b/src/components/EstablishmentLink/index.css similarity index 100% rename from src/components/EstablishmentTableRow/index.css rename to src/components/EstablishmentLink/index.css diff --git a/src/components/EstablishmentTable/EstablishmentsTable.tsx b/src/components/EstablishmentTable/EstablishmentsTable.tsx index d803c9c..2fdb244 100644 --- a/src/components/EstablishmentTable/EstablishmentsTable.tsx +++ b/src/components/EstablishmentTable/EstablishmentsTable.tsx @@ -19,10 +19,11 @@ export const EstablishmentsTable = ({ Business Name Rating Value + {isLoading && ( - Loading ... + Loading ... )} diff --git a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx index fd8d728..e81b840 100644 --- a/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx +++ b/src/components/EstablishmentTablePaginated/PaginatedEstablishmentsTable.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { SelectAuthorities } from "../SelectAuthorities"; import { TableWithFilters } from "./TableWithFilters"; import { TableBasic } from "./TableBasic"; -import { Container } from "../Container"; export const PaginatedEstablishmentsTable = () => { const [localAuthorityId, setLocalAuthorityId] = useState< @@ -10,18 +9,20 @@ export const PaginatedEstablishmentsTable = () => { >(); return ( - + <>

Food Hygiene Ratings

- { - setLocalAuthorityId(val); - }} - /> - {localAuthorityId ? ( - - ) : ( - - )} -
+
+ { + setLocalAuthorityId(val); + }} + /> + {localAuthorityId ? ( + + ) : ( + + )} +
+ ); }; diff --git a/src/components/EstablishmentTablePaginated/index.ts b/src/components/EstablishmentTablePaginated/index.ts new file mode 100644 index 0000000..a17a337 --- /dev/null +++ b/src/components/EstablishmentTablePaginated/index.ts @@ -0,0 +1 @@ +export * from "./PaginatedEstablishmentsTable"; diff --git a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx index d63231a..7cd1966 100644 --- a/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx +++ b/src/components/EstablishmentTableRow/EstablishmentsTableRow.tsx @@ -1,5 +1,6 @@ -import { useNavigate } from "react-router-dom"; -import "./index.css"; +import { useContext } from "react"; +import { EstablishmentFavouriteContext } from "../EstablishmentFavoriteTable"; +import { EstablishmentLink } from "../EstablishmentLink/EstablishmentLink"; type EstablishmentsTableRowProps = { establishment: Pick< @@ -11,23 +12,30 @@ type EstablishmentsTableRowProps = { export const EstablishmentsTableRow = ({ establishment, }: EstablishmentsTableRowProps) => { - const navigate = useNavigate(); - const linkToEstablishment = `/establishment/${establishment.FHRSID}`; + const { getIsInFavourite, addFavouriteItem, removeFavouriteItem } = + useContext(EstablishmentFavouriteContext); + const isChecked = getIsInFavourite(establishment.FHRSID); return ( - { - event.preventDefault(); - navigate(linkToEstablishment); - }} - className="establishment__link" - > + {establishment.BusinessName} - + {establishment.RatingValue} + + { + if (event.target.checked) { + addFavouriteItem(establishment); + } else { + removeFavouriteItem(establishment.FHRSID); + } + }} + /> + ); }; diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx new file mode 100644 index 0000000..e7e249a --- /dev/null +++ b/src/components/Logo/Logo.tsx @@ -0,0 +1,5 @@ +import { ReactComponent as LogoSvg } from "./logo.svg"; + +export const Logo = () => { + return ; +}; diff --git a/src/components/Logo/index.css b/src/components/Logo/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts new file mode 100644 index 0000000..bd3113f --- /dev/null +++ b/src/components/Logo/index.ts @@ -0,0 +1 @@ +export * from "./Logo"; diff --git a/src/components/Logo/logo.svg b/src/components/Logo/logo.svg new file mode 100644 index 0000000..ba02ca8 --- /dev/null +++ b/src/components/Logo/logo.svg @@ -0,0 +1 @@ +site-logo-white \ No newline at end of file diff --git a/src/components/PageTemplate/PageTemplate.tsx b/src/components/PageTemplate/PageTemplate.tsx new file mode 100644 index 0000000..eff34a6 --- /dev/null +++ b/src/components/PageTemplate/PageTemplate.tsx @@ -0,0 +1,21 @@ +import { PropsWithChildren } from "react"; +import { Logo } from "../Logo"; +import "./index.css"; +import { Container } from "../Container"; +import { EstablishmentFavouriteTable } from "../EstablishmentFavoriteTable"; + +export const PageTemplate = ({ children }: PropsWithChildren<{}>) => { + return ( + <> +
+ +
+
+ {children} + + + +
+ + ); +}; diff --git a/src/components/PageTemplate/index.css b/src/components/PageTemplate/index.css new file mode 100644 index 0000000..4f45254 --- /dev/null +++ b/src/components/PageTemplate/index.css @@ -0,0 +1,13 @@ +.header { + padding: 20px 0; +} + +main { + display: flex; + flex-direction: column; + height: 100%; +} + +main div:last-child { + margin-top: auto; +} diff --git a/src/components/PageTemplate/index.ts b/src/components/PageTemplate/index.ts new file mode 100644 index 0000000..55bb775 --- /dev/null +++ b/src/components/PageTemplate/index.ts @@ -0,0 +1 @@ +export * from "./PageTemplate"; diff --git a/src/components/SelectAuthorities/SelectAuthorities.tsx b/src/components/SelectAuthorities/SelectAuthorities.tsx index 1a6dc32..cd75172 100644 --- a/src/components/SelectAuthorities/SelectAuthorities.tsx +++ b/src/components/SelectAuthorities/SelectAuthorities.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useFetchAuthorities } from "../../hooks/useFetchAuthorities"; type SelectAuthoritiesProps = { @@ -8,6 +8,15 @@ type SelectAuthoritiesProps = { export const SelectAuthorities = ({ onChange }: SelectAuthoritiesProps) => { const { data, error, loading } = useFetchAuthorities(); const [value, setValue] = useState(undefined); + + useEffect(() => { + loading ? setValue("loading") : setValue(undefined); + }, [loading]); + + useEffect(() => { + error ? setValue("error") : setValue(undefined); + }, [error]); + return (