diff --git a/README.md b/README.md index 064a39440..74850a60c 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ implement the ability to filter and sort people in the table. - Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). - Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). - Open one more terminal and run tests with `npm test` to ensure your solution is correct. -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_people-table-advanced/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://opalahecha.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..98de16e23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ -import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { Outlet } from 'react-router-dom'; +import React from 'react'; export const App = () => { return ( @@ -10,9 +11,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..c3c0e1687 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,26 @@ +import { + HashRouter as Router, + Routes, + Route, + Navigate, +} from 'react-router-dom'; +import { PeoplePage } from './components/PeoplePage'; +import { App } from './App'; +import React from 'react'; + +export const Root = () => { + return ( + + + }> + Home Page} /> + } /> + }> + } /> + + Page not found} /> + + + + ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..b1b321736 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,10 @@ +import { NavLink } from 'react-router-dom'; +import cn from 'classnames'; +import React from 'react'; + +const getNavActive = ({ isActive }: { isActive: boolean }) => + cn('navbar-item', { 'has-background-grey-lighter': isActive }); + export const Navbar = () => { return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..903a648c4 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,46 @@ -export const PeopleFilters = () => { +import React, { useState } from 'react'; +import { SearchLink } from './SearchLink'; +import cn from 'classnames'; + +interface PeopleFiltersProps { + onNameFilterChange: (query: string) => void; + onCenturyFilterChange: (century: string, isSelected: boolean) => void; + onSortChange: (field: string) => void; + onSexFilterChange: (sex: string | null) => void; +} + +export const PeopleFilters: React.FC = ({ + onNameFilterChange, + onCenturyFilterChange, +}) => { + const [selectedSex, setSelectedSex] = useState(null); + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..5221f34f5 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,8 +1,150 @@ +import { useSearchParams } from 'react-router-dom'; +import React, { useMemo, useEffect, useState } from 'react'; import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { getPeople } from '../api'; +import { getSearchWith } from '../utils/searchHelper'; +import { Person } from '../types'; export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [loading, setLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + setLoading(true); + getPeople() + .then(peopleFromServer => { + setPeople(peopleFromServer); + setHasError(false); + }) + .catch(() => { + setHasError(true); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const handleNameFilterChange = (query: string) => { + setSearchParams(getSearchWith(searchParams, { query: query || null })); + }; + + const toggleCentury = (century: string) => { + const currentCenturies = searchParams.getAll('centuries'); + const updatedCenturies = currentCenturies.includes(century) + ? currentCenturies.filter(c => c !== century) + : [...currentCenturies, century]; + + setSearchParams( + getSearchWith(searchParams, { + centuries: updatedCenturies.length ? updatedCenturies : null, + }), + ); + }; + + const handleCenturyFilterChange = (century: string) => { + toggleCentury(century); + }; + + const handleSexFilterChange = (sex: string | null) => { + setSearchParams(getSearchWith(searchParams, { sex: sex || null })); + }; + + const handleSort = (field: string) => { + const currentSort = searchParams.get('sort'); + const currentOrder = searchParams.get('order'); + + let newSort = field; + let newOrder = 'asc'; + + if (currentSort === field) { + if (currentOrder === 'asc') { + newOrder = 'desc'; + } else if (currentOrder === 'desc') { + newSort = ''; + newOrder = ''; + } + } + + setSearchParams( + getSearchWith(searchParams, { + sort: newSort || null, + order: newOrder || null, + }), + ); + }; + + const filteredPeople = useMemo(() => { + const query = searchParams.get('query')?.toLowerCase() || ''; + const centuries = searchParams.getAll('centuries').map(Number); + const sexFilter = searchParams.get('sex'); + + return people.filter(person => { + const matchesQuery = person.name.toLowerCase().includes(query); + const matchesCentury = + centuries.length === 0 || + centuries.includes(Math.ceil(person.born / 100)); + const matchesSex = !sexFilter || person.sex === sexFilter; + + return matchesQuery && matchesCentury && matchesSex; + }); + }, [people, searchParams]); + + const sortedPeople = useMemo(() => { + const sortField = searchParams.get('sort'); + const sortOrder = searchParams.get('order') === 'desc' ? -1 : 1; + + if (!sortField) { + return filteredPeople; + } + + return [...filteredPeople].sort((a, b) => { + const valueA = a[sortField as keyof Person]; + const valueB = b[sortField as keyof Person]; + + if (typeof valueA === 'string' && typeof valueB === 'string') { + return valueA.localeCompare(valueB) * sortOrder; + } + + if (typeof valueA === 'number' && typeof valueB === 'number') { + return (valueA - valueB) * sortOrder; + } + + return 0; + }); + }, [filteredPeople, searchParams]); + + if (loading) { + return ( + <> +

