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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# React - People table
- Replace `<your_account>` with your Github username in the
[DEMO LINK](https://<your_account>.github.io/react_people-table/)
[DEMO LINK](https://naveed-shoukat.github.io/react_people-table/)
- Follow the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline)

## If you don't use **Typescript**
1. Rename `.tsx` files to `.jsx`
1. use `eslint-config-react` in `.eslintrs.js`
1. use `eslint-config-react` in `.eslintrs.js`

## Basic tasks
1. Install all the NPM packages you need and types for them.
Expand Down Expand Up @@ -91,3 +91,5 @@
- make `mother` and `father` field optional
- update the list of `mothers` and `fathers` according to the entered `born` year (they must be alive)
(selects should be empty and disabled before the born year was entered)

Here is [the working example](https://mate-academy.github.io/react_people-table-advanced/)
42 changes: 11 additions & 31 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,19 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Navigation } from './components/Navigation';

import '@fortawesome/fontawesome-free/css/all.css';
import 'bulma/css/bulma.css';
import './App.scss';

import peopleFromServer from './people.json';
export const App: React.FC = () => (
<div data-cy="app">
<Navigation />

export class App extends React.Component {
state = {};

render() {
return (
<div className="box">
<h1 className="title">People table</h1>

<table className="table is-striped is-narrow">
<thead>
<tr>
<th>name</th>
<th>sex</th>
<th>born</th>
</tr>
</thead>

<tbody>
{peopleFromServer.map(person => (
<tr key={person.slug}>
<td>{person.name}</td>
<td>{person.sex}</td>
<td>{person.born}</td>
</tr>
))}
</tbody>
</table>
<main className="section">
<div className="container">
<Outlet />
</div>
);
}
}
</main>
</div>
);
76 changes: 76 additions & 0 deletions src/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { ReactNode, Reducer, useReducer } from 'react';
import { Person } from './types/Person';

export enum ReducerActions {
setPeople = 'setPeople',
setIsPeopleLoading = 'setIsPeopleLoading',
setHasPeopleLoadingError = 'setHasPeopleLoadingError',
}

type Props = {
children: ReactNode;
};

type DispatchContextType = (action: DispatchActions) => void;

interface State {
people: Person[] | null;
isPeopleLoading: boolean;
hasPeopleLoadingError: boolean;
}
const initialState: State = {
people: null,
isPeopleLoading: false,
hasPeopleLoadingError: false,
};

interface DispatchActions {
type: ReducerActions,
// eslint-disable-next-line
payload: any,
}

const reducerCallBack: Reducer<State, DispatchActions> = (state, action) => {
const { type, payload } = action;

switch (type) {
case ReducerActions.setPeople:
return {
...state,
people: payload,
};

case ReducerActions.setIsPeopleLoading:
return {
...state,
isPeopleLoading: payload,
};

case ReducerActions.setHasPeopleLoadingError:
return {
...state,
hasPeopleLoadingError: payload,
};

default:
return state;
}
};

export const DispatchContext = React.createContext<
DispatchContextType
>(() => {});

export const StateContext = React.createContext(initialState);

export const StateProvider: React.FC<Props> = ({ children }) => {
const [state, dispatch] = useReducer(reducerCallBack, initialState);

return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
};
15 changes: 15 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Person } from './types/Person';

// eslint-disable-next-line max-len
const API_URL = 'https://mate-academy.github.io/react_people-table/api/people.json';

function wait(delay: number) {
return new Promise(resolve => setTimeout(resolve, delay));
}

export function getPeople(): Promise<Person[]> {
// keep this delay for testing purpose
return wait(500)
.then(() => fetch(API_URL))
.then(response => response.json());
}
35 changes: 35 additions & 0 deletions src/components/CenturyFilterLink/CenturyFilterLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import cn from 'classnames';
import { Link, useLocation } from 'react-router-dom';

type Props = {
century: string,
handelCentury: (querytype: string, value: string) => void;
};

export const CenturyFilterLink: React.FC<Props> = (props) => {
const { century, handelCentury } = props;
const { search } = useLocation();
const isActive = search.includes(`century=${century}`);

return (
<Link
data-cy="century"
className={cn(
'button mr-1',
{ 'is-info': isActive },
)}
to={
isActive
? '/people'
: `/people?century=${century}`
}
onClick={(event) => {
event.preventDefault();
handelCentury('century', century);
}}
>
{century}
</Link>
);
};
1 change: 1 addition & 0 deletions src/components/CenturyFilterLink/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CenturyFilterLink';
188 changes: 188 additions & 0 deletions src/components/FilteringForm/FilteringForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import cn from 'classnames';
import { CenturyFilterLink } from '../CenturyFilterLink';

