diff --git a/README.md b/README.md index 064a39440..57c37526c 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,22 @@ implement the ability to filter and sort people in the table. 1. Keep search params when navigating within the `People` page (when selecting a person or clicking the `People` link). 1. The sidebar with the filters should appear only when people are loaded. 1. `NameFilter` should update the `query` search param with the text from the input. - - show only people with the `name`, `motherName` or `fatherName` that match the query case insensitive; - - if the input is empty there should not be `query` in the search params. + - show only people with the `name`, `motherName` or `fatherName` that match the query case insensitive; + - if the input is empty there should not be `query` in the search params. 1. `CenturyFilter` should allow to choose several centuries or all of them. - - add `centuries` search params using `append` method `getAll` method; + - add `centuries` search params using `append` method `getAll` method; 1. Implement sorting by `name`, `sex`, `born` and `died` by clicking on arrows in a `th`; - - the first click on a column sorts people by the selected field ascending (`a -> z` or `0 -> 9`); - - the second click (when people are already sorted ascending by this field) reverses the order of sorting; - - the third click (when people are already sorted in reversed order by this field) disables sorting; - - use `sort` search param to save sort field; - - add `order=desc` (short for `descending`) if sorted in reversed order; - - if sorting is disabled there should not be `sort` and `order` search params; + - the first click on a column sorts people by the selected field ascending (`a -> z` or `0 -> 9`); + - the second click (when people are already sorted ascending by this field) reverses the order of sorting; + - the third click (when people are already sorted in reversed order by this field) disables sorting; + - use `sort` search param to save sort field; + - add `order=desc` (short for `descending`) if sorted in reversed order; + - if sorting is disabled there should not be `sort` and `order` search params; ## Instructions + - Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save. - 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://Sviatoslav593.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/package-lock.json b/package-lock.json index df276ce8a..57e468fff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8710,6 +8710,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..e04b22ef5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ -import { PeoplePage } from './components/PeoplePage'; +import React from 'react'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { Outlet } from 'react-router-dom'; export const App = () => { return ( @@ -10,9 +11,7 @@ export const App = () => {
-

Home Page

-

Page not found

- +
diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 000000000..086cb115b --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,30 @@ +import { + Navigate, + Route, + HashRouter as Router, + Routes, +} from 'react-router-dom'; +import { App } from './App'; +import React from 'react'; +import { NotFoundPage } from './components/NotFoundPage'; +import { HomePage } from './components/HomePage'; +import { PeoplePage } from './components/PeoplePage'; + +export const Root = () => { + return ( + + + }> + }> + + } /> + } /> + + } /> + } /> + + + + + ); +}; diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx new file mode 100644 index 000000000..07caa1eaa --- /dev/null +++ b/src/components/HomePage.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const HomePage = () =>

Home Page

; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..6033004a5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,13 @@ +import classNames from 'classnames'; +import React from 'react'; +import { NavLink } from 'react-router-dom'; + export const Navbar = () => { + const isLinkActive = ({ isActive }: { isActive: boolean }) => + classNames('navbar-item', { + 'has-background-grey-lighter': isActive, + }); + return ( diff --git a/src/components/NotFoundPage.tsx b/src/components/NotFoundPage.tsx new file mode 100644 index 000000000..7a6a157e2 --- /dev/null +++ b/src/components/NotFoundPage.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const NotFoundPage = () =>

Page not found

; diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..8e8f28080 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,63 @@ +import React from 'react'; +import { getSearchWith, SearchParams } from '../utils/searchHelper'; +import { useSearchParams } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; +import classNames from 'classnames'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const centuriesArray = searchParams.getAll('centuries'); + + const setSearchWith = (params: SearchParams) => { + const search = getSearchWith(searchParams, params); + + setSearchParams(search); + }; + + const handleQueryChange = (event: React.ChangeEvent) => { + setSearchWith({ query: event.target.value.trim() || null }); + }; + + const handleResetFiltersBtn = () => { + setSearchWith({ + sort: sort || null, + order: order || null, + query: null, + sex: null, + centuries: null, + }); + }; + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..3244760e8 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,8 +1,84 @@ import { PeopleFilters } from './PeopleFilters'; -import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Person } from '../types'; +import { getPeople } from '../api'; +import { useSearchParams } from 'react-router-dom'; export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const [searchParams] = useSearchParams(); + const query = searchParams.get('query'); + const sex = searchParams.get('sex'); + const centuries = searchParams.getAll('centuries'); + + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const getBorn = (bornYear: number) => { + return Math.ceil(bornYear / 100); + }; + + const visiblePeople = useMemo(() => { + let filtered = [...people]; + + if (query) { + filtered = filtered.filter( + person => + person.name.toLowerCase().includes(query.toLowerCase()) || + person.fatherName?.toLowerCase().includes(query.toLowerCase()) || + person.motherName?.toLowerCase().includes(query.toLowerCase()), + ); + } + + if (sex) { + filtered = filtered.filter(person => person.sex === sex); + } + + if (centuries.length > 0) { + filtered = filtered.filter(person => + centuries.includes(getBorn(person.born).toString()), + ); + } + + if (sort) { + filtered = [...filtered].sort((a, b) => { + switch (sort) { + case 'born': + case 'died': + return a[sort] - b[sort]; + + case 'name': + case 'sex': + return a[sort].toLowerCase().localeCompare(b[sort].toLowerCase()); + + default: + return 0; + } + }); + } + + if (order === 'desc') { + filtered = [...filtered].reverse(); + } + + return filtered; + }, [query, people, sex, centuries, sort, order]); + + useEffect(() => { + setIsLoading(true); + + getPeople() + .then(receivedPeople => { + setPeople(receivedPeople); + setIsLoading(false); + }) + .catch(() => setErrorMessage('Something went wrong')); + }, []); + return ( <>

People Page

@@ -10,21 +86,15 @@ export const PeoplePage = () => {
- + {!isLoading && }
-
- - -

Something went wrong

- -

There are no people on the server

- -

There are no people matching the current search criteria

- - -
+
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..3f8128917 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,645 +1,183 @@ +import React from 'react'; +import { Person } from '../types'; +import { Loader } from './Loader'; +import { PersonLink } from './PersonLink'; +import { SearchLink } from './SearchLink'; +import { useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +type Props = { + people: Person[]; + errorMessage: string; + isLoading: boolean; +}; - - - - - - - - +export const PeopleTable: React.FC = ({ + people, + errorMessage, + isLoading, +}) => { + const [searchParams] = useSearchParams(); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); - - - - - - - - - -
- - Name - - - - - - - - - Sex - - - - - - - - - Born - - - - - - - - - Died - - - - - - - MotherFather
- 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 -
+ return ( + <> +
+ {isLoading && } + + {errorMessage && ( +

+ {errorMessage} +

+ )} + + {!isLoading && !errorMessage && people.length === 0 && ( +

There are no people on the server

+ )} + {/*

There are no people matching the current search criteria

*/} + + {!isLoading && !errorMessage && people.length > 0 && ( + + + + + + + + + + + + + + + + + + {people.map(person => ( + + ))} + +
+ + Name + + + + + + + + + Sex + + + + + + + + + Born + + + + + + + + + Died + + + + + + + MotherFather
+ )} +
+ ); }; diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..516f24e1e --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Person } from '../types'; +import { Link, useLocation, useParams } from 'react-router-dom'; +import classNames from 'classnames'; + +type Props = { + person: Person; + people: Person[]; +}; + +export const PersonLink: React.FC = ({ person, people }) => { + const { slug } = useParams(); + const { search } = useLocation(); + + // console.log(personSlug); + + const selectedPerson = people.find(x => x.slug === slug); + + const personFather = people.find(x => x.name === person.fatherName); + const personMother = people.find(x => x.name === person.motherName); + + return ( + + + + {person.name} + + + + {person.sex} + {person.born} + {person.died} + + {person.motherName !== null ? ( + personMother !== undefined ? ( + + {personMother.name} + + ) : ( + person.motherName + ) + ) : ( + '-' + )} + + + {person.fatherName !== null ? ( + personFather !== undefined ? ( + + {personFather.name} + + ) : ( + person.fatherName + ) + ) : ( + '-' + )} + + + ); +}; diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..331562a2e 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,5 +1,6 @@ import { Link, LinkProps, useSearchParams } from 'react-router-dom'; import { getSearchWith, SearchParams } from '../utils/searchHelper'; +import React from 'react'; /** * To replace the the standard `Link` we take all it props except for `to` diff --git a/src/index.tsx b/src/index.tsx index d72ba5730..e50f81eb3 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 React from 'react'; +import { Root } from './Root'; -createRoot(document.getElementById('root') as HTMLDivElement).render( - - - , -); +createRoot(document.getElementById('root') as HTMLDivElement).render(); diff --git a/tsconfig.json b/tsconfig.json index cfb168bb2..78bb00370 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,8 @@ { "extends": "@mate-academy/students-ts-config", - "include": [ - "src" - ], + "include": ["src"], "compilerOptions": { + "jsx": "react", "sourceMap": false, "types": ["node", "cypress"] }