People Page

+
+
+ +
+
+ + ); + } + + if (hasError) { + return ( + <> +

People Page

+
+
+

+ Something went wrong +

+
+
+ + ); + } + return ( <>

People Page

@@ -10,20 +152,30 @@ export const PeoplePage = () => {
- +
- - -

Something went wrong

- -

There are no people on the server

- -

There are no people matching the current search criteria

- - + {filteredPeople.length > 0 ? ( + + ) : ( +

+ {people.length > 0 + ? 'There are no people matching the current search criteria' + : 'There are no people on the server'} +

+ )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..a72f395cc 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,40 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import React from 'react'; +import { PersonLink } from './personLink'; +import { Person } from '../types'; +import { useParams } from 'react-router-dom'; +import cn from 'classnames'; + +interface PeopleTableProps { + people: Person[]; + sortField: string | null; + sortOrder: string | null; + onSortChange: (field: string) => void; +} +export const PeopleTable: React.FC = ({ + people, + sortField, + sortOrder, + onSortChange, +}) => { + const { slug } = useParams(); + + const findParentInTable = (parentName: string | null) => { + return people.find(person => person.name === parentName); + }; + + const getSortIcon = (field: string) => { + if (sortField === field) { + if (sortOrder === 'asc') { + return ; + } else if (sortOrder === 'desc') { + return ; + } + } + + return ; + }; + return ( { > - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => { + const mother = findParentInTable(person.motherName); + const father = findParentInTable(person.fatherName); + + return ( + + + + + + + + {mother ? ( + + ) : ( + + )} + + {father ? ( + + ) : ( + + )} + + ); + })}
- - Name - - - - - - - - - Sex - - - - - - + onSortChange('name')}> + Name{getSortIcon('name')} - - Born - - - - - - + onSortChange('sex')}>Sex{getSortIcon('sex')} onSortChange('born')}> + Born{getSortIcon('born')} - - Died - - - - - - + onSortChange('died')}> + Died{getSortIcon('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.sex}{person.born}{person.died}{person.motherName || '-'}{person.fatherName || '-'}
); diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..331562a2e 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,5 +1,6 @@ import { Link, LinkProps, useSearchParams } from 'react-router-dom'; import { getSearchWith, SearchParams } from '../utils/searchHelper'; +import React from 'react'; /** * To replace the the standard `Link` we take all it props except for `to` diff --git a/src/components/personLink.tsx b/src/components/personLink.tsx new file mode 100644 index 000000000..9c3b55e23 --- /dev/null +++ b/src/components/personLink.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import cn from 'classnames'; +import { Person } from '../types'; + +interface PersonLinkProps { + person: Person; +} + +export const PersonLink: React.FC = ({ person }) => { + const [searchParams] = useSearchParams(); + + return ( + + + {person.name} + + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index d72ba5730..9568ae587 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,9 @@ import { createRoot } from 'react-dom/client'; -import { HashRouter as Router } from 'react-router-dom'; import 'bulma/css/bulma.css'; import '@fortawesome/fontawesome-free/css/all.css'; -import { App } from './App'; +import { Root } from './Root'; +import React from 'react'; -createRoot(document.getElementById('root') as HTMLDivElement).render( - - - , -); +createRoot(document.getElementById('root') as HTMLDivElement).render();