export const FilteringForm: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();

const handelFilterSearchURL = (filterType: string, value: string) => {
if (filterType === 'sex' || filterType === 'query') {
const prevValue = searchParams.get(filterType);

if (!prevValue) {
searchParams.append(filterType, value);
setSearchParams(searchParams);

return;
}

if (!value) {
searchParams.delete(filterType);
setSearchParams(searchParams);

return;
}

searchParams.set(filterType, value);
setSearchParams(searchParams);
}

if (filterType === 'century') {
const prevValue = searchParams.getAll(filterType);

if (prevValue.length === 0) {
searchParams.append(filterType, value);
setSearchParams(searchParams);

return;
}

const newCenturyValue = prevValue.includes(value)
? prevValue.filter((century: string) => century !== value)
: [...prevValue, value];

searchParams.delete(filterType);

if (!newCenturyValue.length) {
setSearchParams(searchParams);

return;
}

newCenturyValue.forEach(
(century: string) => searchParams.append(filterType, century),
);

setSearchParams(searchParams);
}
};

const resetAllCenturies = (event: React.SyntheticEvent) => {
event.preventDefault();
searchParams.delete('century');
setSearchParams(searchParams);
};

const resetAllFilter = (event: React.SyntheticEvent) => {
event.preventDefault();
searchParams.delete('sex');
searchParams.delete('century');
searchParams.delete('query');
setSearchParams(searchParams);
};

return (
<div className="column is-7-tablet is-narrow-desktop">
<nav className="panel">
<p className="panel-heading">Filters</p>

<p className="panel-tabs" data-cy="SexFilter">
<Link
className={cn({ 'is-active': searchParams.get('sex') === '' })}
to={{ search: '' }}
onClick={(event: React.SyntheticEvent) => {
event.preventDefault();
handelFilterSearchURL('sex', '');
}}
>
All
</Link>

<Link
className={cn({ 'is-active': searchParams.get('sex') === 'm' })}
to={{ search: '?sex=m' }}
onClick={(event: React.SyntheticEvent) => {
event.preventDefault();
handelFilterSearchURL('sex', 'm');
}}
>
Male
</Link>

<Link
className={cn({ 'is-active': searchParams.get('sex') === 'f' })}
to={{ search: '?sex=f' }}
onClick={(event: React.SyntheticEvent) => {
event.preventDefault();
handelFilterSearchURL('sex', 'f');
}}
>
Female
</Link>
</p>

<div className="panel-block">
<p className="control has-icons-left">
<input
data-cy="NameFilter"
className="input"
type="text"
placeholder="Search"
value={searchParams.get('query') || ''}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handelFilterSearchURL('query', event.target.value);
}}
/>
<span className="icon is-left">
<i className="fas fa-search" aria-hidden="true" />
</span>
</p>
</div>

<div className="panel-block">
<div
className="level is-flex-grow-1 is-mobile"
data-cy="CenturyFilter"
>
<div className="level-left">
<CenturyFilterLink
century="16"
handelCentury={handelFilterSearchURL}
/>
<CenturyFilterLink
century="17"
handelCentury={handelFilterSearchURL}
/>
<CenturyFilterLink
century="18"
handelCentury={handelFilterSearchURL}
/>
<CenturyFilterLink
century="19"
handelCentury={handelFilterSearchURL}
/>
<CenturyFilterLink
century="20"
handelCentury={handelFilterSearchURL}
/>
</div>
<div className="level-right ml-4">
<Link
data-cy="centuryALL"
className={cn(
'button',
{ 'is-success': searchParams.getAll('century').length === 0 },
)}
to={{ search: '' }}
onClick={resetAllCenturies}
>
All
</Link>
</div>
</div>
</div>

<div className="panel-block">
<Link
className="button is-link is-outlined is-fullwidth"
to={{ search: '' }}
onClick={resetAllFilter}
>
Reset all filters
</Link>
</div>
</nav>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/FilteringForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './FilteringForm';
Loading