diff --git a/README.md b/README.md index 24519caab..e102c3f9f 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://OlhaMomot.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..94e631420 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,15 @@ -import { PeoplePage } from './components/PeoplePage'; -import { Navbar } from './components/Navbar'; - import './App.scss'; +import { Outlet } from 'react-router-dom'; +import { Navbar } from './components/Navbar'; -export const App = () => { - return ( -
- +export const App = () => ( +
+ -
-
-

Home Page

-

Page not found

- -
+
+
+
-
- ); -}; + +
+); diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 000000000..acbc5c07c --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,27 @@ +import { + Navigate, + Route, + HashRouter as Router, + Routes, +} from 'react-router-dom'; +import { App } from './App'; +import { HomePage } from './pages/HomePage'; +import { PeoplePage } from './pages/PeoplePage'; +import { NotFoundPage } from './components/NotFoundPage'; + +export const Root = () => { + return ( + + + }> + } /> + + } /> + + } /> + } /> + + + + ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c2aa20f1c..5f9b6950d 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,24 +1,29 @@ -export const Navbar = () => { - return ( - +); diff --git a/src/components/NotFoundPage.tsx b/src/components/NotFoundPage.tsx new file mode 100644 index 000000000..c38405914 --- /dev/null +++ b/src/components/NotFoundPage.tsx @@ -0,0 +1,5 @@ +export const NotFoundPage = () => { + return ( +

Page not found

+ ); +}; diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index 2f1608bef..2977b9b26 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,12 +1,46 @@ +import classNames from 'classnames'; +import { useSearchParams } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; +import { CENTURIES, SEX_FILTER } from '../utils/constants'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const params = new URLSearchParams(searchParams); + const selectedCenturies = params.getAll('century') || []; + + const handleQueryChange = (e: React.ChangeEvent) => { + e.preventDefault(); + + const newQuery = e.target.value; + + if (!newQuery) { + params.delete('query'); + } else { + params.set('query', newQuery); + } + + setSearchParams(params); + }; + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx deleted file mode 100644 index 4c8713b16..000000000 --- a/src/components/PeoplePage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { PeopleFilters } from './PeopleFilters'; -import { Loader } from './Loader'; -import { PeopleTable } from './PeopleTable'; - -export const PeoplePage = () => { - return ( - <> -

People Page

- -
-
-
- -
- -
-
- - -

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 8ab5b90f7..5f960d23e 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,683 +1,117 @@ -export const PeopleTable = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import { useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; - - - - - - - - +import { Person } from '../types'; +import { TABLE_COLUMN_NAMES, TABLE_SORT_FIELDS } from '../utils/constants'; +import { getPreparedPeople } from '../utils/getPreparedPeople'; +import { PersonInfo } from './PersonInfo'; +import { SearchLink } from './SearchLink'; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +type Props = { + people: Person[]; +}; - - - - - - - - +export const PeopleTable: React.FC = ({ people }) => { + const [searchParams] = useSearchParams(); + const sort = searchParams.get('sort') || null; + const order = searchParams.get('order') || null; - - - - - - - - + const preparedPeople = getPreparedPeople(people, searchParams); - - - - - - - - + const getSortedPeople = () => { + if (!sort) { + return preparedPeople; + } - - - - - - - - + const sortedPeople = preparedPeople.sort((a, b) => { + switch (sort) { + case 'name': + case 'sex': + return a[sort].localeCompare(b[sort]); + case 'born': + case 'died': + return a[sort] - b[sort]; + default: + return 0; + } + }); - - - - - - - - + if (order) { + return sortedPeople.reverse(); + } - - - - - - - - + return sortedPeople; + }; - - - - - - - - + const getSearchParams = (columnTitle: string) => { + const currentColumnTitle = columnTitle.toLowerCase(); - - - - - - - - + if (currentColumnTitle !== sort) { + return { sort: currentColumnTitle, order: null }; + } - - - - - - - - + if (currentColumnTitle === sort && !order) { + return { sort, order: 'desc' }; + } - - - - - - - - + return { sort: null, order: null }; + }; - - - - - - - - + const peopleToRender = getSortedPeople(); - - - - - - - - - -
- - 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 ( + <> + {peopleToRender.length + ? ( + + + + {TABLE_COLUMN_NAMES.map(columnTitle => { + const newSearchParams = getSearchParams(columnTitle); + + return TABLE_SORT_FIELDS.includes(columnTitle) + ? ( + + ) : ( + + ); + })} + + + + + {peopleToRender.map(person => ( + + ))} + +
+ + {columnTitle} + + + + + + + + {columnTitle} +
+ ) : ( + 'There are no people matching the current search criteria' + )} + ); }; diff --git a/src/components/PersonInfo.tsx b/src/components/PersonInfo.tsx new file mode 100644 index 000000000..2d79e522f --- /dev/null +++ b/src/components/PersonInfo.tsx @@ -0,0 +1,60 @@ +import classNames from 'classnames'; +import { useParams } from 'react-router-dom'; + +import { Person } from '../types'; +import { EMPTY_VALUE } from '../utils/constants'; +import { PersonLink } from './PersonLink'; + +type Props = { + person: Person; +}; + +export const PersonInfo: React.FC = ({ person }) => { + const { personSlug } = useParams(); + const { + sex, + born, + died, + fatherName, + motherName, + slug, + mother, + father, + } = person; + + return ( + + + + + + {sex} + {born} + {died} + + + {mother + ? ( + + ) : ( + motherName || EMPTY_VALUE + )} + + + + {father + ? ( + + ) : ( + fatherName || EMPTY_VALUE + )} + + + ); +}; diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..edda1f446 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,28 @@ +import { Link, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import { Person } from '../types'; +import { FEMALE_SEX } from '../utils/constants'; + +type Props = { + person: Person; +}; + +export const PersonLink: React.FC = ({ person }) => { + const { + name, + sex, + slug, + } = person; + const [searchParams] = useSearchParams(); + + return ( + + {name} + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index 8cb63aeff..33520dd58 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +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'; createRoot(document.getElementById('root') as HTMLDivElement) - .render( - - - , - ); + .render(); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 000000000..0b41a51df --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,5 @@ +export const HomePage = () => { + return ( +

Home Page

+ ); +}; diff --git a/src/pages/PeoplePage.tsx b/src/pages/PeoplePage.tsx new file mode 100644 index 000000000..c9794b2fc --- /dev/null +++ b/src/pages/PeoplePage.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import { getPeople } from '../api'; +import { PeopleTable } from '../components/PeopleTable'; +import { Person } from '../types'; +import { Loader } from '../components/Loader'; +import { PeopleFilters } from '../components/PeopleFilters'; + +export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const getCurrentPeople = async () => { + setIsLoading(true); + try { + const currentPeople = await getPeople(); + + setPeople(currentPeople); + } catch { + setErrorMessage('Something went wrong'); + } + + setIsLoading(false); + }; + + useEffect(() => { + getCurrentPeople(); + }, []); + + const isPeopleTableEmpty = !people.length && !errorMessage && !isLoading; + + return ( + <> +

People Page

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

+ {errorMessage} +

+ )} + + {isPeopleTableEmpty && ( +

+ There are no people on the server +

+ )} + + {!!people.length && ( + + )} +
+
+
+
+ + ); +}; diff --git a/src/utils/constants.tsx b/src/utils/constants.tsx new file mode 100644 index 000000000..df264d33c --- /dev/null +++ b/src/utils/constants.tsx @@ -0,0 +1,33 @@ +export const TABLE_COLUMN_NAMES = [ + 'Name', + 'Sex', + 'Born', + 'Died', + 'Mother', + 'Father', +]; + +export const TABLE_SORT_FIELDS = [ + 'Name', + 'Sex', + 'Born', + 'Died', +]; + +export const CENTURIES = [ + '16', + '17', + '18', + '19', + '20', +]; + +export const SEX_FILTER = { + All: null, + Male: 'm', + Female: 'f', +}; + +export const EMPTY_VALUE = '-'; +export const FEMALE_SEX = 'f'; +export const CENTURY = 100; diff --git a/src/utils/findPersonByName.tsx b/src/utils/findPersonByName.tsx new file mode 100644 index 000000000..16479c894 --- /dev/null +++ b/src/utils/findPersonByName.tsx @@ -0,0 +1,8 @@ +import { Person } from '../types'; + +export const findPersonByName = ( + personName: string | null, + people: Person[], +) => { + return people.find(({ name }) => name === personName); +}; diff --git a/src/utils/getLinkClass.tsx b/src/utils/getLinkClass.tsx new file mode 100644 index 000000000..f5a7156cc --- /dev/null +++ b/src/utils/getLinkClass.tsx @@ -0,0 +1,6 @@ +import classNames from 'classnames'; + +export const getLinkClass = ({ isActive }: { isActive: boolean }) => classNames( + 'navbar-item', + { 'has-background-grey-lighter': isActive }, +); diff --git a/src/utils/getPreparedPeople.tsx b/src/utils/getPreparedPeople.tsx new file mode 100644 index 000000000..ab76c5bcb --- /dev/null +++ b/src/utils/getPreparedPeople.tsx @@ -0,0 +1,39 @@ +import { Person } from '../types'; +import { CENTURY } from './constants'; +import { findPersonByName } from './findPersonByName'; + +export const getPreparedPeople = ( + people: Person[], + searchParams: URLSearchParams, +) => { + const sex = searchParams.get('sex') || null; + const query = searchParams.get('query')?.trim().toLowerCase() || ''; + const centuries = searchParams.getAll('century') || []; + + const peopleWithParents = people.map(person => { + const mother = findPersonByName(person.motherName, people); + + const father = findPersonByName(person.fatherName, people); + + return { ...person, mother, father }; + }); + + if (!sex && !query && !centuries.length) { + return peopleWithParents; + } + + return peopleWithParents.filter((person) => { + const sexFilter = sex ? sex === person.sex : true; + const queryFilter = !query || ( + person.name.toLowerCase().includes(query) + || person.motherName?.toLowerCase().includes(query) + || person.fatherName?.toLowerCase().includes(query) + ); + + const bornCentury = (Math.ceil(person.born / CENTURY)).toString(); + const centuryFilter = !centuries.length + || centuries.includes(bornCentury); + + return sexFilter && queryFilter && centuryFilter; + }); +};