├── 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 |
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 |
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 |
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.*
--------------------------------------------------------------------------------