From 5c8589d9e7d963287a9c3b907a6da4b647d441fa Mon Sep 17 00:00:00 2001 From: Tania Kuzmenko Date: Tue, 17 Dec 2024 02:34:44 +0200 Subject: [PATCH] Solution --- README.md | 2 +- src/App.scss | 4 + src/App.tsx | 6 +- src/Pages/HomePage.tsx | 1 + src/Pages/PageNotFound.tsx | 1 + src/Pages/PeoplePage.tsx | 10 + src/Root.tsx | 32 ++ src/components/Navbar.tsx | 30 +- src/components/PeopleFilters.tsx | 145 +++--- src/components/PeoplePage.tsx | 33 +- src/components/PeopleTable.tsx | 735 +++++-------------------------- src/index.tsx | 12 +- src/store/FilterContext.tsx | 112 +++++ src/store/PeopleContext.tsx | 118 +++++ src/utils/utils.ts | 7 + 15 files changed, 536 insertions(+), 712 deletions(-) create mode 100644 src/Pages/HomePage.tsx create mode 100644 src/Pages/PageNotFound.tsx create mode 100644 src/Pages/PeoplePage.tsx create mode 100644 src/Root.tsx create mode 100644 src/store/FilterContext.tsx create mode 100644 src/store/PeopleContext.tsx create mode 100644 src/utils/utils.ts diff --git a/README.md b/README.md index 064a39440..0cff89658 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ implement the ability to filter and sort people in the table. - Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). - Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). - Open one more terminal and run tests with `npm test` to ensure your solution is correct. -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_people-table-advanced/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://tania-kuzmenko.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/src/App.scss b/src/App.scss index c17d529f4..79992cd31 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,7 @@ iframe { display: none; } + +.section { + margin-top: 3.25em; +} diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..38a68859f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ -import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { Outlet } from 'react-router-dom'; export const App = () => { return ( @@ -10,9 +10,7 @@ export const App = () => {
-

Home Page

-

Page not found

- +
diff --git a/src/Pages/HomePage.tsx b/src/Pages/HomePage.tsx new file mode 100644 index 000000000..153c06306 --- /dev/null +++ b/src/Pages/HomePage.tsx @@ -0,0 +1 @@ +export const HomePage = () =>

Home Page

; diff --git a/src/Pages/PageNotFound.tsx b/src/Pages/PageNotFound.tsx new file mode 100644 index 000000000..a7b602723 --- /dev/null +++ b/src/Pages/PageNotFound.tsx @@ -0,0 +1 @@ +export const PageNotFound = () =>

Page not found

