Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ implement the ability to filter and sort people in the table.
1. Keep search params when navigating within the `People` page (when selecting a person or clicking the `People` link).
1. The sidebar with the filters should appear only when people are loaded.
1. `NameFilter` should update the `query` search param with the text from the input.
- show only people with the `name`, `motherName` or `fatherName` that match the query case insensitive;
- if the input is empty there should not be `query` in the search params.
- show only people with the `name`, `motherName` or `fatherName` that match the query case insensitive;
- if the input is empty there should not be `query` in the search params.
1. `CenturyFilter` should allow to choose several centuries or all of them.
- add `centuries` search params using `append` method `getAll` method;
- add `centuries` search params using `append` method `getAll` method;
1. Implement sorting by `name`, `sex`, `born` and `died` by clicking on arrows in a `th`;
- the first click on a column sorts people by the selected field ascending (`a -> z` or `0 -> 9`);
- the second click (when people are already sorted ascending by this field) reverses the order of sorting;
- the third click (when people are already sorted in reversed order by this field) disables sorting;
- use `sort` search param to save sort field;
- add `order=desc` (short for `descending`) if sorted in reversed order;
- if sorting is disabled there should not be `sort` and `order` search params;
- the first click on a column sorts people by the selected field ascending (`a -> z` or `0 -> 9`);
- the second click (when people are already sorted ascending by this field) reverses the order of sorting;
- the third click (when people are already sorted in reversed order by this field) disables sorting;
- use `sort` search param to save sort field;
- add `order=desc` (short for `descending`) if sorted in reversed order;
- if sorting is disabled there should not be `sort` and `order` search params;

## Instructions

- Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save.
- 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 `<your_account>` with your Github username in the [DEMO LINK](https://<your_account>.github.io/react_people-table-advanced/) and add it to the PR description.
- Replace `<your_account>` with your Github username in the [DEMO LINK](https://ProninaMariia.github.io/react_people-table-advanced/) and add it to the PR description.
7,366 changes: 3,939 additions & 3,427 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
20 changes: 16 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PeoplePage } from './components/PeoplePage';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Navbar } from './components/Navbar';
import { PeoplePage } from './components/PeoplePage';

import './App.scss';

