From 73adf85efc11f31915876e2155ac64c93cf4a961 Mon Sep 17 00:00:00 2001 From: Karol Kocis Date: Sun, 24 Mar 2024 14:00:22 +0100 Subject: [PATCH 1/3] user stories 1, 2, 3, 4 --- package.json | 4 +- src/App.tsx | 15 +- src/api/ratingsAPI.ts | 63 +++++++- src/components/DropdownFilter.tsx | 33 +++++ src/components/EstablishmentDetailPage.tsx | 90 ++++++++++++ src/components/EstablishmentsTableRow.tsx | 10 +- src/components/GenericButton.tsx | 15 ++ src/components/LoadingSpinner.tsx | 17 +++ .../PaginatedEstablishmentsTable.tsx | 137 ++++++++++++++---- src/routing/routesConfig.tsx | 12 ++ src/styles/DropdownFilter.module.css | 49 +++++++ src/styles/EstablishmentDetailPage.module.css | 11 ++ src/styles/EstablishmentsTableRow.module.css | 7 + src/styles/GenericButton.module.css | 16 ++ src/styles/LoadingSpinner.module.css | 22 +++ .../PaginatedEstablishmentsTable.module.css | 11 ++ yarn.lock | 25 ++++ 17 files changed, 497 insertions(+), 40 deletions(-) create mode 100644 src/components/DropdownFilter.tsx create mode 100644 src/components/EstablishmentDetailPage.tsx create mode 100644 src/components/GenericButton.tsx create mode 100644 src/components/LoadingSpinner.tsx create mode 100644 src/routing/routesConfig.tsx create mode 100644 src/styles/DropdownFilter.module.css create mode 100644 src/styles/EstablishmentDetailPage.module.css create mode 100644 src/styles/EstablishmentsTableRow.module.css create mode 100644 src/styles/GenericButton.module.css create mode 100644 src/styles/LoadingSpinner.module.css create mode 100644 src/styles/PaginatedEstablishmentsTable.module.css diff --git a/package.json b/package.json index 185bbd5..27cc6bc 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ ] }, "devDependencies": { - "jest-fetch-mock": "^3.0.3" + "@types/react-css-modules": "^4.6.8", + "jest-fetch-mock": "^3.0.3", + "react-router-dom": "^6.22.3" } } diff --git a/src/App.tsx b/src/App.tsx index 6e1f19b..07cd007 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,17 @@ -import React, { Component } from "react"; +import { Component } from "react"; import HomePage from "./components/HomePage"; - +import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; +import { EstablishmentDetailPage } from './components/EstablishmentDetailPage'; class App extends Component { render() { - return ; - } + return( + + + } /> + } /> + + + )} } export default App; diff --git a/src/api/ratingsAPI.ts b/src/api/ratingsAPI.ts index 1d4080c..9c9e457 100644 --- a/src/api/ratingsAPI.ts +++ b/src/api/ratingsAPI.ts @@ -18,7 +18,42 @@ export type EstablishmentsType = { ]; }; -export function getEstablishmentRatings( +export type AuthoritiesType = { + authorities: {}[]; + meta: { + dataSource: string; + extractDate: string; + itemCount: number; + returncode: string; + totalCount: number; + totalPages: number; + pageSize: number; + pageNumber: number; + }; + links: [ + { + rel: string; + href: string; + } + ]; +}; + +export type EstablishmentDetailType = { + AddressLine1: string, + AddressLine2: string, + AddressLine3: string, + AddressLine4: string, + BusinessName: string, + businessTypeID: number, + FHRSID: number, + localAuthorityBusinessID: string, + PostCode: string, + RatingDate: string, + RatingValue: string, + scores: {} +} + +export async function getEstablishmentRatings( pageNum: number ): Promise { return fetch( @@ -26,3 +61,29 @@ export function getEstablishmentRatings( { headers: { "x-api-version": "2" } } ).then((res) => res.json()); } + +export async function getEstablishmentDetails( + id: string | undefined +): Promise { + return fetch( + `http://api.ratings.food.gov.uk/establishments/${id}`, + { headers: { "x-api-version": "2" } } + ).then((res) => res.json()); +} + +export async function getAuthorities(): Promise { + return fetch( + `http://api.ratings.food.gov.uk/Authorities`, + { headers: { "x-api-version": "2" } } + ).then((res) => res.json()); +} + +export async function filterEstablishmentsByAuthority( + authorityId: number, + pageNum: number +): Promise { + return fetch( + `http://api.ratings.food.gov.uk/Establishments?pageNumber=${pageNum}&pageSize=10&localAuthorityId=${authorityId}`, + { headers: { "x-api-version": "2" } } + ).then((res) => res.json()); +} \ No newline at end of file diff --git a/src/components/DropdownFilter.tsx b/src/components/DropdownFilter.tsx new file mode 100644 index 0000000..9d357a3 --- /dev/null +++ b/src/components/DropdownFilter.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import styles from '../styles/DropdownFilter.module.css' +interface Option { + label: string; + value: string; +} + +interface DropdownProps { + options: Option[]; + defaultValue?: string; + onChange: (value: string) => void; +} + +const Dropdown: React.FC = ({ options, defaultValue, onChange }) => { + const [selectedValue, setSelectedValue] = useState(defaultValue || options[0].value); + + const handleChange = (event: React.ChangeEvent) => { + setSelectedValue(event.target.value); + onChange(event.target.value); + }; + + return ( + + ); +}; + +export default Dropdown; \ No newline at end of file diff --git a/src/components/EstablishmentDetailPage.tsx b/src/components/EstablishmentDetailPage.tsx new file mode 100644 index 0000000..57ff5a0 --- /dev/null +++ b/src/components/EstablishmentDetailPage.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import styles from "../styles/EstablishmentDetailPage.module.css"; +import { + getEstablishmentDetails, + EstablishmentDetailType, +} from "../api/ratingsAPI"; +import LoadingSpinner from "./LoadingSpinner"; +import GenericButton from "./GenericButton"; +import { log } from "console"; + +export const EstablishmentDetailPage = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<{ + message: string; + [key: string]: string; + }>(); + const [details, setDetails] = useState(); + + useEffect(() => { + getEstablishmentDetails(id) + .then( + (result) => { + console.log("result useeffect", result); + + setDetails(result); + }, + (error) => { + setError(error); + } + ) + .finally(() => { + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const navigateBack = () => { + navigate("/"); + }; + const formatedDate = (date: string | undefined) => { + return date ? new Date(date).toLocaleDateString() : "N/A"; + }; + + const displayRating = (values: {} | undefined) => { + console.log("values", values); + + const entries = values ? Object.entries(values) : []; + console.log("entries", entries); + return ( +
    + {entries.map((entry) => ( +
  • +

    {entry[0]}: {entry[1]}

    +
  • + ))} +
+ ); + }; + + return ( +
+ {loading ? ( + + ) : ( +
+

{details?.BusinessName}

+

+ Address:{" "} + {`${details?.AddressLine1} ${details?.AddressLine2} + ${details?.AddressLine3}, + ${details?.PostCode} + ${details?.AddressLine4}`}{" "} +

+

+ Date of inspection: {formatedDate(details?.RatingDate)} +

+
+

Rating:{" "}{details?.RatingValue}

+
+ {displayRating(details?.scores)} + + +
+ )} +
+ ); +}; diff --git a/src/components/EstablishmentsTableRow.tsx b/src/components/EstablishmentsTableRow.tsx index c4cf644..2a9df52 100644 --- a/src/components/EstablishmentsTableRow.tsx +++ b/src/components/EstablishmentsTableRow.tsx @@ -1,10 +1,12 @@ +import styles from '../styles/EstablishmentsTableRow.module.css'; +import {Link } from 'react-router-dom'; export const EstablishmentsTableRow: React.FC<{ establishment: { [key: string]: string } | null | undefined; }> = ({ establishment }) => { return ( - - {establishment?.BusinessName} - {establishment?.RatingValue} - + + {establishment?.BusinessName} + {establishment?.RatingValue} + ); }; diff --git a/src/components/GenericButton.tsx b/src/components/GenericButton.tsx new file mode 100644 index 0000000..4879f7a --- /dev/null +++ b/src/components/GenericButton.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import styles from "../styles/GenericButton.module.css"; + +const GenericButton: React.FC<{ text: string; onClick?: () => void }> = ({ + text, + onClick, +}) => { + return ( + + ); +}; + +export default GenericButton; diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..34b8f71 --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import styles from '../styles/LoadingSpinner.module.css' + +interface LoadingProps { + message?: string; +} + +const Loading: React.FC = ({ message }) => { + return ( +
+
+ {message &&

{message}

} +
+ ); +}; + +export default Loading; \ No newline at end of file diff --git a/src/components/PaginatedEstablishmentsTable.tsx b/src/components/PaginatedEstablishmentsTable.tsx index af6d64a..9852d0c 100644 --- a/src/components/PaginatedEstablishmentsTable.tsx +++ b/src/components/PaginatedEstablishmentsTable.tsx @@ -1,8 +1,20 @@ import { useState, useEffect } from "react"; import { EstablishmentsTable } from "./EstablishmentsTable"; import { EstablishmentsTableNavigation } from "./EstablishmentsTableNavigation"; -import { getEstablishmentRatings } from "../api/ratingsAPI"; +import { + getAuthorities, + getEstablishmentRatings, + AuthoritiesType, + filterEstablishmentsByAuthority, +} from "../api/ratingsAPI"; +import LoadingSpinner from "./LoadingSpinner"; +import Dropdown from "./DropdownFilter"; +import styles from "../styles/PaginatedEstablishmentsTable.module.css"; +interface Option { + label: string; + value: string; +} const tableStyle = { background: "rgba(51, 51, 51, 0.9)", padding: "10px", @@ -12,49 +24,101 @@ const tableStyle = { }; export const PaginatedEstablishmentsTable = () => { - const [error, setError] = - useState<{ message: string; [key: string]: string }>(); + 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); - + const [currentFilter, setCurrentFilter] = useState("all"); + const [loading, setLoading] = useState(true); + const [authoritiesDropdownOptions, setAuthoritiesDropdownOptions] = useState< + Option[] + >([{ label: "All", value: "all" }]); useEffect(() => { - getEstablishmentRatings(pageNum).then( + getAuthorities().then( (result) => { - setEstablishments(result?.establishments); + handleAuthoritiesOptions(result?.authorities); }, (error) => { setError(error); } ); + callApiEstablishmentsRatings(pageNum); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const handleAuthoritiesOptions = ( + authoritiesList: { [key: string]: string }[] + ) => { + const filteredOptions = authoritiesDropdownOptions; + authoritiesList.forEach((authority) => { + filteredOptions.push({ + label: authority.Name, + value: authority.LocalAuthorityId, + }); + }); + setAuthoritiesDropdownOptions(filteredOptions); + }; + async function handlePreviousPage() { + setLoading(true); pageNum > 1 && setPageNum(pageNum - 1); - getEstablishmentRatings(pageNum).then( - (result) => { - setEstablishments(result.establishments); - }, - (error) => { - setError(error); - } - ); + if (currentFilter === "all") { + callApiEstablishmentsRatings(pageNum); + } else { + callApiFilterEstablishments(currentFilter, pageNum); + } } - async function handleNextPage() { + setLoading(true); pageNum < pageCount && setPageNum(pageNum + 1); - getEstablishmentRatings(pageNum).then( - (result) => { - setEstablishments(result.establishments); - }, - (error) => { - setError(error); - } - ); + if (currentFilter === "all") { + callApiEstablishmentsRatings(pageNum); + } else { + callApiFilterEstablishments(currentFilter, pageNum); + } } + const handleValueChange = (value: string) => { + setLoading(true); + console.log("Selected value:", value); + setCurrentFilter(value); + callApiFilterEstablishments(value, pageNum); + }; + + const callApiEstablishmentsRatings = async (pageNum: number) => { + getEstablishmentRatings(pageNum) + .then( + (result) => { + setEstablishments(result.establishments); + }, + (error) => { + setError(error); + } + ) + .finally(() => { + setLoading(false); + }); + }; + + const callApiFilterEstablishments = async (id: string, pageNum: number) => { + filterEstablishmentsByAuthority(Number.parseInt(id), pageNum) + .then( + (result) => { + setEstablishments(result?.establishments); + }, + (error) => { + setError(error); + } + ) + .finally(() => { + setLoading(false); + }); + }; if (error) { return
Error: {error.message}
; @@ -62,13 +126,26 @@ export const PaginatedEstablishmentsTable = () => { return (

Food Hygiene Ratings

- - +
+ Filter by authorities + +
+ {loading ? ( + + ) : ( +
+ + +
+ )}
); } diff --git a/src/routing/routesConfig.tsx b/src/routing/routesConfig.tsx new file mode 100644 index 0000000..128a7b4 --- /dev/null +++ b/src/routing/routesConfig.tsx @@ -0,0 +1,12 @@ +import App from '../App'; +import { EstablishmentDetailPage } from '../components/EstablishmentDetailPage'; + +interface RouteConfig { + path: string; + element: React.ReactElement; +} + +export const routes: RouteConfig[] = [ + { path: '/', element: }, + { path: '/detail/:id', element: } +]; \ No newline at end of file diff --git a/src/styles/DropdownFilter.module.css b/src/styles/DropdownFilter.module.css new file mode 100644 index 0000000..cd82b9f --- /dev/null +++ b/src/styles/DropdownFilter.module.css @@ -0,0 +1,49 @@ +.dropdown { + background: none; + border: 2px solid rgba(209, 69, 8, 0.3); + padding: 5px; + min-width: 200px; + border-radius: 4px; + font-size: 20px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: hsl(0, 0%, 100%); + cursor: pointer; + + &:hover { + border-color: rgba(0, 0, 0, 0.5); + } + &:focus { + outline: none; + border-color: rgba(0, 0, 0, 0.7); + } +} + +.dropdown option:checked { + background: #d14508; +} + +/* Hide the default arrow by targeting the down arrow element within the select */ +.dropdown select::-ms-expand-downarrow { + display: none; +} + +/* Style the dropdown content (list of options) */ +.dropdown-content { + display: none; + position: absolute; + background: none; + min-width: 100%; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown:hover .dropdown-content { + display: block; +} \ No newline at end of file diff --git a/src/styles/EstablishmentDetailPage.module.css b/src/styles/EstablishmentDetailPage.module.css new file mode 100644 index 0000000..2068e92 --- /dev/null +++ b/src/styles/EstablishmentDetailPage.module.css @@ -0,0 +1,11 @@ + .detailTable { + background-color: rgba(51, 51, 51, 0.9); + padding: 10px; + width: max-content; + margin: 65px 0px 0px 50px; + color: white; + } + + .details { + font-size: 20px; + } \ No newline at end of file diff --git a/src/styles/EstablishmentsTableRow.module.css b/src/styles/EstablishmentsTableRow.module.css new file mode 100644 index 0000000..6e59a03 --- /dev/null +++ b/src/styles/EstablishmentsTableRow.module.css @@ -0,0 +1,7 @@ +.tableRow { + font-size: 20px; +} +.link{ + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/src/styles/GenericButton.module.css b/src/styles/GenericButton.module.css new file mode 100644 index 0000000..8b60d24 --- /dev/null +++ b/src/styles/GenericButton.module.css @@ -0,0 +1,16 @@ +.button { + background-color: #d14508; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; +} + +.button:hover { + background-color: #d144089b; +} \ No newline at end of file diff --git a/src/styles/LoadingSpinner.module.css b/src/styles/LoadingSpinner.module.css new file mode 100644 index 0000000..f336c8e --- /dev/null +++ b/src/styles/LoadingSpinner.module.css @@ -0,0 +1,22 @@ +.loading { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + flex-direction: column +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid #ccc; + border-radius: 50%; + border-top-color: #333; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/src/styles/PaginatedEstablishmentsTable.module.css b/src/styles/PaginatedEstablishmentsTable.module.css new file mode 100644 index 0000000..68cf838 --- /dev/null +++ b/src/styles/PaginatedEstablishmentsTable.module.css @@ -0,0 +1,11 @@ +.filterAuthorities { + padding: 10px 0px 20px 0px; + display: flex; + flex-direction: column; +} + +span { +font-size: 20px; +padding-bottom: 10px; +padding-left: 5px; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9508d03..a9e4ed3 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" @@ -1811,6 +1816,11 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react-css-modules@^4.6.8": + version "4.6.8" + resolved "https://registry.yarnpkg.com/@types/react-css-modules/-/react-css-modules-4.6.8.tgz#70063b911200f4f3e6631e4b867977e22fd45f87" + integrity sha512-rgnrIOTUE1ZULMNH6I994AZCZ09iEQVcoiLDVBRgbNaCEcW4mT5At1aB5k3XW09KT/nLrvo6FqgjECj27i+Fkg== + "@types/react-dom@^17.0.0": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" @@ -9107,6 +9117,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 ed4c7e791f3e3a5bbfee434be8f0a259546524af Mon Sep 17 00:00:00 2001 From: Karol Kocis Date: Sun, 24 Mar 2024 22:40:26 +0100 Subject: [PATCH 2/3] user task 5 --- src/components/DropdownFilter.tsx | 2 +- src/components/EstablishmentsTable.tsx | 6 +- src/components/EstablishmentsTableRow.tsx | 54 ++++++++++-- src/components/FavouriteCheckbox.tsx | 31 +++++++ .../FavouritedEstablishmentsTable.tsx | 20 +++++ src/components/GenericButton.tsx | 7 +- src/components/HomePage.tsx | 25 +++++- .../PaginatedEstablishmentsTable.tsx | 35 ++++++-- src/constants.tsx | 4 + src/context/favouriteItems.tsx | 8 ++ src/styles/DropdownFilter.module.css | 87 ++++++++++--------- src/styles/EstablishmentsTableRow.module.css | 4 + src/styles/FavouriteCheckbox.module.css | 24 +++++ .../FavouritedEstablishmentsTable.module.css | 7 ++ 14 files changed, 254 insertions(+), 60 deletions(-) create mode 100644 src/components/FavouriteCheckbox.tsx create mode 100644 src/components/FavouritedEstablishmentsTable.tsx create mode 100644 src/constants.tsx create mode 100644 src/context/favouriteItems.tsx create mode 100644 src/styles/FavouriteCheckbox.module.css create mode 100644 src/styles/FavouritedEstablishmentsTable.module.css diff --git a/src/components/DropdownFilter.tsx b/src/components/DropdownFilter.tsx index 9d357a3..d26fd3c 100644 --- a/src/components/DropdownFilter.tsx +++ b/src/components/DropdownFilter.tsx @@ -20,7 +20,7 @@ const Dropdown: React.FC = ({ options, defaultValue, onChange }) }; return ( - {options.map((option) => (