diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..3419b8904 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; @@ -10,9 +11,15 @@ 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..8c8bcdb3b 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,10 @@ +import { Link, NavLink, useMatch, useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; + export const Navbar = () => { + const [searchParams] = useSearchParams(); + const isPeoplePage = useMatch('/people/*'); + return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..79cbb4130 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,53 @@ +import { useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; +import { SearchLink } from './SearchLink'; +import { getSearchWith } from '../utils/searchHelper'; + +const CENTURIES = ['16', '17', '18', '19', '20']; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const sex = searchParams.get('sex'); + const query = searchParams.get('query') || ''; + const centuries = searchParams.getAll('centuries'); + + const handleQueryChange = (e: React.ChangeEvent) => { + const value = e.target.value; + + setSearchParams(getSearchWith(searchParams, { query: value || null })); + }; + + const toggleCentury = (century: string): string[] => { + if (centuries.includes(century)) { + return centuries.filter(c => c !== century); + } + + return [...centuries, century]; + }; + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..3fd8c266f 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,29 +1,125 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { getPeople } from '../api'; +import { Person } from '../types'; export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [searchParams] = useSearchParams(); + + useEffect(() => { + setLoading(true); + setError(false); + + getPeople() + .then(data => { + const peopleWithParents = data.map(person => ({ + ...person, + mother: data.find(p => p.name === person.motherName), + father: data.find(p => p.name === person.fatherName), + })); + + setPeople(peopleWithParents); + }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); + + const sex = searchParams.get('sex'); + const query = searchParams.get('query') || ''; + const centuries = searchParams.getAll('centuries'); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const filteredPeople = useMemo(() => { + let result = [...people]; + + if (sex) { + result = result.filter(p => p.sex === sex); + } + + if (query) { + const q = query.toLowerCase(); + + result = result.filter( + p => + p.name.toLowerCase().includes(q) || + (p.motherName || '').toLowerCase().includes(q) || + (p.fatherName || '').toLowerCase().includes(q), + ); + } + + if (centuries.length > 0) { + result = result.filter(p => + centuries.includes(String(Math.ceil(p.born / 100))), + ); + } + + if (sort) { + result.sort((a, b) => { + let cmp = 0; + + switch (sort) { + case 'name': + case 'sex': + cmp = a[sort].localeCompare(b[sort]); + break; + case 'born': + case 'died': + cmp = a[sort] - b[sort]; + break; + } + + return order === 'desc' ? -cmp : cmp; + }); + } + + return result; + }, [people, sex, query, centuries, sort, order]); + + const loaded = !loading && !error; + const hasPeople = loaded && people.length > 0; + const noPeople = loaded && people.length === 0; + const noMatches = hasPeople && filteredPeople.length === 0; + return ( <>

People Page

-
- -
+ {hasPeople && ( +
+ +
+ )}
- + {loading && } -

Something went wrong

+ {error && ( +

Something went wrong

+ )} -

There are no people on the server

+ {noPeople && ( +

+ There are no people on the server +

+ )} -

There are no people matching the current search criteria

+ {noMatches && ( +

There are no people matching the current search criteria

+ )} - + {hasPeople && !noMatches && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..fe871c47d 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,644 +1,112 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import { Link, useParams, useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; +import { Person } from '../types'; +import { SearchLink } from './SearchLink'; - - - - - - - - +type Props = { + people: Person[]; +}; - - - - - - - - +type SortField = 'name' | 'sex' | 'born' | 'died'; - - - - - - - - +const SORT_FIELDS: SortField[] = ['name', 'sex', 'born', 'died']; - - - - - - - - +export const PeopleTable: React.FC = ({ people }) => { + const { slug } = useParams(); + const [searchParams] = useSearchParams(); + const sort = searchParams.get('sort') as SortField | null; + const order = searchParams.get('order'); - - - - - - - - + const getSortParams = (field: SortField) => { + if (sort !== field) { + return { sort: field, order: null }; + } - - - - - - - - + if (order !== 'desc') { + return { sort: field, order: 'desc' }; + } - - - - - - - - + return { sort: null, order: null }; + }; - - - - - - - - + const getSortIcon = (field: SortField) => { + if (sort !== field) { + return 'fas fa-sort'; + } - - - - - - - - + return order === 'desc' ? 'fas fa-sort-down' : 'fas fa-sort-up'; + }; - - - - - - - - + const renderParent = (person: Person, type: 'mother' | 'father') => { + const parentName = + type === 'mother' ? person.motherName : person.fatherName; + const parent = type === 'mother' ? person.mother : person.father; - - - - - - - - + if (!parentName) { + return '-'; + } - - - - - - - - + if (!parent) { + return parentName; + } - - - - - - - - + return ( + + {parent.name} + + ); + }; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + return ( +
- - 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
+ + + {SORT_FIELDS.map(field => ( + + ))} + + + - - - - - - - - + + {people.map(person => ( + + + + + + + + + ))}
+ + {field.charAt(0).toUpperCase() + field.slice(1)} + + + + + + + MotherFather
- Emile Haverbeke - m18771968 - - Maria Sturm - - - Carolus Haverbeke -
+ + {person.name} + + {person.sex}{person.born}{person.died}{renderParent(person, 'mother')}{renderParent(person, 'father')}
);