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..cb88053c0 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,23 @@ +import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; +import { App } from './App'; +import { HomePage } from './components/HomePage'; +import { PeoplePage } from './components/PeoplePage'; +import { NotFoundPage } from './components/NotFoundPage'; + +export const Root = () => { + return ( + + + }> + } /> + + } /> + } /> + + } /> + } /> + + + + ); +}; diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx new file mode 100644 index 000000000..551de3626 --- /dev/null +++ b/src/components/HomePage.tsx @@ -0,0 +1,5 @@ +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 3f63898b2..57c6df376 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,12 @@ +import { Link, useLocation } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; + export const Navbar = () => { + const location = useLocation(); + + const isHomeActive = location.pathname === '/'; + const isPeopleActive = location.pathname.startsWith('/people'); + return ( diff --git a/src/components/NotFoundPage.tsx b/src/components/NotFoundPage.tsx new file mode 100644 index 000000000..b3a3366fc --- /dev/null +++ b/src/components/NotFoundPage.tsx @@ -0,0 +1,5 @@ +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 c9c819cd3..4af462ac5 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,77 @@ +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const currentSex = searchParams.get('sex'); + const query = searchParams.get('query') || ''; + + const selectedCenturies = searchParams.getAll('centuries'); + + const centuriesList = ['16', '17', '18', '19', '20']; + + //Інпут для пошуку по імені// + + const handleSearchChange = (event: React.ChangeEvent) => { + const text = event.target.value; + const newParams = new URLSearchParams(searchParams); + + if (text.trim()) { + newParams.set('query', text); + } else { + newParams.delete('query'); + } + + setSearchParams(newParams); + }; + + // Фільтр за статтю // + + const getUpdatedCenturies = (targetCentury: string) => { + const currentParams = new URLSearchParams(searchParams); + const activeCenturies = currentParams.getAll('centuries'); + + const nextCenturies = activeCenturies.includes(targetCentury) + ? activeCenturies.filter(century => century !== targetCentury) + : [...activeCenturies, targetCentury]; + + currentParams.delete('centuries'); + + nextCenturies.forEach(century => { + currentParams.append('centuries', century); + }); + + return currentParams.getAll('centuries').length > 0 + ? currentParams.getAll('centuries') + : null; + }; + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..ffd20e953 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,29 +1,145 @@ import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { Person } from '../types'; +import { useState, useEffect, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getPeople } from '../api'; export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(false); + + const [searchParams] = useSearchParams(); + const currentSex = searchParams.get('sex'); + const query = searchParams.get('query') || ''; + const selectedCenturies = searchParams.getAll('centuries'); + + const currentSort = searchParams.get('sort'); + const currentOrder = searchParams.get('order'); + + useEffect(() => { + setIsLoading(true); + setError(false); + getPeople() + .then(fetchedPeople => { + const peopleWithRelations = fetchedPeople.map(person => { + const mother = person.motherName + ? fetchedPeople.find(human => human.name === person.motherName) + : undefined; + const father = person.fatherName + ? fetchedPeople.find(human => human.name === person.fatherName) + : undefined; + + return { + ...person, + mother, + father, + }; + }); + + setPeople(peopleWithRelations); + }) + .catch(() => { + setError(true); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + const filteredPeople = useMemo(() => { + return people.filter(person => { + if (currentSex && person.sex !== currentSex) { + return false; + } + + if (query.trim()) { + const normalizedQuery = query.toLowerCase().trim(); + const matchesName = person.name.toLowerCase().includes(normalizedQuery); + const matchesMother = person.motherName + ?.toLowerCase() + .includes(normalizedQuery); + const matchesFather = person.fatherName + ?.toLowerCase() + .includes(normalizedQuery); + + if (!matchesName && !matchesMother && !matchesFather) { + return false; + } + } + + if (selectedCenturies.length > 0) { + const personCentury = Math.ceil(person.born / 100).toString(); + + if (!selectedCenturies.includes(personCentury)) { + return false; + } + } + + return true; + }); + }, [people, currentSex, query, selectedCenturies]); + + const sortedPeople = useMemo(() => { + return [...filteredPeople].sort((firstPerson, secondPerson) => { + if (!currentSort) { + return 0; + } + + const firstValue = firstPerson[currentSort as keyof Person] ?? ''; + const secondValue = secondPerson[currentSort as keyof Person] ?? ''; + + if (currentOrder === 'desc') { + return typeof secondValue === 'number' && typeof firstValue === 'number' + ? secondValue - firstValue + : secondValue.toString().localeCompare(firstValue.toString()); + } + + return typeof firstValue === 'number' && typeof secondValue === 'number' + ? firstValue - secondValue + : firstValue.toString().localeCompare(secondValue.toString()); + }); + }, [filteredPeople, currentSort, currentOrder]); + + const isDataLoaded = !isLoading && !error && people.length > 0; + return ( <>

People Page

-
- -
+ {isDataLoaded && ( +
+ +
+ )}
- + {isLoading && } -

Something went wrong

+ {!isLoading && error && ( +

Something went wrong

+ )} -

There are no people on the server

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

+ There are no people on the server +

+ )} -

There are no people matching the current search criteria

+ {isDataLoaded && sortedPeople.length === 0 && ( +

+ There are no people matching the current search criteria +

+ )} - + {isDataLoaded && sortedPeople.length > 0 && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..b4341e60a 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,41 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import React from 'react'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; +import { Person } from '../types'; + +interface Props { + people: Person[]; +} + +export const PeopleTable: React.FC = ({ people }) => { + const { slug } = useParams(); + + const [searchParams] = useSearchParams(); + const currentSort = searchParams.get('sort'); + const currentOrder = searchParams.get('order'); + + // Реалізація логіки 3-х кліків (asc -> desc -> вимкнено) + const getSortParams = (columnName: string) => { + if (currentSort !== columnName) { + return { sort: columnName, order: null }; // перший клік: порядок за замовчуванням (asc) + } + + if (currentOrder !== 'desc') { + return { sort: columnName, order: 'desc' }; // другий клік: desc + } + + return { sort: null, order: null }; // третій клік: вимкнути сортування + }; + + const getSortIcon = (columnName: string) => { + if (currentSort !== columnName) { + return 'fas fa-sort'; + } + + return currentOrder === 'desc' ? 'fas fa-sort-down' : 'fas fa-sort-up'; + }; + return ( { @@ -57,588 +93,66 @@ export const PeopleTable = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => { + const isSelected = person.slug === slug; + + return ( + + + + + + + + + {/* Стовпчик Father */} + + + ); + })}
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 -
+ + {person.name} + + {person.sex}{person.born}{person.died} + {person.motherName && person.mother?.slug ? ( + + {person.motherName} + + ) : ( + person.motherName || '-' + )} + + {person.fatherName && person.father?.slug ? ( + + {person.fatherName} + + ) : ( + person.fatherName || '-' + )} +
); diff --git a/src/index.tsx b/src/index.tsx index d72ba5730..b23e74d0f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,7 @@ 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( - - - , -); +createRoot(document.getElementById('root') as HTMLDivElement).render();