; diff --git a/src/Pages/PeoplePage.tsx b/src/Pages/PeoplePage.tsx new file mode 100644 index 000000000..cd060b954 --- /dev/null +++ b/src/Pages/PeoplePage.tsx @@ -0,0 +1,10 @@ +import { PeopleTable } from '../components/PeopleTable'; + +export const PeoplePage = () => { + return ( + <> +

People Page

+ + + ); +}; diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 000000000..8122fb6e2 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,32 @@ +import { + Navigate, + Route, + HashRouter as Router, + Routes, +} from 'react-router-dom'; +import { App } from './App'; +import { PeopleProvider } from './store/PeopleContext'; +import { HomePage } from './Pages/HomePage'; +import { PeoplePage } from './components/PeoplePage'; +import { PageNotFound } from './Pages/PageNotFound'; +import { FilterProvider } from './store/FilterContext'; + +export const Root = () => ( + + + + + }> + } /> + } /> + }> + } /> + + + } /> + + + + + +); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..437be7a23 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,6 @@ +import { NavLink } from 'react-router-dom'; +import { getLinkClass } from '../utils/utils'; + export const Navbar = () => { return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..05733fa7d 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,58 @@ +import { Link, NavLink } from 'react-router-dom'; +import { useFilters } from '../store/FilterContext'; +import classNames from 'classnames'; + export const PeopleFilters = () => { + const { query, sexs, centuries, setSearchWith, getSearchWith, searchParams } = + useFilters(); + + function handleQueryChange(event: React.ChangeEvent) { + setSearchWith({ query: event.target.value }); + } + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..794333202 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,29 +1,38 @@ import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { usePeople } from '../store/PeopleContext'; export const PeoplePage = () => { + const { isLoading, errorMsg, filteredPeople } = usePeople(); + + if (isLoading) { + return ; + } + return ( <>

People Page

-
- -
+ {!isLoading && ( +
+ +
+ )}
- - -

Something went wrong

- -

There are no people on the server

- -

There are no people matching the current search criteria

- - + {!isLoading && errorMsg && ( +

+ {errorMsg} +

+ )} + {!isLoading && !errorMsg && filteredPeople.length === 0 && ( +

{errorMsg}

+ )} + {filteredPeople.length > 0 && }
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..0f1bae0ab 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,30 @@ +import { Link, useParams } from 'react-router-dom'; +import { usePeople } from '../store/PeopleContext'; +import { Person } from '../types'; +import classNames from 'classnames'; +import { useFilters } from '../store/FilterContext'; + /* eslint-disable jsx-a11y/control-has-associated-label */ + +function isPersonFemale(person: Person | undefined): boolean { + return person?.sex === 'f'; +} + +function findPersonByName( + people: Person[], + name: string | null, +): Person | undefined { + return people.find(person => person.name === name); +} + export const PeopleTable = () => { + const { filteredPeople } = usePeople(); + const { slug } = useParams(); + const selectedPerson = slug || 'notSelected'; + const { getNextSortOrder, getSearchWith, searchParams, sort, order } = + useFilters(); + const sortField = ['name', 'sex', 'born', 'died']; + return ( { > - - - - - - - - + {sortField.map(field => ( + + ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {filteredPeople.map(person => { + const mother = findPersonByName(filteredPeople, person.motherName); + const father = findPersonByName(filteredPeople, person.fatherName); + + return ( + + + + + + + + + + + ); + })}
- - Name - - - - - - - - - Sex - - - - - - - - - Born - - - - - - - - - Died - - - - - - - + + {field.charAt(0).toUpperCase() + field.slice(1)} + {/* Capitalize column name */} + + + + + + + Mother Father
- Pieter Haverbeke - m16021642- - - Lieven van Haverbeke - -
- - Anna van Hecke - - f16071670Martijntken BeelaertPaschasius van Hecke
- Lieven Haverbeke - m16311676 - - Anna van Hecke - - - Pieter Haverbeke -
- - Elisabeth Hercke - - f16321674Margriet de BrabanderWillem Hercke
- Daniel Haverbeke - m16521723 - - Elisabeth Hercke - - - Lieven Haverbeke -
- - Joanna de Pape - - f16541723Petronella WautersVincent de Pape
- - Martina de Pape - - f16661727Petronella WautersVincent de Pape
- Willem Haverbeke - m16681731 - - Elisabeth Hercke - - - Lieven Haverbeke -
- Jan Haverbeke - m16711731 - - Elisabeth Hercke - - - Lieven Haverbeke -
- - Maria de Rycke - - f16831724Laurentia van VlaenderenFrederik de Rycke
- - Livina Haverbeke - - f16921743 - - Joanna de Pape - - - Daniel Haverbeke -
- - Pieter Bernard Haverbeke - - m16951762Petronella Wauters - Willem Haverbeke -
- - Lieven de Causmaecker - - m16961724Joanna ClaesCarel de Causmaecker
- - Jacoba Lammens - - f16991740Livina de VriezeLieven Lammens
- Pieter de Decker - m17051780Petronella van de SteeneJoos de Decker
- - Laurentia Haverbeke - - f17101786 - - Maria de Rycke - - - Jan Haverbeke -
- - Elisabeth Haverbeke - - f17111754 - - Maria de Rycke - - - Jan Haverbeke -
- Jan van Brussel - m17141748Joanna van RootenJacobus van Brussel
- - Bernardus de Causmaecker - - m17211789 - - Livina Haverbeke - - - - Lieven de Causmaecker - -
- - Jan Francies Haverbeke - - m17251779Livina de Vrieze - - Pieter Bernard Haverbeke - -
- - Angela Haverbeke - - f17281734Livina de Vrieze - - Pieter Bernard Haverbeke - -
- - Petronella de Decker - - f17311781 - - Livina Haverbeke - - - Pieter de Decker -
- - Jacobus Bernardus van Brussel - - m17361809 - - Elisabeth Haverbeke - - - Jan van Brussel -
- - Pieter Antone Haverbeke - - m17531798 - - Petronella de Decker - - - - Jan Francies Haverbeke - -
- - Jan Frans van Brussel - - m17611833- - - Jacobus Bernardus van Brussel - -
- - Livina Sierens - - f17611826Maria van WaesJan Sierens
- - Joanna de Causmaecker - - f17621807- - - Bernardus de Causmaecker - -
- Carel Haverbeke - m17961837 - - Livina Sierens - - - - Pieter Antone Haverbeke - -
- - Maria van Brussel - - f18011834 - - Joanna de Causmaecker - - - - Jan Frans van Brussel - -
- Carolus Haverbeke - m18321905 - - Maria van Brussel - - - Carel Haverbeke -
- - Maria Sturm - - f18351917Seraphina SpelierCharles Sturm
- - Emma de Milliano - - f18761956Sophia van DammePetrus de Milliano
- Emile Haverbeke - m18771968 - - Maria Sturm - - - Carolus Haverbeke -
+ + {person.name} + + {person.sex}{person.born}{person.died} + {mother ? ( + + {mother.name} + + ) : ( + person.motherName || ' - ' + )} + + {father ? ( + + {father.name} + + ) : ( + person.fatherName || ' - ' + )} +
); diff --git a/src/index.tsx b/src/index.tsx index d72ba5730..cd262265d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,9 @@ import { createRoot } from 'react-dom/client'; -import { HashRouter as Router } from 'react-router-dom'; - import 'bulma/css/bulma.css'; import '@fortawesome/fontawesome-free/css/all.css'; -import { App } from './App'; +import { Root } from './Root'; + +const container = document.getElementById('root') as HTMLDivElement; -createRoot(document.getElementById('root') as HTMLDivElement).render( - - - , -); +createRoot(container).render(); diff --git a/src/store/FilterContext.tsx b/src/store/FilterContext.tsx new file mode 100644 index 000000000..95d555362 --- /dev/null +++ b/src/store/FilterContext.tsx @@ -0,0 +1,112 @@ +import React, { ReactNode, useContext } from 'react'; +import { SetURLSearchParams, useSearchParams } from 'react-router-dom'; + +type FilterContextType = { + query: string | ''; + centuries: string[]; + sexs: string[]; + searchParams: URLSearchParams; + setSearchParams: SetURLSearchParams; + sort: string | ''; + order: string | ''; + getSearchWith: (params: Params, search?: string | URLSearchParams) => string; + setSearchWith: (params: Params) => void; + getNextSortOrder: ( + currentSort: string | '', + currentOrder: string | '', + field: string | '', + ) => { + sort: string | null; + order: string | null; + }; +}; + +type Param = string | number; +type Params = { + [key: string]: Param[] | Param | null; +}; + +export const FilterContext = React.createContext( + undefined, +); + +type Props = { + children: ReactNode; +}; + +export const FilterProvider: React.FC = ({ children }) => { + const [searchParams, setSearchParams] = useSearchParams(); + + const query = searchParams.get('query') || ''; + const sexs = searchParams.getAll('sexs') || []; + const centuries = searchParams.getAll('centuries') || []; + + const sort = searchParams.get('sort') || ''; + const order = searchParams.get('order') || ''; + + function getSearchWith(params: Params, search?: string | URLSearchParams) { + const newParams = new URLSearchParams(search); + + for (const [key, value] of Object.entries(params)) { + if (value === null) { + newParams.delete(key); + } else if (Array.isArray(value)) { + newParams.delete(key); + value.forEach(item => newParams.append(key, item.toString())); + } else { + newParams.set(key, value.toString()); + } + } + + return newParams.toString(); + } + + function setSearchWith(params: Params) { + const search = getSearchWith(params, searchParams); + + setSearchParams(search); + } + + const getNextSortOrder = ( + currentSort: string | '', + currentOrder: string | '', + field: string | '', + ) => { + if (currentSort !== field) { + return { sort: field, order: 'asc' }; + } + + if (currentOrder === 'asc') { + return { sort: field, order: 'desc' }; + } + + return { sort: null, order: null }; + }; + + const value = { + query, + centuries, + sexs, + searchParams, + setSearchParams, + getSearchWith, + setSearchWith, + getNextSortOrder, + sort, + order, + }; + + return ( + {children} + ); +}; + +export const useFilters = () => { + const context = useContext(FilterContext); + + if (!context) { + throw new Error('usePeopleContext must be used within a FilterProvider'); + } + + return context; +}; diff --git a/src/store/PeopleContext.tsx b/src/store/PeopleContext.tsx new file mode 100644 index 000000000..1cedcb910 --- /dev/null +++ b/src/store/PeopleContext.tsx @@ -0,0 +1,118 @@ +import React, { + ReactNode, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { Person } from '../types'; +import { getPeople } from '../api'; +import { useFilters } from './FilterContext'; + +type PeopleContextType = { + isLoading: boolean; + errorMsg: string; + filteredPeople: Person[]; +}; + +export const PeopleContext = React.createContext( + undefined, +); + +type Props = { + children: ReactNode; +}; + +export const PeopleProvider: React.FC = ({ children }) => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(''); + const { query, sexs, centuries, sort, order } = useFilters(); + + const filteredPeople = useMemo(() => { + let preparedPeople = [...people]; + + if (query) { + const lowerCaseQuery = query.toLowerCase(); + + preparedPeople = preparedPeople.filter(person => + person.name.toLowerCase().includes(lowerCaseQuery), + ); + } + + if (sexs.length > 0) { + preparedPeople = preparedPeople.filter(person => + sexs.includes(person.sex), + ); + } + + if (centuries.length > 0) { + preparedPeople = preparedPeople.filter(person => { + const personCentury = Math.ceil(person.born / 100); + + return centuries.includes(personCentury.toString()); + }); + } + + if (sort) { + preparedPeople.sort((a, b) => { + const valueA = a[sort as keyof Person]; + const valueB = b[sort as keyof Person]; + + if (valueA === null || valueB === null) { + return 0; + } + + if (valueA && valueB && valueA < valueB) { + return order === 'desc' ? 1 : -1; + } + + if (valueA && valueB && valueA > valueB) { + return order === 'desc' ? -1 : 1; + } + + return 0; + }); + } + + return preparedPeople; + }, [people, query, sexs, centuries, sort, order]); + + useEffect(() => { + const fetchPeople = async () => { + setIsLoading(true); + setErrorMsg(''); + try { + const fetchedPeople = await getPeople(); + + setPeople(fetchedPeople); + } catch { + setErrorMsg('Something went wrong'); + } finally { + setIsLoading(false); + } + }; + + fetchPeople(); + }, []); + + const value = { + isLoading, + errorMsg, + filteredPeople, + }; + + return ( + {children} + ); +}; + +export const usePeople = () => { + const context = useContext(PeopleContext); + + if (!context) { + throw new Error('usePeopleContext must be used within a PeopleProvider'); + } + + return context; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 000000000..66dfe2ebd --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,7 @@ +import classNames from 'classnames'; + +export const getLinkClass = ( + isActive: boolean, + baseClass: string, + activeClass: string, +) => classNames(baseClass, { [activeClass]: isActive });