From 441ce77f8cd07d4c2a22206b56960d1fb2637a9e Mon Sep 17 00:00:00 2001 From: Volodymyr Shepel Date: Mon, 4 Dec 2023 11:16:14 +0200 Subject: [PATCH 1/2] solution --- src/App.tsx | 14 +- src/components/HomePage.tsx | 7 + src/components/Navbar.tsx | 26 +- src/components/NotFoundPage.tsx | 7 + src/components/PeopleFilters.tsx | 154 ++++-- src/components/PeoplePage.tsx | 116 ++++- src/components/PeopleTable.tsx | 792 +++++-------------------------- src/components/PersonLink.tsx | 26 + src/types/Filter.ts | 5 + src/types/Sort.ts | 13 + src/types/index.ts | 2 + 11 files changed, 424 insertions(+), 738 deletions(-) create mode 100644 src/components/HomePage.tsx create mode 100644 src/components/NotFoundPage.tsx create mode 100644 src/components/PersonLink.tsx create mode 100644 src/types/Filter.ts create mode 100644 src/types/Sort.ts diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..b0c69d100 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,8 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; +import { HomePage } from './components/HomePage'; +import { NotFoundPage } from './components/NotFoundPage'; import './App.scss'; @@ -10,9 +13,14 @@ export const App = () => {
-

Home Page

-

Page not found

- + + } /> + } /> + + } /> + + } /> +
diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx new file mode 100644 index 000000000..7e0204ed6 --- /dev/null +++ b/src/components/HomePage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const HomePage: React.FC = () => { + return ( +

Home Page

+ ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c2aa20f1c..7e45936cc 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,15 @@ +import { NavLink, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; + +const getNavLinkClass = ({ isActive }: { isActive: boolean }) => { + return classNames('navbar-item', { + 'has-background-grey-lighter': isActive, + }); +}; + export const Navbar = () => { + const [searchParams] = useSearchParams(); + return ( diff --git a/src/components/NotFoundPage.tsx b/src/components/NotFoundPage.tsx new file mode 100644 index 000000000..14b2b5e26 --- /dev/null +++ b/src/components/NotFoundPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const NotFoundPage: React.FC = () => { + return ( +

Page not found

+ ); +}; diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index 2f1608bef..54e323c9b 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,12 +1,90 @@ +import { Link, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import React, { useEffect, useState } from 'react'; +import { FilterParams } from '../types'; + +const filterCenturies = [16, 17, 18, 19, 20]; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const centuries = searchParams.getAll(FilterParams.CENTURIES) || []; + const defaultQuery = searchParams.get(FilterParams.NAME) || ''; + const [query, setQuery] = useState(defaultQuery); + + const getParams = (key: FilterParams, value = '') => { + const params = new URLSearchParams(searchParams); + + if (value.trim()) { + if (key === FilterParams.CENTURIES) { + const newCenturies = centuries.includes(value) + ? centuries.filter(century => century !== value) + : [...centuries, value]; + + params.delete(FilterParams.CENTURIES); + + newCenturies.forEach(century => { + params.append(FilterParams.CENTURIES, century); + }); + } else { + params.set(key, value.trim()); + } + } else { + params.delete(key); + } + + return params.toString(); + }; + + const getResetParams = () => { + const params = new URLSearchParams(searchParams); + + params.delete(FilterParams.NAME); + params.delete(FilterParams.SEX); + params.delete(FilterParams.CENTURIES); + + return params.toString(); + }; + + const handleChangeQuery = (e: React.ChangeEvent) => { + setQuery(e.currentTarget.value); + setSearchParams(getParams(FilterParams.NAME, e.currentTarget.value)); + }; + + useEffect(() => { + if (!defaultQuery) { + setQuery(''); + } + }, [defaultQuery]); + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index 4c8713b16..2c2aece89 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,31 +1,125 @@ +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { + FilterParams, ORDER_PARAM, OrderType, Person, SORT_PARAM, SortFields, +} from '../types'; +import { getPeople } from '../api'; + +export const PeoplePage: React.FC = () => { + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const [people, setPeople] = useState([]); + const [searchParams] = useSearchParams(); + + const filteredPeople = () => { + const sort = searchParams.get(SORT_PARAM); + const isDesc = searchParams.get(ORDER_PARAM) === OrderType.DESC; + const sex = searchParams.get(FilterParams.SEX); + const nameQuery = searchParams.get(FilterParams.NAME); + const centuries = searchParams.getAll(FilterParams.CENTURIES) || []; + let filtered = [...people]; + + if (sort) { + switch (sort) { + case SortFields.NAME || SortFields.SEX: + filtered.sort((a, b) => a[sort].localeCompare(b[sort])); + break; + case SortFields.BORN || SortFields.DIED: + filtered.sort((a, b) => a[sort] - b[sort]); + break; + default: + break; + } + + if (isDesc) { + filtered.reverse(); + } + } + + if (sex) { + filtered = filtered.filter(person => person.sex === sex); + } + + if (nameQuery) { + filtered = filtered.filter(person => { + return person.name.includes(nameQuery) + || person.motherName?.includes(nameQuery) + || person.fatherName?.includes(nameQuery); + }); + } + + if (centuries.length) { + filtered = filtered.filter(person => { + return centuries.includes( + String(Math.ceil(person.born / 100)), + ); + }); + } + + return filtered; + }; + + const preparedPeople = filteredPeople(); + + const isNotFetching = !isLoading && !hasError; + + useEffect(() => { + setIsLoading(true); + + getPeople() + .then(data => { + const mappedData = data.map(person => ({ + ...person, + mother: data.find(mother => mother.name === person.motherName), + father: data.find(father => father.name === person.fatherName), + })); + + setPeople(mappedData); + }) + .catch(() => { + setHasError(true); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); -export const PeoplePage = () => { return ( <>

People Page

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

Something went wrong

+ {!isLoading && hasError && ( +

Something went wrong

+ )} -

- There are no people on the server -

+ {isNotFetching && !people.length && ( +

+ There are no people on the server +

+ )} -

There are no people matching the current search criteria

+ {isNotFetching && !!people.length && !preparedPeople.length && ( +

There are no people matching the current search criteria

+ )} - + {isNotFetching && !!preparedPeople.length && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index b8275e8e0..bde621823 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,77 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import React from 'react'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import { + ORDER_PARAM, SORT_PARAM, Person, SortFields, OrderType, +} from '../types'; +import { PersonLink } from './PersonLink'; + +type Props = { + people: Person[]; +}; + +type Column = { + slug: string, + title: string, + sort?: SortFields, +}; + +const columns: Column[] = [ + { + slug: 'name', + title: 'Name', + sort: SortFields.NAME, + }, + { + slug: 'sex', + title: 'Sex', + sort: SortFields.SEX, + }, + { + slug: 'born', + title: 'Born', + sort: SortFields.BORN, + }, + { + slug: 'died', + title: 'Died', + sort: SortFields.DIED, + }, + { + slug: 'mother', + title: 'Mother', + }, + { + slug: 'father', + title: 'Father', + }, +]; + +export const PeopleTable: React.FC = ({ people }) => { + const { personSlug } = useParams(); + const [searchParams] = useSearchParams(); + const sort = searchParams.get(SORT_PARAM); + const order = searchParams.get(ORDER_PARAM); + const hasOrderDesc = order === OrderType.DESC; + + const getParamsWithSort = (sortValue: SortFields) => { + const params = new URLSearchParams(searchParams); + + if (params.get(SORT_PARAM) === sortValue) { + if (params.get(ORDER_PARAM) === OrderType.DESC) { + params.delete(SORT_PARAM); + params.delete(ORDER_PARAM); + } else { + params.set(ORDER_PARAM, OrderType.DESC); + } + } else { + params.set(SORT_PARAM, sortValue); + params.delete(ORDER_PARAM); + } + + return params.toString(); + }; + return ( { > - - - - - - - - - - + ) : col.title} + + ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => ( + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + + + + + + ))}
- - Name - - - - - - - - - Sex - - - - - - - - - Born - - - - - - - - - Died - - - + {columns.map(col => ( + + {col.sort ? ( + + {col.title} + + + + + - - - 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 -
+ + {person.sex}{person.born}{person.died} + {person.mother ? ( + + ) : person.motherName || '-'} + + {person.father ? ( + + ) : person.fatherName || '-'} +
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..fa2e11571 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import { Person } from '../types'; + +type Props = { + person: Person; +}; + +export const PersonLink: React.FC = ({ person }) => { + const [searchParams] = useSearchParams(); + + return ( + + {person.name} + + ); +}; diff --git a/src/types/Filter.ts b/src/types/Filter.ts new file mode 100644 index 000000000..21aac7d2d --- /dev/null +++ b/src/types/Filter.ts @@ -0,0 +1,5 @@ +export enum FilterParams { + NAME = 'name', + SEX = 'sex', + CENTURIES = 'centuries', +} diff --git a/src/types/Sort.ts b/src/types/Sort.ts new file mode 100644 index 000000000..c0c932561 --- /dev/null +++ b/src/types/Sort.ts @@ -0,0 +1,13 @@ +export const SORT_PARAM = 'sort'; +export const ORDER_PARAM = 'order'; + +export enum SortFields { + NAME = 'name', + SEX = 'sex', + BORN = 'born', + DIED = 'died', +} + +export enum OrderType { + DESC = 'desc', +} diff --git a/src/types/index.ts b/src/types/index.ts index 1cbfa3601..31ba71d9d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1 +1,3 @@ export * from './Person'; +export * from './Sort'; +export * from './Filter'; From 992d3bfa03d038fa5a32e4904381e7a44ec95609 Mon Sep 17 00:00:00 2001 From: Volodymyr Shepel Date: Mon, 4 Dec 2023 11:26:16 +0200 Subject: [PATCH 2/2] fixed sorting --- src/components/PeoplePage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index 2c2aece89..aec911215 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -24,10 +24,12 @@ export const PeoplePage: React.FC = () => { if (sort) { switch (sort) { - case SortFields.NAME || SortFields.SEX: + case SortFields.NAME: + case SortFields.SEX: filtered.sort((a, b) => a[sort].localeCompare(b[sort])); break; - case SortFields.BORN || SortFields.DIED: + case SortFields.BORN: + case SortFields.DIED: filtered.sort((a, b) => a[sort] - b[sort]); break; default: