├── public ├── .nojekyll ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json ├── index.html └── 404.html ├── src ├── reducers │ ├── index.js │ └── contactReducer.js ├── index.jsx ├── store.js ├── components │ ├── layout │ │ ├── LoadingSpinner.jsx │ │ ├── TextInputGroup.jsx │ │ ├── Header.jsx │ │ └── ConfirmationModal.jsx │ ├── pages │ │ ├── NotFound.jsx │ │ └── About.jsx │ ├── App.jsx │ └── contacts │ │ ├── Contacts.jsx │ │ ├── AddContact.jsx │ │ ├── EditContact.jsx │ │ └── Contact.jsx ├── services │ └── toastService.js └── actions │ └── contactActions.js ├── .gitignore ├── vite.config.js ├── index.html ├── package.json └── README.md /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmahmud/ContactManager/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmahmud/ContactManager/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmahmud/ContactManager/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import contactReducer from "./contactReducer"; 3 | 4 | export default combineReducers({ 5 | contact: contactReducer 6 | }); 7 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './components/App'; 4 | import 'bootstrap/dist/css/bootstrap.css'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { thunk } from 'redux-thunk'; 3 | 4 | import rootReducer from './reducers'; 5 | 6 | const initialState = {}; 7 | 8 | const middleware = [thunk]; 9 | 10 | const store = createStore(rootReducer, initialState, applyMiddleware(...middleware)); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | .vite/ 8 | 9 | # Environment variables 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # IDE and Editor files 17 | .vscode/ 18 | .idea/ 19 | *.swp 20 | 21 | # OS generated files 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | 28 | 29 | # Logs 30 | logs 31 | *.log -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | base: '/ContactManager/', 7 | server: { 8 | port: 3000, 9 | open: true, 10 | }, 11 | build: { 12 | outDir: 'dist', 13 | sourcemap: true, 14 | assetsDir: 'assets', 15 | }, 16 | esbuild: { 17 | loader: 'jsx', 18 | include: /src\/.*\.[jt]sx?$/, 19 | exclude: [], 20 | }, 21 | optimizeDeps: { 22 | esbuildOptions: { 23 | loader: { 24 | '.js': 'jsx', 25 | }, 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/layout/LoadingSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingSpinner = ({ size = 'md', text = 'Loading...' }) => { 4 | const sizeClasses = { 5 | sm: 'spinner-border-sm', 6 | md: '', 7 | lg: 'spinner-border-lg', 8 | }; 9 | 10 | return ( 11 |
12 |
13 | {text} 14 |
15 | {text && {text}} 16 |
17 | ); 18 | }; 19 | 20 | export default LoadingSpinner; 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | Contact Manager 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | Contact Manager 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/services/toastService.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | 3 | export const showSuccess = (message) => { 4 | toast.success(message, { 5 | position: 'top-right', 6 | autoClose: 3000, 7 | hideProgressBar: false, 8 | closeOnClick: true, 9 | pauseOnHover: true, 10 | draggable: true, 11 | }); 12 | }; 13 | 14 | export const showError = (message) => { 15 | toast.error(message, { 16 | position: 'top-right', 17 | autoClose: 4000, 18 | hideProgressBar: false, 19 | closeOnClick: true, 20 | pauseOnHover: true, 21 | draggable: true, 22 | }); 23 | }; 24 | 25 | export const showInfo = (message) => { 26 | toast.info(message, { 27 | position: 'top-right', 28 | autoClose: 3000, 29 | hideProgressBar: false, 30 | closeOnClick: true, 31 | pauseOnHover: true, 32 | draggable: true, 33 | }); 34 | }; 35 | 36 | export const showWarning = (message) => { 37 | toast.warning(message, { 38 | position: 'top-right', 39 | autoClose: 3000, 40 | hideProgressBar: false, 41 | closeOnClick: true, 42 | pauseOnHover: true, 43 | draggable: true, 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/layout/TextInputGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | const TextInputGroup = ({ label, name, value, placeholder, type, onChange, error }) => { 6 | return ( 7 |
8 | 11 | 21 | {error &&
{error}
} 22 |
23 | ); 24 | }; 25 | 26 | TextInputGroup.propTypes = { 27 | label: PropTypes.string.isRequired, 28 | name: PropTypes.string.isRequired, 29 | placeholder: PropTypes.string.isRequired, 30 | value: PropTypes.string.isRequired, 31 | type: PropTypes.string.isRequired, 32 | onChange: PropTypes.func.isRequired, 33 | error: PropTypes.string, 34 | }; 35 | 36 | TextInputGroup.defaultProps = { 37 | type: 'text', 38 | }; 39 | 40 | export default TextInputGroup; 41 | -------------------------------------------------------------------------------- /src/components/pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 |
12 |

404

13 |

Page Not Found

14 |

15 | Oops! The page you're looking for doesn't exist. It might have been moved, deleted, or 16 | you entered the wrong URL. 17 |

18 |
19 | 20 | Go Home 21 | 22 | 23 | Add Contact 24 | 25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contactmanager", 3 | "version": "0.1.0", 4 | "homepage": "https://devmahmud.github.io/ContactManager", 5 | "private": true, 6 | "type": "module", 7 | "dependencies": { 8 | "axios": "^1.7.7", 9 | "bootstrap": "^5.3.3", 10 | "classnames": "^2.5.1", 11 | "gh-pages": "^6.1.1", 12 | "react": "^18.3.1", 13 | "react-dom": "^18.3.1", 14 | "react-redux": "^9.1.2", 15 | "react-router-dom": "^6.26.2", 16 | "react-toastify": "^11.0.5", 17 | "redux": "^5.0.1", 18 | "redux-thunk": "^3.1.0", 19 | "uuid": "^10.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.3.12", 23 | "@types/react-dom": "^18.3.1", 24 | "@vitejs/plugin-react": "^4.3.3", 25 | "eslint": "^8.57.1", 26 | "eslint-plugin-react": "^7.37.2", 27 | "eslint-plugin-react-hooks": "^5.0.0", 28 | "eslint-plugin-react-refresh": "^0.4.14", 29 | "vite": "^5.4.10" 30 | }, 31 | "scripts": { 32 | "dev": "vite", 33 | "build": "vite build", 34 | "preview": "vite preview", 35 | "deploy": "npm run build && gh-pages -d dist --dotfiles", 36 | "predeploy": "npm run build" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | Contact Manager 15 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ToastContainer } from 'react-toastify'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | import store from '../store'; 7 | 8 | import Contacts from './contacts/Contacts'; 9 | import Header from './layout/Header'; 10 | import About from './pages/About'; 11 | import AddContact from './contacts/AddContact'; 12 | import NotFound from './pages/NotFound'; 13 | import EditContact from './contacts/EditContact'; 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | <> 20 |
21 |
22 | 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | 29 |
30 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /src/reducers/contactReducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | contacts: [], 3 | contact: {}, 4 | loading: false, 5 | error: null, 6 | }; 7 | 8 | export default function (state = initialState, action) { 9 | switch (action.type) { 10 | case 'GET_CONTACTS_START': 11 | case 'GET_CONTACT_START': 12 | case 'ADD_CONTACT_START': 13 | case 'UPDATE_CONTACT_START': 14 | case 'DELETE_CONTACT_START': 15 | return { ...state, loading: true, error: null }; 16 | 17 | case 'GET_CONTACTS_SUCCESS': 18 | return { ...state, contacts: action.payload, loading: false }; 19 | 20 | case 'GET_CONTACT_SUCCESS': 21 | return { ...state, contact: action.payload, loading: false }; 22 | 23 | case 'ADD_CONTACT_SUCCESS': 24 | return { 25 | ...state, 26 | contacts: [action.payload, ...state.contacts], 27 | loading: false, 28 | }; 29 | 30 | case 'UPDATE_CONTACT_SUCCESS': 31 | return { 32 | ...state, 33 | contacts: state.contacts.map((contact) => 34 | contact.id === action.payload.id ? action.payload : contact 35 | ), 36 | loading: false, 37 | }; 38 | 39 | case 'DELETE_CONTACT_SUCCESS': 40 | return { 41 | ...state, 42 | contacts: state.contacts.filter((contact) => contact.id !== action.payload), 43 | loading: false, 44 | }; 45 | 46 | case 'GET_CONTACTS_ERROR': 47 | case 'GET_CONTACT_ERROR': 48 | case 'ADD_CONTACT_ERROR': 49 | case 'UPDATE_CONTACT_ERROR': 50 | case 'DELETE_CONTACT_ERROR': 51 | return { ...state, loading: false, error: action.payload }; 52 | 53 | case 'CLEAR_ERROR': 54 | return { ...state, error: null }; 55 | 56 | default: 57 | return state; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/layout/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Header = (props) => { 5 | return ( 6 | 46 | ); 47 | }; 48 | 49 | Header.defaultProps = { 50 | header: 'Contact Manager', 51 | }; 52 | 53 | export default Header; 54 | -------------------------------------------------------------------------------- /src/components/contacts/Contacts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import Contact from './Contact'; 5 | import LoadingSpinner from '../layout/LoadingSpinner'; 6 | import { getContacts } from '../../actions/contactActions'; 7 | 8 | function Contacts() { 9 | const dispatch = useDispatch(); 10 | const { contacts, loading, error } = useSelector((state) => state.contact); 11 | 12 | useEffect(() => { 13 | dispatch(getContacts()); 14 | }, [dispatch]); 15 | 16 | if (loading) { 17 | return ( 18 | <> 19 |
20 |

21 | Contact List 22 |

23 | 24 | Add Contact 25 | 26 |
27 |
28 | 29 |
30 | 31 | ); 32 | } 33 | 34 | if (error) { 35 | return ( 36 | <> 37 |
38 |

39 | Contact List 40 |

41 | 42 | Add Contact 43 | 44 |
45 |
46 | 47 | {error} 48 |
49 | 50 | ); 51 | } 52 | 53 | return ( 54 | <> 55 |
56 |

57 | Contact List 58 |

59 | 60 | Add Contact 61 | 62 |
63 | {contacts.length === 0 ? ( 64 |
65 | 66 |

No contacts found

67 |

Get started by adding your first contact!

68 | 69 | Add Your First Contact 70 | 71 |
72 | ) : ( 73 | contacts.map((contact) => ) 74 | )} 75 | 76 | ); 77 | } 78 | 79 | export default Contacts; 80 | -------------------------------------------------------------------------------- /src/actions/contactActions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { showSuccess, showError } from '../services/toastService'; 3 | 4 | export const getContacts = () => async (dispatch) => { 5 | try { 6 | dispatch({ type: 'GET_CONTACTS_START' }); 7 | const response = await axios.get('https://jsonplaceholder.typicode.com/users'); 8 | dispatch({ 9 | type: 'GET_CONTACTS_SUCCESS', 10 | payload: response.data, 11 | }); 12 | } catch (error) { 13 | dispatch({ 14 | type: 'GET_CONTACTS_ERROR', 15 | payload: error.message, 16 | }); 17 | showError('Failed to load contacts'); 18 | } 19 | }; 20 | 21 | export const getContact = (id) => async (dispatch) => { 22 | try { 23 | dispatch({ type: 'GET_CONTACT_START' }); 24 | const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`); 25 | dispatch({ 26 | type: 'GET_CONTACT_SUCCESS', 27 | payload: response.data, 28 | }); 29 | } catch (error) { 30 | dispatch({ 31 | type: 'GET_CONTACT_ERROR', 32 | payload: error.message, 33 | }); 34 | showError('Failed to load contact details'); 35 | } 36 | }; 37 | 38 | export const deleteContact = (id) => async (dispatch) => { 39 | try { 40 | dispatch({ type: 'DELETE_CONTACT_START' }); 41 | await axios.delete(`https://jsonplaceholder.typicode.com/users/${id}`); 42 | dispatch({ 43 | type: 'DELETE_CONTACT_SUCCESS', 44 | payload: id, 45 | }); 46 | showSuccess('Contact deleted successfully'); 47 | } catch (error) { 48 | dispatch({ 49 | type: 'DELETE_CONTACT_ERROR', 50 | payload: error.message, 51 | }); 52 | showError('Failed to delete contact'); 53 | } 54 | }; 55 | 56 | export const addContact = (contact) => async (dispatch) => { 57 | try { 58 | dispatch({ type: 'ADD_CONTACT_START' }); 59 | const response = await axios.post('https://jsonplaceholder.typicode.com/users/', contact); 60 | dispatch({ 61 | type: 'ADD_CONTACT_SUCCESS', 62 | payload: response.data, 63 | }); 64 | showSuccess('Contact added successfully'); 65 | } catch (error) { 66 | dispatch({ 67 | type: 'ADD_CONTACT_ERROR', 68 | payload: error.message, 69 | }); 70 | showError('Failed to add contact'); 71 | } 72 | }; 73 | 74 | export const updateContact = (contact) => async (dispatch) => { 75 | try { 76 | dispatch({ type: 'UPDATE_CONTACT_START' }); 77 | const response = await axios.put( 78 | `https://jsonplaceholder.typicode.com/users/${contact.id}`, 79 | contact 80 | ); 81 | dispatch({ 82 | type: 'UPDATE_CONTACT_SUCCESS', 83 | payload: response.data, 84 | }); 85 | showSuccess('Contact updated successfully'); 86 | } catch (error) { 87 | dispatch({ 88 | type: 'UPDATE_CONTACT_ERROR', 89 | payload: error.message, 90 | }); 91 | showError('Failed to update contact'); 92 | } 93 | }; 94 | 95 | export const clearError = () => ({ 96 | type: 'CLEAR_ERROR', 97 | }); 98 | -------------------------------------------------------------------------------- /src/components/contacts/AddContact.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import TextInputGroup from '../layout/TextInputGroup'; 5 | import LoadingSpinner from '../layout/LoadingSpinner'; 6 | import { addContact } from '../../actions/contactActions'; 7 | 8 | function AddContact() { 9 | const [name, setName] = useState(''); 10 | const [email, setEmail] = useState(''); 11 | const [phone, setPhone] = useState(''); 12 | const [errors, setErrors] = useState({}); 13 | 14 | const dispatch = useDispatch(); 15 | const navigate = useNavigate(); 16 | const { loading } = useSelector((state) => state.contact); 17 | 18 | const onSubmit = (e) => { 19 | e.preventDefault(); 20 | 21 | // Check For Errors 22 | if (name === '') { 23 | setErrors({ name: 'Name is required' }); 24 | return; 25 | } 26 | 27 | if (email === '') { 28 | setErrors({ email: 'Email is required' }); 29 | return; 30 | } 31 | 32 | if (phone === '') { 33 | setErrors({ phone: 'Phone is required' }); 34 | return; 35 | } 36 | 37 | const newContact = { 38 | name, 39 | email, 40 | phone, 41 | }; 42 | 43 | //// SUBMIT CONTACT //// 44 | dispatch(addContact(newContact)); 45 | 46 | // Clear State 47 | setName(''); 48 | setEmail(''); 49 | setPhone(''); 50 | setErrors({}); 51 | 52 | //Redirect to home 53 | navigate('/'); 54 | }; 55 | 56 | const onChange = (e) => { 57 | const { name, value } = e.target; 58 | if (name === 'name') setName(value); 59 | if (name === 'email') setEmail(value); 60 | if (name === 'phone') setPhone(value); 61 | }; 62 | 63 | return ( 64 |
65 |
66 |

Add Contact

67 |
68 |
69 |
70 | 78 | 87 | 95 |
96 | 108 |
109 | 110 |
111 |
112 | ); 113 | } 114 | 115 | export default AddContact; 116 | -------------------------------------------------------------------------------- /src/components/layout/ConfirmationModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | const ConfirmationModal = ({ 4 | show, 5 | onHide, 6 | onConfirm, 7 | title, 8 | message, 9 | confirmText = 'Delete', 10 | cancelText = 'Cancel', 11 | variant = 'danger', 12 | loading = false, 13 | }) => { 14 | useEffect(() => { 15 | const handleEscape = (e) => { 16 | if (e.key === 'Escape' && show) { 17 | onHide(); 18 | } 19 | }; 20 | 21 | if (show) { 22 | document.addEventListener('keydown', handleEscape); 23 | document.body.style.overflow = 'hidden'; 24 | } 25 | 26 | return () => { 27 | document.removeEventListener('keydown', handleEscape); 28 | document.body.style.overflow = 'unset'; 29 | }; 30 | }, [show, onHide]); 31 | 32 | if (!show) return null; 33 | 34 | const handleBackdropClick = (e) => { 35 | if (e.target === e.currentTarget) { 36 | onHide(); 37 | } 38 | }; 39 | 40 | const getIcon = () => { 41 | switch (variant) { 42 | case 'danger': 43 | return 'fa-exclamation-triangle'; 44 | case 'warning': 45 | return 'fa-exclamation-circle'; 46 | case 'info': 47 | return 'fa-info-circle'; 48 | default: 49 | return 'fa-question-circle'; 50 | } 51 | }; 52 | 53 | const getConfirmIcon = () => { 54 | switch (variant) { 55 | case 'danger': 56 | return 'fa-trash'; 57 | case 'warning': 58 | return 'fa-exclamation'; 59 | case 'info': 60 | return 'fa-check'; 61 | default: 62 | return 'fa-check'; 63 | } 64 | }; 65 | 66 | return ( 67 |
72 |
73 |
74 |
75 |
76 | 77 | {title} 78 |
79 | 86 |
87 |
88 |

{message}

89 |
90 |
91 | 94 | 112 |
113 |
114 |
115 |
116 | ); 117 | }; 118 | 119 | export default ConfirmationModal; 120 | -------------------------------------------------------------------------------- /src/components/contacts/EditContact.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import TextInputGroup from '../layout/TextInputGroup'; 3 | import LoadingSpinner from '../layout/LoadingSpinner'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { useParams, useNavigate } from 'react-router-dom'; 6 | import { getContact, updateContact } from '../../actions/contactActions'; 7 | 8 | function EditContact() { 9 | const [name, setName] = useState(''); 10 | const [email, setEmail] = useState(''); 11 | const [phone, setPhone] = useState(''); 12 | const [errors, setErrors] = useState({}); 13 | 14 | const dispatch = useDispatch(); 15 | const navigate = useNavigate(); 16 | const { id } = useParams(); 17 | const { contact, loading } = useSelector((state) => state.contact); 18 | 19 | useEffect(() => { 20 | dispatch(getContact(id)); 21 | }, [dispatch, id]); 22 | 23 | useEffect(() => { 24 | if (contact) { 25 | setName(contact.name || ''); 26 | setEmail(contact.email || ''); 27 | setPhone(contact.phone || ''); 28 | } 29 | }, [contact]); 30 | 31 | const onSubmit = (e) => { 32 | e.preventDefault(); 33 | 34 | // Check For Errors 35 | if (name === '') { 36 | setErrors({ name: 'Name is required' }); 37 | return; 38 | } 39 | 40 | if (email === '') { 41 | setErrors({ email: 'Email is required' }); 42 | return; 43 | } 44 | 45 | if (phone === '') { 46 | setErrors({ phone: 'Phone is required' }); 47 | return; 48 | } 49 | 50 | const updContact = { 51 | id, 52 | name, 53 | email, 54 | phone, 55 | }; 56 | 57 | dispatch(updateContact(updContact)); 58 | 59 | // Clear State 60 | setName(''); 61 | setEmail(''); 62 | setPhone(''); 63 | setErrors({}); 64 | 65 | navigate('/'); 66 | }; 67 | 68 | const onChange = (e) => { 69 | const { name, value } = e.target; 70 | if (name === 'name') setName(value); 71 | if (name === 'email') setEmail(value); 72 | if (name === 'phone') setPhone(value); 73 | }; 74 | 75 | return ( 76 |
77 |
78 |

Edit Contact

79 |
80 |
81 |
82 | 90 | 99 | 107 |
108 | 120 |
121 | 122 |
123 |
124 | ); 125 | } 126 | 127 | export default EditContact; 128 | -------------------------------------------------------------------------------- /src/components/contacts/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { deleteContact } from '../../actions/contactActions'; 5 | import ConfirmationModal from '../layout/ConfirmationModal'; 6 | 7 | function Contact({ contact }) { 8 | const [showContactInfo, setShowContactInfo] = useState(false); 9 | const [deleting, setDeleting] = useState(false); 10 | const [showDeleteModal, setShowDeleteModal] = useState(false); 11 | const dispatch = useDispatch(); 12 | const { loading } = useSelector((state) => state.contact); 13 | 14 | const { id, name, email, phone } = contact; 15 | 16 | const handleDeleteClick = () => { 17 | setShowDeleteModal(true); 18 | }; 19 | 20 | const handleDeleteConfirm = async () => { 21 | setShowDeleteModal(false); 22 | setDeleting(true); 23 | await dispatch(deleteContact(id)); 24 | setDeleting(false); 25 | }; 26 | 27 | const handleDeleteCancel = () => { 28 | setShowDeleteModal(false); 29 | }; 30 | 31 | return ( 32 | <> 33 |
34 |
35 |
36 |
37 | 38 | {name} 39 |
40 |
41 | 48 | 53 | 54 | 55 | 65 |
66 |
67 |
68 | {showContactInfo && ( 69 |
70 |
71 |
72 |
73 | 74 | Email: 75 | {email} 76 |
77 |
78 |
79 |
80 | 81 | Phone: 82 | {phone} 83 |
84 |
85 |
86 |
87 | )} 88 |
89 | 90 | 101 | 102 | ); 103 | } 104 | 105 | export default Contact; 106 | -------------------------------------------------------------------------------- /src/components/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function About() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | 11 |

Contact Manager

12 |

A modern, responsive contact management application

13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 |
Fast & Modern
21 |

22 | Built with React 18, Vite, and Bootstrap 5 for optimal performance and user 23 | experience. 24 |

25 |
26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
Responsive Design
34 |

35 | Fully responsive design that works perfectly on desktop, tablet, and mobile 36 | devices. 37 |

38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
State Management
47 |

48 | Powered by Redux for predictable state management and better data flow. 49 |

50 |
51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
Real-time Feedback
59 |

60 | Toast notifications and loading states provide instant feedback for all user 61 | actions. 62 |

63 |
64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 |
Features
72 |
73 |
74 | 75 | Add Contacts 76 |
77 |
78 | 79 | Edit Contacts 80 |
81 |
82 | 83 | Delete Contacts 84 |
85 |
86 | 87 | View Details 88 |
89 |
90 | 91 | Contact List 92 |
93 |
94 | 95 | Mobile Friendly 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 |

104 | Version: 2.0.0 105 |

106 |

107 | Built with: React, Redux, Vite, Bootstrap 5, React Router v6 108 |

109 |
110 |
111 |
112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contact Manager - Redux Learning Project 2 | 3 | A comprehensive React application designed to help you learn **Redux** state management through practical implementation. This project demonstrates modern Redux patterns, async operations, and best practices in a real-world application. 4 | 5 | ## 🎯 Why This Project for Learning Redux? 6 | 7 | This Contact Manager is specifically designed as a **Redux learning resource** that covers: 8 | 9 | - ✅ **Redux Fundamentals** - Actions, Reducers, Store, and Dispatch 10 | - ✅ **Async Operations** - Redux Thunk for API calls 11 | - ✅ **Modern Redux Patterns** - Loading states, error handling, and optimistic updates 12 | - ✅ **Real-world Scenarios** - CRUD operations with proper state management 13 | - ✅ **Best Practices** - Clean code structure and separation of concerns 14 | 15 | ## 🚀 Quick Start 16 | 17 | ```bash 18 | # Clone and install 19 | git clone https://github.com/devmahmud/ContactManager.git 20 | cd ContactManager 21 | npm install 22 | 23 | # Start learning 24 | npm run dev 25 | ``` 26 | 27 | ## 📚 Redux Learning Path 28 | 29 | ### 1. **Understanding the Store Structure** 30 | ```javascript 31 | // src/store.js - Redux store configuration 32 | { 33 | contact: { 34 | contacts: [], // All contacts 35 | contact: {}, // Single contact for editing 36 | loading: false, // Loading state for async operations 37 | error: null // Error handling 38 | } 39 | } 40 | ``` 41 | 42 | ### 2. **Actions - What Happens** 43 | ```javascript 44 | // src/actions/contactActions.js 45 | export const getContacts = () => async (dispatch) => { 46 | dispatch({ type: 'GET_CONTACTS_START' }); // Loading starts 47 | try { 48 | const response = await axios.get('/users'); 49 | dispatch({ type: 'GET_CONTACTS_SUCCESS', payload: response.data }); 50 | } catch (error) { 51 | dispatch({ type: 'GET_CONTACTS_ERROR', payload: error.message }); 52 | } 53 | }; 54 | ``` 55 | 56 | ### 3. **Reducers - How State Changes** 57 | ```javascript 58 | // src/reducers/contactReducer.js 59 | export default function (state = initialState, action) { 60 | switch (action.type) { 61 | case 'GET_CONTACTS_START': 62 | return { ...state, loading: true, error: null }; 63 | case 'GET_CONTACTS_SUCCESS': 64 | return { ...state, contacts: action.payload, loading: false }; 65 | case 'GET_CONTACTS_ERROR': 66 | return { ...state, loading: false, error: action.payload }; 67 | default: 68 | return state; 69 | } 70 | } 71 | ``` 72 | 73 | ### 4. **Components - How to Use Redux** 74 | ```javascript 75 | // Using Redux in React components 76 | import { useSelector, useDispatch } from 'react-redux'; 77 | 78 | function Contacts() { 79 | const dispatch = useDispatch(); 80 | const { contacts, loading, error } = useSelector(state => state.contact); 81 | 82 | useEffect(() => { 83 | dispatch(getContacts()); // Dispatch action 84 | }, [dispatch]); 85 | 86 | // Component renders based on Redux state 87 | } 88 | ``` 89 | 90 | ## 🛠️ Tech Stack & Learning Focus 91 | 92 | | Technology | Purpose | Learning Value | 93 | | ------------------- | ---------------- | ----------------------------------- | 94 | | **Redux 5.0.1** | State Management | Core Redux concepts and patterns | 95 | | **Redux Thunk** | Async Operations | Handling API calls and side effects | 96 | | **React 18.3.1** | UI Framework | Modern React with hooks | 97 | | **React Router v6** | Navigation | Client-side routing | 98 | | **Vite** | Build Tool | Modern development experience | 99 | 100 | ## 📁 Redux-Focused Project Structure 101 | 102 | ``` 103 | src/ 104 | ├── store.js # Redux store configuration 105 | ├── actions/ # Action creators (async operations) 106 | │ └── contactActions.js # CRUD operations with Redux Thunk 107 | ├── reducers/ # State reducers 108 | │ ├── contactReducer.js # Contact state management 109 | │ └── index.js # Root reducer 110 | └── components/ # React components using Redux 111 | ├── contacts/ # Contact CRUD components 112 | └── layout/ # Shared components 113 | ``` 114 | 115 | ## 🔄 Redux Patterns Demonstrated 116 | 117 | ### **1. Loading States** 118 | ```javascript 119 | // Actions dispatch loading states 120 | dispatch({ type: 'GET_CONTACTS_START' }); 121 | 122 | // Components show loading UI 123 | {loading && } 124 | ``` 125 | 126 | ### **2. Error Handling** 127 | ```javascript 128 | // Reducers handle errors 129 | case 'GET_CONTACTS_ERROR': 130 | return { ...state, loading: false, error: action.payload }; 131 | 132 | // Components display errors 133 | {error &&
{error}
} 134 | ``` 135 | 136 | ### **3. Optimistic Updates** 137 | ```javascript 138 | // Immediate UI updates with rollback on error 139 | case 'ADD_CONTACT_SUCCESS': 140 | return { ...state, contacts: [action.payload, ...state.contacts] }; 141 | ``` 142 | 143 | ### **4. Async Operations with Thunk** 144 | ```javascript 145 | // Redux Thunk for async actions 146 | export const addContact = (contact) => async (dispatch) => { 147 | dispatch({ type: 'ADD_CONTACT_START' }); 148 | try { 149 | const response = await axios.post('/users', contact); 150 | dispatch({ type: 'ADD_CONTACT_SUCCESS', payload: response.data }); 151 | } catch (error) { 152 | dispatch({ type: 'ADD_CONTACT_ERROR', payload: error.message }); 153 | } 154 | }; 155 | ``` 156 | 157 | ## 🎨 Features for Redux Learning 158 | 159 | ### **CRUD Operations** 160 | - ✅ **Create** - Add new contacts with Redux state updates 161 | - ✅ **Read** - Fetch and display contacts from Redux store 162 | - ✅ **Update** - Edit contacts with optimistic updates 163 | - ✅ **Delete** - Remove contacts with confirmation modal 164 | 165 | ### **State Management Patterns** 166 | - ✅ **Loading States** - Show spinners during async operations 167 | - ✅ **Error Handling** - Display errors from Redux state 168 | - ✅ **Form Management** - Controlled components with Redux 169 | - ✅ **Navigation** - Route-based state management 170 | 171 | ### **Modern Redux Features** 172 | - ✅ **Redux Thunk** - Async action creators 173 | - ✅ **useSelector/useDispatch** - Modern React-Redux hooks 174 | - ✅ **Immutable Updates** - Proper state immutability 175 | - ✅ **Action Types** - Consistent action naming 176 | 177 | ## 🚀 Available Scripts 178 | 179 | ```bash 180 | npm run dev # Start development server 181 | npm run build # Build for production 182 | npm run preview # Preview production build 183 | npm run deploy # Deploy to GitHub Pages 184 | ``` 185 | 186 | ## 🌐 Deployment 187 | 188 | ### GitHub Pages 189 | The application is configured for automatic deployment to GitHub Pages: 190 | 191 | ```bash 192 | npm run deploy 193 | ``` 194 | 195 | **Live Demo**: [https://devmahmud.github.io/ContactManager](https://devmahmud.github.io/ContactManager) 196 | 197 | ### Deployment Features 198 | - ✅ **Automatic Build** - Builds before deployment 199 | - ✅ **Client-side Routing** - 404.html handles React Router 200 | - ✅ **Asset Optimization** - Proper base path configuration 201 | - ✅ **Jekyll Bypass** - .nojekyll file prevents Jekyll processing 202 | 203 | ## 🌐 API Integration 204 | 205 | Uses JSONPlaceholder API for realistic data: 206 | - **GET** `/users` - Fetch all contacts 207 | - **POST** `/users` - Create new contact 208 | - **PUT** `/users/:id` - Update contact 209 | - **DELETE** `/users/:id` - Delete contact 210 | 211 | ## 📖 Learning Resources 212 | 213 | ### **Redux Concepts Covered** 214 | 1. **Store** - Single source of truth 215 | 2. **Actions** - Plain objects describing what happened 216 | 3. **Reducers** - Pure functions that specify how state changes 217 | 4. **Dispatch** - Method to trigger state changes 218 | 5. **Selectors** - Functions to extract data from state 219 | 220 | ### **Advanced Patterns** 221 | 1. **Redux Thunk** - Middleware for async operations 222 | 2. **Loading States** - Managing async operation states 223 | 3. **Error Boundaries** - Handling and displaying errors 224 | 4. **Optimistic Updates** - Immediate UI feedback 225 | 5. **State Normalization** - Efficient data structure 226 | 227 | ## 🎯 Learning Objectives 228 | 229 | After studying this project, you'll understand: 230 | 231 | - ✅ How to structure Redux applications 232 | - ✅ How to handle async operations with Redux Thunk 233 | - ✅ How to manage loading and error states 234 | - ✅ How to integrate Redux with React components 235 | - ✅ How to implement CRUD operations with Redux 236 | - ✅ How to write clean, maintainable Redux code 237 | 238 | ## 🤝 Contributing 239 | 240 | This is a learning project! Feel free to: 241 | - Add new Redux patterns 242 | - Improve error handling 243 | - Add more complex state management scenarios 244 | - Create additional learning examples 245 | 246 | ## 📝 License 247 | 248 | MIT License - Feel free to use this for learning and teaching Redux! 249 | 250 | --- 251 | 252 | **Start your Redux journey today! 🚀** 253 | 254 | *This project is designed to be a comprehensive learning resource for Redux. Each component and pattern is implemented with educational value in mind.* --------------------------------------------------------------------------------