diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..1e6e96031 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ -import { PeoplePage } from './components/PeoplePage'; +import { Outlet } from 'react-router-dom'; import { Navbar } from './components/Navbar'; - import './App.scss'; export const App = () => { @@ -10,9 +9,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..424e63b1b --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,25 @@ +import { + HashRouter as Router, + Routes, + Route, + Navigate, +} from 'react-router-dom'; +import { App } from './App'; +import { HomePage } from './pages/HomePage'; +import { PeoplePage } from './pages/PeoplePage'; +import { ErrorPage } from './pages/ErrorPage'; + +export const Root = () => ( + + + }> + } /> + } /> + }> + } /> + + } /> + + + +); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c2aa20f1c..c51e097e4 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,12 @@ +import { NavLink, useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; + export const Navbar = () => { + const [searchParams] = useSearchParams(); + + const styleLink = ({ isActive }: { isActive: boolean }) => cn('navbar-item', + { 'has-background-grey-lighter': isActive }); + return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index 2f1608bef..d89f4a243 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,12 +1,39 @@ -export const PeopleFilters = () => { +import cn from 'classnames'; +import { Link, useSearchParams } from 'react-router-dom'; +import { filterOptions } from '../types/filterBySex'; +import { CenturyFilter } from '../types/CenturyFilter'; +import { SearchParams, getSearchWith } from '../utils/searchHelper'; +import { SearchLink } from './SearchLink'; + +type Props = { + setSearchWith: (params: SearchParams) => void; +}; + +export const PeopleFilters: React.FC = ({ setSearchWith }) => { + const [searchParams] = useSearchParams(); + + const query = searchParams.get('query') || ''; + const filterBySex = searchParams.get('sex') || 'all'; + const filterByCentury = searchParams.getAll('centuries') || []; + + const handleQueryChange = (event: React.ChangeEvent) => { + setSearchWith({ query: event.target.value || null }); + }; + 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..f5a2f826f 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,4 +1,41 @@ -export const PeopleTable = () => { +import React from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { PersonInfo } from './PersonInfo'; +import { Person } from '../types/Person'; +import { SearchLink } from './SearchLink'; +import { SearchParams } from '../utils/searchHelper'; + +type Props = { + people: Person[]; +}; + +export const PeopleTable: React.FC = ({ people }) => { + const { personId } = useParams(); + const [searchParams] = useSearchParams(); + + const sort = searchParams.get('sort') || null; + const order = searchParams.get('order') || null; + + const getSortParams = (sortBy: string) => { + if (!sort || sort !== sortBy) { + return { + sort: sortBy, + }; + } + + if (sort === sortBy && !order) { + return { + sort: sortBy, + order: 'desc', + }; + } + + return { + sort: null, + order: null, + }; + }; + return ( { @@ -56,627 +95,13 @@ export const PeopleTable = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => ( + + ))}
Name - + - + Sex - + - + Born - + - + Died - + - +
- 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/PersonInfo.tsx b/src/components/PersonInfo.tsx new file mode 100644 index 000000000..f7028e7ea --- /dev/null +++ b/src/components/PersonInfo.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import cn from 'classnames'; +import { Person } from '../types'; +import { PersonLink } from './PersonLink'; + +type Props = { + person: Person; + personId: string | undefined; +}; + +export const PersonInfo: React.FC = ({ + person, + personId, +}) => { + return ( + + + + + + {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..f6f78d11d --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { Person } from '../types'; + +type Props = { + person: Person; +}; + +export const PersonLink:React.FC = ({ person }) => { + const [searchParams] = useSearchParams(); + + return ( + + {person.name} + + ); +}; diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index 9b409b2f6..b2333bea4 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/index.tsx b/src/index.tsx index 8cb63aeff..96070ba57 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,10 @@ import { createRoot } from 'react-dom/client'; -import { HashRouter as Router } from 'react-router-dom'; +import { Root } from './Root'; import 'bulma/css/bulma.css'; import '@fortawesome/fontawesome-free/css/all.css'; -import { App } from './App'; - createRoot(document.getElementById('root') as HTMLDivElement) .render( - - - , + , ); diff --git a/src/pages/ErrorPage.tsx b/src/pages/ErrorPage.tsx new file mode 100644 index 000000000..f9927803f --- /dev/null +++ b/src/pages/ErrorPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const ErrorPage: React.FC = () => { + return ( +

Page not found

+ ); +}; 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/PeoplePage.tsx b/src/pages/PeoplePage.tsx new file mode 100644 index 000000000..4e0abdc15 --- /dev/null +++ b/src/pages/PeoplePage.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getPeople } from '../api'; +import { Person } from '../types'; +import { preparePeopleData } from '../utils/prepareData'; +import { SearchParams, getSearchWith } from '../utils/searchHelper'; +import { Loader } from '../components/Loader'; +import { PeopleFilters } from '../components/PeopleFilters'; +import { PeopleTable } from '../components/PeopleTable'; + +export const PeoplePage: React.FC = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const [people, setPeople] = useState([]); + const [peopleLoading, setPeopleLoading] = useState(false); + const [isLoadingError, setIsLoadingError] = useState(false); + + const query = searchParams.get('query') || ''; + const filterBySex = searchParams.get('sex') || 'all'; + const filterByCentury = searchParams.getAll('centuries') || []; + const sort = searchParams.get('sort') || null; + const order = searchParams.get('order') || null; + + const isDataAvailable = !peopleLoading && people.length > 0; + const isArrayEmpty = !peopleLoading && people.length === 0; + + useEffect(() => { + setIsLoadingError(false); + setPeopleLoading(true); + + getPeople() + .then((response) => { + const preparedData = preparePeopleData(response); + + setPeople(preparedData); + }) + .catch(() => setIsLoadingError(true)) + .finally(() => setPeopleLoading(false)); + }, []); + + const setSearchWith = (params: SearchParams) => { + const search = getSearchWith(params, searchParams); + + setSearchParams(search); + }; + + const getPeopleToRender = (allPeople: Person[]) => { + let filteredPeople: Person[] = allPeople; + + if (query) { + const lowerQuery = query.toLowerCase(); + + filteredPeople = filteredPeople.filter(person => ( + person.name.toLowerCase().includes(lowerQuery) + || person.motherName?.toLowerCase().includes(lowerQuery) + || person.fatherName?.toLowerCase().includes(lowerQuery) + )); + } + + if (filterBySex !== 'all') { + filteredPeople = filteredPeople + .filter(person => person.sex === filterBySex); + } + + if (filterByCentury.length) { + filteredPeople = filteredPeople + .filter(person => filterByCentury + .includes(Math.ceil(person.born / 100).toString())); + } + + if (sort) { + switch (sort) { + case 'name': + case 'sex': + return filteredPeople.sort((a, b) => a[sort].localeCompare(b[sort])); + case 'born': + case 'died': + return filteredPeople.sort((a, b) => a[sort] - b[sort]); + default: + return filteredPeople; + } + } + + if (order) { + filteredPeople.reverse(); + } + + return filteredPeople; + }; + + let visiblePeople = getPeopleToRender(people); + + useEffect(() => { + visiblePeople = getPeopleToRender(visiblePeople); + }, [searchParams]); + + return ( + <> +

People Page

+ +
+
+
+ {isDataAvailable && ( + + )} +
+ +
+
+ {peopleLoading && } + + {isLoadingError && ( +

+ Something went wrong +

+ )} + + {isDataAvailable && } + + {isArrayEmpty && ( +

+ There are no people on the server +

+ )} +
+
+
+
+ + ); +}; diff --git a/src/types/CenturyFilter.ts b/src/types/CenturyFilter.ts new file mode 100644 index 000000000..7d0a8033e --- /dev/null +++ b/src/types/CenturyFilter.ts @@ -0,0 +1 @@ +export const CenturyFilter = ['16', '17', '18', '19', '20']; diff --git a/src/types/Person.ts b/src/types/Person.ts index 952873d1d..0a58ced99 100644 --- a/src/types/Person.ts +++ b/src/types/Person.ts @@ -6,6 +6,6 @@ export interface Person { fatherName: string | null, motherName: string | null, slug: string, - mother?: Person, - father?: Person, + mother?: Person | null, + father?: Person | null, } diff --git a/src/types/filterBySex.ts b/src/types/filterBySex.ts new file mode 100644 index 000000000..11bb7df04 --- /dev/null +++ b/src/types/filterBySex.ts @@ -0,0 +1,5 @@ +export const filterOptions = [ + { title: 'All', value: null }, + { title: 'Male', value: 'm' }, + { title: 'Female', value: 'f' }, +]; diff --git a/src/utils/prepareData.ts b/src/utils/prepareData.ts new file mode 100644 index 000000000..5d98f7e3d --- /dev/null +++ b/src/utils/prepareData.ts @@ -0,0 +1,19 @@ +import { Person } from '../types'; + +export const preparePeopleData = (peopleFromServer : Person[]) => { + const findMother = (person: Person) => { + return peopleFromServer.find(m => m.name === person.motherName) || null; + }; + + const findFather = (person: Person) => { + return peopleFromServer.find(f => f.name === person.fatherName) || null; + }; + + const result = peopleFromServer.map(person => ({ + ...person, + mother: findMother(person), + father: findFather(person), + })); + + return result; +}; diff --git a/src/utils/searchHelper.ts b/src/utils/searchHelper.ts index 42d8db56d..680424299 100644 --- a/src/utils/searchHelper.ts +++ b/src/utils/searchHelper.ts @@ -2,36 +2,19 @@ export type SearchParams = { [key: string]: string | string[] | null, }; -/** - * This function prepares a correct search string - * from a given currentParams and paramsToUpdate. - */ export function getSearchWith( + paramsToUpdate: SearchParams, currentParams: URLSearchParams, - paramsToUpdate: SearchParams, // it's our custom type ): string { - // copy currentParams by creating new object from a string const newParams = new URLSearchParams( currentParams.toString(), ); - // Here is the example of paramsToUpdate - // { - // sex: 'm', ['sex', 'm'] - // order: null, ['order', null] - // centuries: ['16', '19'], ['centuries', ['16', '19']] - // } - // - // - params with the `null` value are deleted; - // - string value is set to given param key; - // - array of strings adds several params with the same key; - Object.entries(paramsToUpdate) .forEach(([key, value]) => { if (value === null) { newParams.delete(key); } else if (Array.isArray(value)) { - // we delete the key to remove old values newParams.delete(key); value.forEach(part => { @@ -42,6 +25,5 @@ export function getSearchWith( } }); - // we return a string to use it inside links return newParams.toString(); }