diff --git a/README.md b/README.md index 24519caab..fbf44c39c 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://Vayts.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..8f9ffb1be 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,8 @@ iframe { display: none; } + +.icon { + cursor: pointer; + color: blue; +} diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..3ff131cec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ -import { PeoplePage } from './components/PeoplePage'; +import { Navigate, Route, Routes } from 'react-router-dom'; import { Navbar } from './components/Navbar'; - import './App.scss'; +import { HomePage } from './pages/HomePage'; +import { NothingFound } from './pages/NotFind'; +import { PeoplePage } from './pages/PeoplePage'; export const App = () => { return ( @@ -10,9 +12,18 @@ export const App = () => {
-

Home Page

-

Page not found

- + + } /> + } /> + + } /> + } /> + + } + /> +
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c2aa20f1c..e63e1cbaf 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,11 @@ +import { NavLink } from 'react-router-dom'; +import classNames from 'classnames'; + +const linkClassName = ({ isActive }: { isActive: boolean }) => classNames( + 'navbar-item', + { 'has-background-grey-lighter': isActive }, +); + export const Navbar = () => { return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index 2f1608bef..1817808af 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,12 +1,57 @@ -export const PeopleFilters = () => { +import React from 'react'; +import classNames from 'classnames'; +import { SexFilter } from '../types/SexFilter'; +import { SearchLink } from './SearchLink'; + +type Props = { + sex: string, + query: string, + centuries: string[], + handleQuery: (query: React.ChangeEvent) => void, + handleSexFilter: (sex: SexFilter) => void, +}; + +const centuriesForFilter = ['16', '17', '18', '19', '20']; + +export const PeopleFilters: React.FC = ({ + sex, + query, + centuries, + handleQuery, + handleSexFilter, +}) => { 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..6b369fb98 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,4 +1,36 @@ -export const PeopleTable = () => { +import React from 'react'; +import cn from 'classnames'; +import { useSearchParams } from 'react-router-dom'; +import { Person } from '../types'; +import { PersonLink } from './PersonLink'; +import { SearchLink } from './SearchLink'; +import { SORT_BY_FIELDS } from '../constants/app.constants'; + +type Props = { + people: Person[], +}; + +const SORT_PARAM = 'sort'; +const ORDER_PARAM = 'order'; + +export const PeopleTable: React.FC = ({ people }) => { + const [searchParams] = useSearchParams(); + + const sort = searchParams.get(SORT_PARAM) || null; + const order = searchParams.get(ORDER_PARAM) || null; + + const getSortParams = (category: string) => { + if (sort !== category) { + return { sort: category, order: null }; + } + + if (sort === category && !order) { + return { sort: category, order: 'desc' }; + } + + return { sort: null, order: null }; + }; + return ( { > - - - - - - - + + ); + })} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => ( + + ))}
- - Name - - - - - - - - - Sex - - - + {SORT_BY_FIELDS.map(category => { + const preparedCategory = category.toLowerCase(); + + return ( + + + {category} + + + + + - - - - - Born - - - - - - - - - Died - - - - - - - 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 -
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..60d0c4414 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { useParams, Link, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import { Person } from '../types'; +import { SexFilter } from '../types/SexFilter'; +import { NOT_SET_VALUE } from '../constants/app.constants'; + +type Props = { + person: Person; +}; + +export const PersonLink: React.FC = ({ person }) => { + const { userSlug } = useParams(); + const { + name, + slug, + sex, + born, + died, + motherName, + fatherName, + } = person; + + const [searchParams] = useSearchParams(); + + return ( + + + + {name} + + + + {sex} + {born} + {died} + + {person.mother ? ( + + {motherName} + + ) : (motherName || NOT_SET_VALUE)} + + + + {person.father ? ( + + {fatherName} + + ) : (fatherName || NOT_SET_VALUE)} + + + ); +}; diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index 9b409b2f6..adf091448 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,34 +1,23 @@ import { Link, LinkProps, useSearchParams } from 'react-router-dom'; import { getSearchWith, SearchParams } from '../utils/searchHelper'; -/** - * To replace the the standard `Link` we take all it props except for `to` - * along with the custom `params` prop that we use for updating the search - */ type Props = Omit & { params: SearchParams, }; -/** - * SearchLink updates the given `params` in the search keeping the `pathname` - * and the other existing search params (see `getSearchWith`) - */ export const SearchLink: React.FC = ({ - children, // this is the content between the open and closing tags - params, // the params to be updated in the `search` - ...props // all usual Link props like `className`, `style` and `id` + children, + params, + ...props }) => { const [searchParams] = useSearchParams(); return ( {children} diff --git a/src/constants/app.constants.ts b/src/constants/app.constants.ts new file mode 100644 index 000000000..043f0c62e --- /dev/null +++ b/src/constants/app.constants.ts @@ -0,0 +1,9 @@ +export const NOT_SET_VALUE = '-'; +export const CENTURY_DIVIDER = 100; + +export const SORT_BY_FIELDS = [ + 'Name', + 'Sex', + 'Born', + 'Died', +]; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 000000000..7e0204ed6 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const HomePage: React.FC = () => { + return ( +

Home Page

+ ); +}; diff --git a/src/pages/NotFind.tsx b/src/pages/NotFind.tsx new file mode 100644 index 000000000..09be943e0 --- /dev/null +++ b/src/pages/NotFind.tsx @@ -0,0 +1,3 @@ +export const NothingFound = () => ( +

Page not found

+); diff --git a/src/pages/PeoplePage.tsx b/src/pages/PeoplePage.tsx new file mode 100644 index 000000000..1901cf884 --- /dev/null +++ b/src/pages/PeoplePage.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getFilteredPeople } from '../utils/getFilteredPeople'; +import { Order } from '../types/Order'; +import { SortType } from '../types/SortType'; +import { SexFilter } from '../types/SexFilter'; +import { Person } from '../types'; +import { getPeople } from '../api'; +import { getSearchWith } from '../utils/searchHelper'; +import { PeopleFilters } from '../components/PeopleFilters'; +import { Loader } from '../components/Loader'; +import { PeopleTable } from '../components/PeopleTable'; + +export const PeoplePage = () => { + const [initialPeople, setInitialPeople] = useState([]); + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + const query = searchParams.get('query') || ''; + const sex = searchParams.get('sex') as SexFilter; + const sort = searchParams.get('sort') as SortType; + const order = searchParams.get('order') as Order; + const centuries = searchParams.getAll('centuries') || []; + + const visiblePeople = useMemo(() => { + return getFilteredPeople(initialPeople, { + query, + sex, + sort, + order, + centuries, + }); + }, [initialPeople, searchParams, query]); + + const fetchPeople = async () => { + setIsLoading(true); + setError(false); + + try { + const peopleFormServer = await getPeople(); + const childrenWithParents = peopleFormServer.map(person => ({ + ...person, + father: peopleFormServer + .find(({ name }) => name === person.fatherName), + mother: peopleFormServer + .find(({ name }) => name === person.motherName), + })); + + setInitialPeople(childrenWithParents); + setPeople(childrenWithParents); + } catch { + setError(true); + } finally { + setIsLoading(false); + } + }; + + const handleQuery = (event: React.ChangeEvent) => { + setSearchParams( + getSearchWith(searchParams, { query: event.target.value }), + ); + }; + + const handleSexFilter = (current: string) => { + setSearchParams(getSearchWith(searchParams, { current })); + }; + + useEffect(() => { + fetchPeople(); + }, []); + + return ( + <> +

People Page

+ +
+
+
+ {!isLoading && ( + + )} +
+ +
+
+ {isLoading + ? + : ( + <> + {error && ( +

+ Something went wrong +

+ )} + + {!people.length && !error && ( +

+ There are no people on the server +

+ )} + + {!!visiblePeople.length && !error ? ( + + ) : ( +

+ There are no people matching the current search criteria +

+ )} + + )} +
+
+
+
+ + ); +}; diff --git a/src/types/FilterParams.ts b/src/types/FilterParams.ts new file mode 100644 index 000000000..4740dce32 --- /dev/null +++ b/src/types/FilterParams.ts @@ -0,0 +1,11 @@ +import { SexFilter } from './SexFilter'; +import { Order } from './Order'; +import { SortType } from './SortType'; + +export type FilterParams = { + query: string; + centuries: string[]; + sex: SexFilter; + sort: SortType; + order: Order; +}; diff --git a/src/types/Order.ts b/src/types/Order.ts new file mode 100644 index 000000000..8fa5dd534 --- /dev/null +++ b/src/types/Order.ts @@ -0,0 +1,4 @@ +export enum Order { + Asc = 'asc', + Desc = 'desc', +} diff --git a/src/types/SexFilter.ts b/src/types/SexFilter.ts new file mode 100644 index 000000000..a6544e38e --- /dev/null +++ b/src/types/SexFilter.ts @@ -0,0 +1,5 @@ +export enum SexFilter { + Female = 'f', + Male = 'm', + All = 'all', +} diff --git a/src/types/SortType.ts b/src/types/SortType.ts new file mode 100644 index 000000000..4691540aa --- /dev/null +++ b/src/types/SortType.ts @@ -0,0 +1,6 @@ +export enum SortType { + Name = 'name', + Sex = 'sex', + Born = 'born', + Died = 'died', +} diff --git a/src/utils/app.helper.ts b/src/utils/app.helper.ts new file mode 100644 index 000000000..02fde1242 --- /dev/null +++ b/src/utils/app.helper.ts @@ -0,0 +1,6 @@ +export function isStringIncludesQuery( + str: string | null, + query: string, +): boolean | null { + return str ? str.toLowerCase().includes(query) : false; +} diff --git a/src/utils/getFilteredPeople.tsx b/src/utils/getFilteredPeople.tsx new file mode 100644 index 000000000..8c1c0a918 --- /dev/null +++ b/src/utils/getFilteredPeople.tsx @@ -0,0 +1,61 @@ +import { Person } from '../types'; +import { FilterParams } from '../types/FilterParams'; +import { Order } from '../types/Order'; +import { SortType } from '../types/SortType'; +import { isStringIncludesQuery } from './app.helper'; +import { CENTURY_DIVIDER } from '../constants/app.constants'; + +export const getFilteredPeople = (people: Person[], { + query, + centuries, + sex, + sort, + order, +}: FilterParams): Person[] => { + let filteredPeople = people; + + if (query.trim()) { + const normalizedQuery = query.toLowerCase().trim(); + + filteredPeople = filteredPeople.filter( + person => isStringIncludesQuery(person.name, normalizedQuery) + || isStringIncludesQuery(person.motherName, normalizedQuery) + || isStringIncludesQuery(person.fatherName, normalizedQuery), + ); + } + + if (centuries.length) { + filteredPeople = filteredPeople.filter((person) => { + return centuries.includes( + Math.ceil(person.born / CENTURY_DIVIDER).toString(), + ); + }); + } + + if (sex) { + filteredPeople = filteredPeople.filter(person => person.sex === sex); + } + + if (sort) { + filteredPeople.sort((p1: Person, p2: Person) => { + switch (sort) { + case SortType.Name: + case SortType.Sex: + return p1[sort].localeCompare(p2[sort]); + + case SortType.Born: + case SortType.Died: + return p1[sort] - p2[sort]; + + default: + return 0; + } + }); + } + + if (order === Order.Desc) { + filteredPeople.reverse(); + } + + return filteredPeople; +};