diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..d074a8deb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; @@ -10,9 +11,19 @@ export const App = () => {
-

Home Page

-

Page not found

- + + Home Page} /> + } /> + + } /> + } /> + + + Page not found} + /> +
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..50a9dc067 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,8 @@ +import { NavLink, useLocation } from 'react-router-dom'; + export const Navbar = () => { + const { search } = useLocation(); + return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..b6b5c1767 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,36 @@ +import { useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; +import { SearchLink } from './SearchLink'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const query = searchParams.get('query') || ''; + const sex = searchParams.get('sex') || ''; + const selectedCenturies = searchParams.getAll('centuries'); + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..42c901900 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,29 +1,138 @@ +/* eslint-disable max-len */ +/* eslint-disable prettier/prettier */ import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { useSearchParams } from 'react-router-dom'; +import { useEffect, useMemo, useState } from 'react'; +import { getPeople } from '../api'; +import { Person } from '../types'; export const PeoplePage = () => { + const [searchParams] = useSearchParams(); + const [people, setPeople] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const query = searchParams.get('query')?.toLowerCase() || ''; + const sex = searchParams.get('sex') || ''; + const centuries = searchParams.getAll('centuries'); + const sortBy = searchParams.get('sort') || ''; + const order = searchParams.get('order') || ''; + + useEffect(() => { + setLoading(true); + setError(false); + getPeople() + .then(data => { + setPeople(data); + setLoading(false); + }) + .catch(() => { + setError(true); + setLoading(false); + }); + }, []); + + const visiblePeople = useMemo(() => { + let filtered = [...people]; + + if (sex) { + filtered = filtered.filter(person => person.sex === sex); + } + + if (query) { + filtered = filtered.filter(person => { + const matchesName = person.name.toLowerCase().includes(query); + + const matchesMother = person.motherName?.toLowerCase().includes(query); + + const matchesFather = person.fatherName?.toLowerCase().includes(query); + + return matchesName || matchesMother || matchesFather; + }); + } + + if (centuries.length > 0) { + filtered = filtered.filter(person => { + const century = Math.ceil(person.born / 100); + + return centuries.includes(century.toString()); + }); + } + + if (sortBy) { + filtered.sort((a, b) => { + const valA = a[sortBy as keyof Person]; + const valB = b[sortBy as keyof Person]; + + let result = 0; + + if (typeof valA === 'number' && typeof valB === 'number') { + result = valA - valB; + } else { + result = String(valA).localeCompare(String(valB)); + } + + return order === 'desc' ? -result : result; + }); + } + + return filtered; + }, [people, sex, query, centuries, sortBy, order]); + + const getSortParams = (field: string) => { + if (sortBy !== field) { + return { sort: field, order: null }; + } + + if (order !== 'desc') { + return { sort: field, order: 'desc' }; + } + + return { sort: null, order: null }; + }; + return ( <>

People Page

-
- -
+ {!loading && !error && people.length > 0 && ( +
+ +
+ )}
- + {loading && } -

Something went wrong

+ {!loading && error && ( +

Something went wrong

+ )} -

There are no people on the server

+ {!loading && !error && people.length === 0 && ( +

There are no people on the server

+ )} -

There are no people matching the current search criteria

+ {!loading && + !error && + people.length > 0 && + visiblePeople.length === 0 && ( +

+ There are no people matching the current search criteria{' '} +

+ )} - + {!loading && !error && visiblePeople.length > 0 && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..66aaed659 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,644 +1,135 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import React from 'react'; +import { Link, useSearchParams, useParams } from 'react-router-dom'; +import cn from 'classnames'; +import { Person } from '../types'; +import { SearchLink } from './SearchLink'; + +type Props = { + people: Person[]; + sortBy: string; + order: string; + getSortParams: (field: string) => { + sort: string | null; + order: string | null; + }; +}; + +export const PeopleTable: React.FC = ({ + people, + sortBy, + order, + getSortParams, +}) => { + const [searchParams] = useSearchParams(); + const { personSlug } = useParams(); + + const findPersonByName = (name: string) => people.find(p => p.name === name); + + const renderSortableHeader = (field: string, label: string) => ( + + + {label} + + + + + + ); + return ( - - - - - - - - + {renderSortableHeader('name', 'Name')} + {renderSortableHeader('sex', 'Sex')} + {renderSortableHeader('born', 'Born')} + {renderSortableHeader('died', 'Died')} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => { + const isSelected = person.slug === personSlug; + const mother = findPersonByName(person.motherName || ''); + const father = findPersonByName(person.fatherName || ''); + + return ( + + + + + + + + + ); + })}
- - Name - - - - - - - - - Sex - - - - - - - - - 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 -
+ + {person.name} + + {person.sex}{person.born}{person.died} + {person.motherName && + (mother ? ( + + {person.motherName} + + ) : ( + {person.motherName} + ))} + {!person.motherName && ( + - + )} + + {person.fatherName && + (father ? ( + + {person.fatherName} + + ) : ( + {person.fatherName} + ))} + {!person.fatherName && ( + - + )} +
); diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..44dfa8531 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,34 +1,21 @@ +import React from 'react'; 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` -}) => { +export const SearchLink: React.FC = ({ children, params, ...props }) => { const [searchParams] = useSearchParams(); return ( {children}