Expand All @@ -10,9 +11,20 @@ export const App = () => {

<div className="section">
<div className="container">
<h1 className="title">Home Page</h1>
<h1 className="title">Page not found</h1>
<PeoplePage />
<Routes>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current routing works, but nothing here ensures that search params are preserved when navigating within /people (checklist #4/#16). When you later update links (e.g. People link in Navbar and person links in the table) to keep query/sort/centuries, make sure the router structure still supports that behavior.

<Route path="/" element={<h1 className="title">Home Page</h1>} />

<Route path="/people" element={<PeoplePage />} />

<Route path="/people/:slug" element={<PeoplePage />} />

<Route path="/home" element={<Navigate to="/" replace />} />

<Route
path="*"
element={<h1 className="title">Page not found</h1>}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The People nav link doesn’t preserve current search params, so navigating back to /people from a person page will reset filters and sorting, violating checklist items #4 and #16. Consider using your SearchLink or useSearchParams to keep the existing search in the URL for this link when already on the People section.

</Routes>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/components/Loader/index.tsx

This file was deleted.

30 changes: 23 additions & 7 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { NavLink, useLocation } from 'react-router-dom';
import classNames from 'classnames';

export const Navbar = () => {
const location = useLocation();

return (
<nav
data-cy="nav"
Expand All @@ -8,17 +13,28 @@ export const Navbar = () => {
>
<div className="container">
<div className="navbar-brand">
<a className="navbar-item" href="#/">
<NavLink
to="/"
end
className={({ isActive }) =>
classNames('navbar-item', {
'has-background-grey-lighter': isActive,
})
}
>
Home
</a>
</NavLink>

<a
aria-current="page"
className="navbar-item has-background-grey-lighter"
href="#/people"
<NavLink
to={{ pathname: '/people', search: location.search }}
className={({ isActive }) =>
classNames('navbar-item', {
'has-background-grey-lighter': isActive,
})
}
>
People
</a>
</NavLink>
</div>
Comment on lines +37 to 38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PeopleFilters is rendered unconditionally, but checklist item #5 requires the sidebar with filters to appear only when people are loaded. You should gate this component behind !loading && !error && people.length > 0 (or equivalent) so it doesn’t render before data is available.

</div>
</nav>
Expand Down
110 changes: 58 additions & 52 deletions src/components/PeopleFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
import { useSearchParams } from 'react-router-dom';
import { SearchLink } from './SearchLink';
import { getSearchWith } from '../utils/searchHelper';

const CENTURIES = ['16', '17', '18', '19', '20'];

export const PeopleFilters = () => {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('query') || '';
const sex = searchParams.get('sex') || '';
const centuries = searchParams.getAll('centuries');

const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchParams(
getSearchWith(searchParams, { query: e.target.value || null }),
);
};

const toggleCentury = (century: string) => {
const newCenturies = centuries.includes(century)
? centuries.filter(c => c !== century)
: [...centuries, century];

return newCenturies.length ? newCenturies : null;
};

return (
<nav className="panel">
<p className="panel-heading">Filters</p>

<p className="panel-tabs" data-cy="SexFilter">
<a className="is-active" href="#/people">
<SearchLink params={{ sex: null }} className={!sex ? 'is-active' : ''}>
All
</a>
<a className="" href="#/people?sex=m">
</SearchLink>
<SearchLink
params={{ sex: 'm' }}
className={sex === 'm' ? 'is-active' : ''}
>
Male
</a>
<a className="" href="#/people?sex=f">
</SearchLink>
<SearchLink
params={{ sex: 'f' }}
className={sex === 'f' ? 'is-active' : ''}
>
Female
</a>
</SearchLink>
</p>

<div className="panel-block">
Expand All @@ -22,8 +53,9 @@ export const PeopleFilters = () => {
type="search"
className="input"
placeholder="Search"
value={query}
onChange={handleQueryChange}
/>

<span className="icon is-left">
<i className="fas fa-search" aria-hidden="true" />
</span>
Expand All @@ -33,63 +65,37 @@ export const PeopleFilters = () => {
<div className="panel-block">
<div className="level is-flex-grow-1 is-mobile" data-cy="CenturyFilter">
<div className="level-left">
<a
data-cy="century"
className="button mr-1"
href="#/people?centuries=16"
>
16
</a>

<a
data-cy="century"
className="button mr-1 is-info"
href="#/people?centuries=17"
>
17
</a>

<a
data-cy="century"
className="button mr-1 is-info"
href="#/people?centuries=18"
>
18
</a>

<a
data-cy="century"
className="button mr-1 is-info"
href="#/people?centuries=19"
>
19
</a>

<a
data-cy="century"
className="button mr-1"
href="#/people?centuries=20"
>
20
</a>
{CENTURIES.map(century => (
<SearchLink
key={century}
data-cy="century"
params={{ centuries: toggleCentury(century) }}
className={`button mr-1 ${centuries.includes(century) ? 'is-info' : ''}`}
>
{century}
</SearchLink>
))}
</div>

<div className="level-right ml-4">
<a
<SearchLink
data-cy="centuryALL"
className="button is-success is-outlined"
href="#/people"
params={{ centuries: null }}
className={`button is-success ${!centuries.length ? '' : 'is-outlined'}`}
>
All
</a>
</SearchLink>
</div>
</div>
</div>

<div className="panel-block">
<a className="button is-link is-outlined is-fullwidth" href="#/people">
<SearchLink
params={{ query: null, sex: null, centuries: null }}
className="button is-link is-outlined is-fullwidth"
>
Reset all filters
</a>
</SearchLink>
</div>
</nav>
);
Expand Down
103 changes: 96 additions & 7 deletions src/components/PeoplePage.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,118 @@
import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Person } from '../types/Person';
import { getPeople } from '../api';
import { PeopleFilters } from './PeopleFilters';
import { Loader } from './Loader';
import { Loader } from './Loader/Loader';
import { PeopleTable } from './PeopleTable';

export const PeoplePage = () => {
const [people, setPeople] = useState<Person[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [searchParams] = useSearchParams();

const query = searchParams.get('query') || '';
const sex = searchParams.get('sex') || '';
const centuries = searchParams.getAll('centuries');
const sort = searchParams.get('sort') || '';
const order = searchParams.get('order') || '';

useEffect(() => {
setLoading(true);
getPeople()
.then(data => {
const peopleWithParents = data.map(person => ({
Comment on lines +22 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current routing setup correctly renders PeoplePage for both /people and /people/:slug, but it doesn’t address checklist items #4 and #16: search params must be kept when navigating within the People page. You’ll need to ensure links to /people and person details use search-aware navigation (via SearchLink) so query/sort/centuries params are preserved.

...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));
}, []);
Comment on lines +34 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This NavLink to /people ignores any existing search params, so if the user has filters or sorting applied, clicking People will drop them, which violates checklist items #4 and #16. Consider replacing this with a component that uses useSearchParams (like SearchLink) to preserve the current search when navigating to the People page.


const visiblePeople = useMemo(() => {
let filtered = [...people];

if (query) {
const q = query.toLowerCase();

filtered = filtered.filter(
p =>
p.name.toLowerCase().includes(q) ||
p.motherName?.toLowerCase().includes(q) ||
p.fatherName?.toLowerCase().includes(q),
);
}

if (sex) {
filtered = filtered.filter(p => p.sex === sex);
}

if (centuries.length) {
filtered = filtered.filter(p =>
centuries.includes(String(Math.ceil(p.born / 100))),
);
}

if (sort) {
filtered.sort((a, b) => {
let result = 0;

switch (sort) {
case 'name':
case 'sex':
result = a[sort].localeCompare(b[sort]);
break;
case 'born':
case 'died':
result = a[sort] - b[sort];
break;
}

return order === 'desc' ? -result : result;
});
}

return filtered;
}, [people, query, sex, centuries, sort, order]);

const dataLoaded = !loading && !error && people.length > 0;

return (
<>
<h1 className="title">People Page</h1>

<div className="block">
<div className="columns is-desktop is-flex-direction-row-reverse">
<div className="column is-7-tablet is-narrow-desktop">
<PeopleFilters />
{dataLoaded && <PeopleFilters />}
</div>

<div className="column">
<div className="box table-container">
<Loader />
{loading && <Loader />}

<p data-cy="peopleLoadingError">Something went wrong</p>
{error && (
<p data-cy="peopleLoadingError">Something went wrong</p>
)}

<p data-cy="noPeopleMessage">There are no people on the server</p>
{!loading && !error && people.length === 0 && (
<p data-cy="noPeopleMessage">
There are no people on the server
</p>
)}

<p>There are no people matching the current search criteria</p>
{dataLoaded && visiblePeople.length === 0 && (
<p>There are no people matching the current search criteria</p>
)}

<PeopleTable />
{dataLoaded && visiblePeople.length > 0 && (
<PeopleTable people={visiblePeople} />
)}
</div>
</div>
</div>
Expand Down
Loading
Loading