├── .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 |
7 | 8 |
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 |
21 |
22 |

React Redux Filtering

23 |
24 |
25 |
26 | 32 |
33 |
34 | 38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Header; 45 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
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 | 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 | --------------------------------------------------------------------------------