├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.js
├── components
│ ├── Header.js
│ ├── Loader.js
│ ├── Paginate.js
│ ├── PostCard.js
│ └── Posts.js
├── index.js
├── redux
│ ├── actions
│ │ └── PostActions.js
│ ├── constants
│ │ └── PostConstants.js
│ ├── reducers
│ │ └── PostReducers.js
│ └── rootReducers.js
└── store.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Redux Filtering
2 |
3 | ## Getting Started
4 |
5 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
6 |
7 | ```
8 | node@v10.16.0 or higher
9 | npm@6.9.0 or higher
10 | git@2.17.1 or higher
11 | ```
12 |
13 | ## How To Use
14 |
15 | From your command line, clone and run React Redux Filtering:
16 |
17 | ```bash
18 | # Clone this repository
19 | $ git clone https://github.com/saidMounaim/React-Redux-Filtering.git
20 |
21 | # Go into the repository
22 | $ cd React-Redux-Filtering
23 |
24 | # Install dependencies
25 | $ npm install
26 |
27 | #Start's development server
28 | $ npm start
29 | ```
30 |
31 | ## Technologies Used
32 |
33 | - [React](https://reactjs.org/)
34 | - [Redux](https://redux.js.org/)
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shopping-cart",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-redux": "^7.2.3",
13 | "react-scripts": "4.0.3",
14 | "redux": "^4.0.5",
15 | "redux-thunk": "^2.3.0",
16 | "web-vitals": "^1.0.1"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidMounaim/React-Redux-Filtering/3fa8abc4a93afa24c1c6b313cddba6af54c2bd36/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Filtering, Sorting and Pagination With React Hooks & Redux
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500&display=swap');
2 |
3 | * {
4 | padding: 0;
5 | margin: 0;
6 | box-sizing: border-box;
7 | }
8 |
9 | body {
10 | font-family: 'Poppins', sans-serif;
11 | width: 100%;
12 | padding-bottom: 100px;
13 | }
14 |
15 | input {
16 | border: none;
17 | outline: none;
18 | }
19 |
20 | /** START LOADER STYLE **/
21 |
22 | @keyframes bouncing-loader {
23 | to {
24 | opacity: 0.1;
25 | transform: translate3d(0, -16px, 0);
26 | }
27 | }
28 |
29 | .bouncing-loader {
30 | display: flex;
31 | justify-content: center;
32 | }
33 |
34 | .bouncing-loader > div {
35 | width: 16px;
36 | height: 16px;
37 | margin: 3rem 0.2rem;
38 | background: #8385aa;
39 | border-radius: 50%;
40 | animation: bouncing-loader 0.6s infinite alternate;
41 | }
42 |
43 | .bouncing-loader > div:nth-child(2) {
44 | animation-delay: 0.2s;
45 | }
46 |
47 | .bouncing-loader > div:nth-child(3) {
48 | animation-delay: 0.4s;
49 | }
50 |
51 | /** END LOADER STYLE **/
52 |
53 | /** START HEADER STYLE **/
54 |
55 | header {
56 | display: flex;
57 | flex-direction: column;
58 | align-items: center;
59 | justify-content: center;
60 | padding: 80px 0;
61 | }
62 |
63 | header .title h1 {
64 | font-size: 40px;
65 | }
66 |
67 | header .filters {
68 | width: 530px;
69 | max-width: 95%;
70 | margin: auto;
71 | margin-top: 50px;
72 | display: flex;
73 | align-items: center;
74 | justify-content: space-between;
75 | }
76 |
77 | header .search {
78 | width: 100%;
79 | display: flex;
80 | flex-direction: column;
81 | margin-right: 15px;
82 | }
83 |
84 | header .sort {
85 | width: 30%;
86 | display: flex;
87 | flex-direction: column;
88 | }
89 |
90 | header .filters input,
91 | header .filters select {
92 | font-size: 15px;
93 | font-family: inherit;
94 | color: #292929;
95 | width: 100%;
96 | padding: 10px 15px;
97 | background-color: transparent;
98 | border: 1px solid #292929;
99 | outline: none;
100 | }
101 |
102 | header .filters input::placeholder {
103 | color: #292929;
104 | }
105 |
106 | /** END HEADER STYLE **/
107 |
108 | .posts {
109 | margin-top: 40px;
110 | margin-bottom: 80px;
111 | display: grid;
112 | grid-template-columns: repeat(3, 1fr);
113 | grid-gap: 20px;
114 | }
115 |
116 | .posts .post {
117 | padding: 30px;
118 | border: 1px solid #292929;
119 | color: #292929;
120 | }
121 |
122 | .posts .post h2 {
123 | font-size: 21px;
124 | margin-bottom: 20px;
125 | }
126 |
127 | .posts .post p {
128 | font-size: 16px;
129 | }
130 |
131 | /** END POSTS STYLE **/
132 |
133 | @media screen and (max-width: 768px) {
134 | .posts {
135 | grid-template-columns: repeat(2, 1fr);
136 | }
137 | }
138 |
139 | @media screen and (max-width: 500px) {
140 | .posts {
141 | grid-template-columns: repeat(1, 1fr);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import Posts from './components/Posts';
3 |
4 | function App() {
5 | return (
6 |
9 | );
10 | }
11 |
12 | export default App;
13 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { sortPostsAsc, sortPostsDesc, searchPosts } from '../redux/actions/PostActions';
4 |
5 | const Header = ({ search, setSearch, onChange }) => {
6 | const dispatch = useDispatch();
7 | const [sort, setSort] = useState('asc');
8 |
9 | useEffect(() => {
10 | dispatch(searchPosts(search));
11 | if (sort === 'desc') {
12 | dispatch(sortPostsDesc());
13 | }
14 | if (sort === 'asc') {
15 | dispatch(sortPostsAsc());
16 | }
17 | }, [search, sort, dispatch]);
18 |
19 | return (
20 |
41 | );
42 | };
43 |
44 | export default Header;
45 |
--------------------------------------------------------------------------------
/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loader = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default Loader;
14 |
--------------------------------------------------------------------------------
/src/components/Paginate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Paginate = ({ currentPage, setCurrentPage, totalPosts, postPerPage }) => {
4 | const totalPages = Math.ceil(totalPosts / postPerPage);
5 |
6 | let pages = [];
7 |
8 | for (let p = 1; p <= totalPages; p++) {
9 | pages.push(p);
10 | }
11 |
12 | return (
13 |
14 | -
15 |
18 |
19 | {pages.map((page) => (
20 | - setCurrentPage(page)}
24 | >
25 |
26 |
27 | ))}
28 | -
29 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default Paginate;
38 |
--------------------------------------------------------------------------------
/src/components/PostCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PostCard = ({ post }) => {
4 | return (
5 |
6 |
{post?.title}
7 |
{post?.body}
8 |
9 | );
10 | };
11 |
12 | export default PostCard;
13 |
--------------------------------------------------------------------------------
/src/components/Posts.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Header from './Header';
3 | import PostCard from './PostCard';
4 | import Loader from './Loader';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { fetchPosts } from '../redux/actions/PostActions';
7 | import Paginate from './Paginate';
8 |
9 | const Posts = () => {
10 |
11 | const [search, setSearch] = useState('');
12 | const dispatch = useDispatch();
13 | const { posts, loading } = useSelector((state) => state.PostReducers);
14 | const [currentPage, setCurrentPage] = useState(1);
15 |
16 | const handleChangeSearch = (e) => {
17 | if(e.target.value.length > 0) {
18 | setCurrentPage(1);
19 | }
20 | setSearch(e.target.value);
21 | }
22 |
23 | useEffect(() => {
24 | dispatch(fetchPosts());
25 | }, [dispatch]);
26 |
27 | const postPerPage = 15;
28 | const totalPosts = posts.length;
29 |
30 | const indexOfLastPost = currentPage * postPerPage;
31 | const indexOfFirstPost = indexOfLastPost - postPerPage;
32 | const filterPosts = posts.slice(indexOfFirstPost, indexOfLastPost);
33 |
34 | return (
35 | <>
36 |
37 | {loading ? (
38 |
39 | ) : (
40 |
41 |
42 | {filterPosts.map((post) => (
43 |
44 | ))}
45 |
46 | {totalPosts > postPerPage && (
47 |
53 | )}
54 |
55 | )}
56 | >
57 | );
58 | };
59 |
60 | export default Posts;
61 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | import { Provider } from 'react-redux';
6 | import store from './store';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/src/redux/actions/PostActions.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../constants/PostConstants';
2 | import axios from 'axios';
3 |
4 | export const fetchPosts = () => async (dispatch) => {
5 | dispatch({ type: actions.FETCH_POST_REQUEST });
6 |
7 | try {
8 | const data = await axios.get('https://jsonplaceholder.typicode.com/posts');
9 | dispatch({ type: actions.FETCH_POST_SUCCESS, payload: data });
10 | } catch (error) {
11 | dispatch({ type: actions.FETCH_POST_FAILED, payload: error.message });
12 | console.log(error.message);
13 | }
14 | };
15 |
16 | export const sortPostsAsc = () => (dispatch, getState) => {
17 | const { PostReducers } = getState();
18 | dispatch({ type: actions.SORT_POSTS_ASC, payload: PostReducers.posts });
19 | };
20 |
21 | export const sortPostsDesc = () => (dispatch, getState) => {
22 | const { PostReducers } = getState();
23 | dispatch({ type: actions.SORT_POSTS_DESC, payload: PostReducers.posts });
24 | };
25 |
26 | export const searchPosts = (query) => (dispatch, getState) => {
27 | console.log(query);
28 | const { PostReducers } = getState();
29 | const searchResults = PostReducers.searchResults.filter((post) =>
30 | post.title.toLowerCase().includes(query.toLowerCase())
31 | );
32 | dispatch({ type: actions.SEARCH_POSTS, payload: searchResults });
33 | };
34 |
--------------------------------------------------------------------------------
/src/redux/constants/PostConstants.js:
--------------------------------------------------------------------------------
1 | export const FETCH_POST_REQUEST = 'FETCH_POST_REQUEST';
2 | export const FETCH_POST_SUCCESS = 'FETCH_POST_SUCCESS';
3 | export const FETCH_POST_FAILED = 'FETCH_POST_FAILED';
4 |
5 | export const SORT_POSTS_ASC = 'SORT_POSTS_ASC';
6 | export const SORT_POSTS_DESC = 'SORT_POSTS_DESC';
7 |
8 | export const SEARCH_POSTS = 'SEARCH_POSTS';
9 |
--------------------------------------------------------------------------------
/src/redux/reducers/PostReducers.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../constants/PostConstants';
2 |
3 | const initialState = {
4 | posts: [],
5 | searchResults: [],
6 | page: 1,
7 | };
8 |
9 | export const PostReducers = (state = initialState, action) => {
10 | switch (action.type) {
11 | case actions.FETCH_POST_REQUEST:
12 | return {
13 | ...state,
14 | loading: true,
15 | };
16 | case actions.FETCH_POST_SUCCESS:
17 | return {
18 | ...state,
19 | loading: false,
20 | posts: action.payload.data,
21 | searchResults: action.payload.data,
22 | };
23 | case actions.FETCH_POST_FAILED:
24 | return {
25 | ...state,
26 | loading: false,
27 | error: action.payload,
28 | };
29 | case actions.SORT_POSTS_ASC:
30 | const sortAsc = action.payload.sort((a, b) => (a.title < b.title ? 1 : a.title > b.title ? -1 : 0));
31 | return {
32 | ...state,
33 | posts: sortAsc,
34 | };
35 | case actions.SORT_POSTS_DESC:
36 | const sortDesc = action.payload.sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0));
37 | return {
38 | ...state,
39 | posts: sortDesc,
40 | };
41 | case actions.SEARCH_POSTS:
42 | return {
43 | ...state,
44 | posts: action.payload,
45 | page: 1
46 | };
47 | default:
48 | return state;
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/src/redux/rootReducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { PostReducers } from './reducers/PostReducers';
3 |
4 | const rootReducers = combineReducers({
5 | PostReducers,
6 | });
7 |
8 | export default rootReducers;
9 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducers from './redux/rootReducers';
4 |
5 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
6 |
7 | const store = createStore(rootReducers, composeEnhancer(applyMiddleware(thunk)));
8 |
9 | export default store;
10 |
--------------------------------------------------------------------------------