diff --git a/package-lock.json b/package-lock.json
index a25baa5..6739593 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
- "name": "goit-fs91-react-hw-03-phonebook",
+ "name": "goit-fs91-react-hw-04-phonebook",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "goit-fs91-react-hw-03-phonebook",
+ "name": "goit-fs91-react-hw-04-phonebook",
"version": "0.1.0",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/material": "^5.14.18",
+ "@reduxjs/toolkit": "^2.0.1",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
@@ -20,8 +21,10 @@
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-icons": "^4.11.0",
+ "react-redux": "^9.0.4",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.3",
+ "redux-persist": "^6.0.0",
"shortid": "^2.2.16",
"web-vitals": "^2.1.3"
},
@@ -3114,6 +3117,38 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz",
+ "integrity": "sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.0",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
+ "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3748,9 +3783,9 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/react": {
- "version": "17.0.43",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.43.tgz",
- "integrity": "sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==",
+ "version": "18.2.43",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.43.tgz",
+ "integrity": "sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -3834,6 +3869,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -12203,6 +12243,32 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-redux": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.0.4.tgz",
+ "integrity": "sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25",
+ "react": "^18.0",
+ "react-native": ">=0.69",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -12357,6 +12423,27 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0.tgz",
+ "integrity": "sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA=="
+ },
+ "node_modules/redux-persist": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
+ "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
+ "peerDependencies": {
+ "redux": ">4.0.0"
+ }
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -12499,6 +12586,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
+ "node_modules/reselect": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.1.tgz",
+ "integrity": "sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg=="
+ },
"node_modules/resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@@ -14026,6 +14118,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17077,6 +17177,24 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
},
+ "@reduxjs/toolkit": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz",
+ "integrity": "sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==",
+ "requires": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.0",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.0.1"
+ },
+ "dependencies": {
+ "immer": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
+ "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A=="
+ }
+ }
+ },
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -17558,9 +17676,9 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"@types/react": {
- "version": "17.0.43",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.43.tgz",
- "integrity": "sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==",
+ "version": "18.2.43",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.43.tgz",
+ "integrity": "sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -17644,6 +17762,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
},
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -23601,6 +23724,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "react-redux": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.0.4.tgz",
+ "integrity": "sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==",
+ "requires": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ }
+ },
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -23715,6 +23847,23 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0.tgz",
+ "integrity": "sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA=="
+ },
+ "redux-persist": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
+ "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
+ "requires": {}
+ },
+ "redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "requires": {}
+ },
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -23826,6 +23975,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
+ "reselect": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.1.tgz",
+ "integrity": "sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg=="
+ },
"resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@@ -24955,6 +25109,12 @@
"requires-port": "^1.0.0"
}
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 14756cb..0e2e668 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,14 @@
{
- "name": "goit-fs91-react-hw-04-phonebook",
+ "name": "goit-fs91-react-hw-06-phonebook",
"version": "0.1.0",
"private": true,
- "homepage": "https://kharchenkok.github.io/goit-fs91-react-hw-04-phonebook/",
+ "homepage": "https://kharchenkok.github.io/goit-fs91-react-hw-06-phonebook/",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/material": "^5.14.18",
+ "@reduxjs/toolkit": "^2.0.1",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
@@ -16,8 +17,10 @@
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-icons": "^4.11.0",
+ "react-redux": "^9.0.4",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.3",
+ "redux-persist": "^6.0.0",
"shortid": "^2.2.16",
"web-vitals": "^2.1.3"
},
diff --git a/src/App.js b/src/App.js
index eb45455..bbd3941 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,86 +1,41 @@
-import { useState } from 'react';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
-import shortid from 'shortid';
+
import ContactForm from './components/ContactForm';
import ContactList from './components/ContactList';
-import Filter from './components/Filter';
+import NameFilter from './components/Filter';
import Section from './components/Section';
-import Notification from './components/Notification';
-import useLocalStorage from './hooks/useLocalStorage';
-const initialContacts = [
- { id: 'id-1', name: 'Rosie Simpson', number: '459-12-56' },
- { id: 'id-2', name: 'Hermione Kline', number: '443-89-12' },
- { id: 'id-3', name: 'Eden Clements', number: '645-17-79' },
- { id: 'id-4', name: 'Annie Copeland', number: '227-91-26' },
-];
+import PhoneIcon from './images/phonebook.png';
function App() {
- const [contacts, setContacts] = useLocalStorage('contacts', initialContacts);
- const [filter, setFilter] = useState('');
-
- const addContact = ({ name, number }) => {
- const contact = {
- id: shortid.generate(),
- name,
- number,
- };
- setContacts(prevContacts => [contact, ...prevContacts]);
- };
-
- const deleteContact = contactId => {
- setContacts(prevContacts =>
- prevContacts.filter(contact => contact.id !== contactId)
- );
- };
-
- const changeFilter = e => {
- setFilter(e.currentTarget.value);
- };
-
- const getVisibleContacts = () => {
- const normalizedFilter = filter.toLowerCase();
- return contacts.filter(contact =>
- contact.name.toLowerCase().includes(normalizedFilter)
- );
- };
-
- const isContactExist = name => {
- return contacts.some(
- contact => contact.name.toLowerCase() === name.toLowerCase()
- );
- };
-
- const visibleContacts = getVisibleContacts();
return (
-
+ <>
-
-
- {contacts.length > 0 ? (
- <>
-
- {visibleContacts.length > 0 ? (
-
- ) : (
-
- )}
- >
- ) : (
-
- )}
-
-
+
+
+
+
+ Phonebook
+
+
+
+
+
+
+ >
);
}
diff --git a/src/components/ContactForm/ContactForm.js b/src/components/ContactForm/ContactForm.js
index 41fdfbc..2612df7 100644
--- a/src/components/ContactForm/ContactForm.js
+++ b/src/components/ContactForm/ContactForm.js
@@ -1,193 +1,88 @@
-import React, { useReducer } from 'react';
-import { showError, showWarning } from '../../utils/ToastNotification';
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { addContactAction } from '../../store/contacts/contactsSlice';
+import {
+ showError,
+ showSuccess,
+ showWarning,
+} from '../../utils/ToastNotification';
+import {
+ colorPickerOptions,
+ DEFAULT_COLOR,
+} from '../../constans/ColorConstans';
+import style from './ContactForm.module.css';
-// ============== useReducer ====================================================================
-const formReducer = (state, action) => {
- switch (action.type) {
- case 'CHANGE':
- return { ...state, [action.field]: action.value };
- case 'RESET':
- return { name: '', number: '' };
- default:
- return state;
- }
-};
-
-export default function ContactForm({ isContactExist, onFormSubmit }) {
- const [formData, dispatch] = useReducer(formReducer, {
- name: '',
- number: '',
- });
-
- const handleChange = event => {
- const { name, value } = event.target;
- dispatch({ type: 'CHANGE', field: name, value });
- };
+export default function ContactForm() {
+ const dispatch = useDispatch();
+ const { contacts } = useSelector(state => state.contacts);
const handleSubmit = event => {
event.preventDefault();
+ const { name, number, color } = event.target;
+ const existingContact = contacts.find(
+ contact => contact.name.toLowerCase() === name.value.toLowerCase()
+ );
- const { name, number } = formData;
-
- if (name.trim() === '' || number.trim() === '') {
+ if (!name.value.trim() || !number.value.trim()) {
showError('Make sure all fields are completed!');
return;
}
- if (isContactExist(name)) {
- showWarning(`${name} is already in contacts!`);
+ if (existingContact) {
+ showWarning(`${name.value} is already in contacts!`);
return;
}
- onFormSubmit({ name, number });
- dispatch({ type: 'RESET' });
+ const selectedColor = color.value || DEFAULT_COLOR;
+
+ dispatch(
+ addContactAction({
+ name: name.value,
+ number: number.value,
+ color: selectedColor,
+ })
+ );
+ showSuccess(`${name.value} added to contacts!`);
+
+ event.target.reset();
};
return (
-
);
}
-
-// ============== useState name and number ====================================================================
-
-// export default function ContactForm({ isContactExist, onFormSubmit }) {
-// const [name, setName] = useState('');
-// const [number, setNumber] = useState('');
-//
-// const handleSubmit = event => {
-// event.preventDefault();
-//
-// if (name.trim() === '' || number.trim() === '') {
-// showError('Make sure all fields are completed!');
-// return;
-// }
-//
-// if (isContactExist(name)) {
-// showWarning(`${name} is already in contacts!`);
-// return;
-// }
-//
-// onFormSubmit({ name, number });
-// setName('');
-// setNumber('');
-// };
-//
-// // const handleNameChange = event => {
-// // setName(event.currentTarget.value);
-// // };
-// //
-// // const handleNumberChange = event => {
-// // setNumber(event.currentTarget.value);
-// // };
-//
-// return (
-//
-// );
-// }
-
-// ============== useState FormData ====================================================================
-
-// export default function ContactForm({ isContactExist, onFormSubmit }) {
-// const [formData, setFormData] = useState({ name: '', number: '' });
-//
-// const handleSubmit = event => {
-// event.preventDefault();
-//
-// const { name, number } = formData;
-//
-// if (name.trim() === '' || number.trim() === '') {
-// showError('Make sure all fields are completed!');
-// return;
-// }
-//
-// if (isContactExist(name)) {
-// showWarning(`${name} is already in contacts!`);
-// return;
-// }
-//
-// onFormSubmit({ name, number });
-// setFormData({ name: '', number: '' });
-// };
-// const handleFormChange = event => {
-// const { name, value } = event.currentTarget;
-// setFormData(prevFormData => ({ ...prevFormData, [name]: value }));
-// };
-// return (
-//
-// );
-// }
diff --git a/src/components/ContactForm/ContactForm.module.css b/src/components/ContactForm/ContactForm.module.css
new file mode 100644
index 0000000..bcc58b7
--- /dev/null
+++ b/src/components/ContactForm/ContactForm.module.css
@@ -0,0 +1,89 @@
+.form {
+ padding: 40px 20px 20px;
+ position: relative;
+ display: grid;
+ gap: 20px;
+ justify-content: center;
+ width: 100%;
+ max-width: 600px;
+ color: #2a5d8a;
+ font-weight: 700;
+ font-size: 22px;
+ background-color: white;
+ border-radius: 16px 16px 0px 16px;
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+}
+.formLabel {
+ position: relative;
+}
+
+.formInput {
+ padding: 10px 20px;
+ border: 1px solid #98d8ff;
+ border-radius: 16px;
+}
+.formInput::placeholder {
+ font-weight: 500;
+ font-size: 18px;
+ color: #ccc;
+ font-style: italic;
+}
+
+.form button[type='submit'] {
+ position: absolute;
+ right: 0;
+ top: 100%;
+ background-color: white;
+ color: #98d8ff;
+ border: none;
+ padding: 16px;
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+ font-size: 24px;
+ font-weight: 700;
+ cursor: pointer;
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 10px 15px;
+ transition: all 250ms ease-in-out;
+}
+
+.form button[type='submit']:hover {
+ background-color: #98d8ff;
+ color: white;
+}
+
+.formRadioWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ gap: 8px;
+}
+
+.formRadio {
+ opacity: 0;
+ visibility: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 0;
+}
+
+.formRadio:checked + .formRadioColor {
+ border: 2px solid slategrey;
+ transform: scale(1.1);
+}
+.formRadio:hover + .formRadioColor {
+ transform: scale(1.1);
+}
+
+.formRadioColor {
+ display: flex;
+ width: 50px;
+ height: 50px;
+ border: 1px solid currentColor;
+ background-color: currentColor;
+ border-radius: 16px;
+ cursor: pointer;
+ transition: all 200ms linear;
+}
diff --git a/src/components/ContactList/ContactList.jsx b/src/components/ContactList/ContactList.jsx
index a5e20ec..b205730 100644
--- a/src/components/ContactList/ContactList.jsx
+++ b/src/components/ContactList/ContactList.jsx
@@ -1,30 +1,59 @@
import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { deleteContactAction } from '../../store/contacts/contactsSlice';
import { BiSolidUserRectangle, BiPhone } from 'react-icons/bi';
+import ColorFilter from '../Filter/ColorFilter';
+import Notification from '../Notification';
+import { showInfo } from '../../utils/ToastNotification';
import style from './ContactList.module.css';
-const ContactList = ({ data, onDeleteContact }) => {
+const ContactList = () => {
+ const dispatch = useDispatch();
+ const { contacts } = useSelector(state => state.contacts);
+ const { nameFilter, colorFilter } = useSelector(state => state.filter);
+
+ const filteredContacts = contacts.filter(
+ contact =>
+ contact.name.toLowerCase().includes(nameFilter.toLowerCase()) &&
+ (colorFilter === '' || contact.color === colorFilter)
+ );
+
+ const handleDelete = (id, name) => {
+ dispatch(deleteContactAction(id));
+ showInfo(`Contact ${name} deleted`);
+ };
return (
-
- {data.map(({ id, name, number }) => (
- -
-
-
- {name}
-
-
-
- {number}
-
-
-
- ))}
-
+
+ {contacts.length > 0 &&
}
+ {filteredContacts.length > 0 && contacts.length > 0 ? (
+
+ {filteredContacts.map(({ id, name, number, color }) => (
+ -
+
+
+ {name}
+
+
+
+ {number}
+
+
+
+ ))}
+
+ ) : (
+
+ )}
+
);
};
export default ContactList;
diff --git a/src/components/ContactList/ContactList.module.css b/src/components/ContactList/ContactList.module.css
index a6d9f15..c7771be 100644
--- a/src/components/ContactList/ContactList.module.css
+++ b/src/components/ContactList/ContactList.module.css
@@ -1,51 +1,72 @@
-.contact__list {
- margin: 0 auto;
- padding: 40px;
+.contactList {
+ padding: 0 40px;
display: grid;
-
- max-width: 800px;
width: 100%;
- border: 1px solid #ccc;
- border-radius: 4px;
+ border-radius:16px;
+
}
-.contact__item {
+.contactItem {
+ position: relative;
display: grid;
gap: 20px;
grid-template-columns: 1fr 1fr 0.7fr;
align-items: center;
- border-radius: 4px;
+ /*border-radius: 16px;*/
background-color: white;
padding: 10px 20px;
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+}
+.contactItem:first-child {
+ border-top-left-radius: 16px;
+ border-top-right-radius: 16px;
+}
+.contactItem:first-child .contactDelButton{
+ border-top-right-radius: 16px;
+}
+.contactItem:last-child {
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+}
+.contactItem:last-child .contactDelButton{
+ border-bottom-right-radius: 16px;
}
-.contact__item:not(:last-child) {
+.contactItem:not(:last-child) {
border-bottom: 1px dashed #ccc;
}
-.contact__item p{
- display: flex;
- gap: 6px;
- align-items: center;
+.nameIcon {
+ width: 30px;
+ height: 30px;
+}
+
+.contactItem p {
+ display: flex;
+ gap: 6px;
+ align-items: center;
}
-.contact__item svg{
+.contactItem svg {
color: #2a5d8a;
}
-.contact__button {
- color: white;
- font-weight: 700;
- font-size: 16px;
- background-color: #F85771;
- border: 2px solid #F85771;
- padding: 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 250ms ease-in-out;
+.contactDelButton {
+ padding: 8px 20px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ color: #f85771;
+ font-weight: 700;
+ font-size: 16px;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ transition: all 250ms ease-in-out;
}
-.contact__button:hover {
- background-color: white;
- color: #F85771;
+.contactDelButton:hover {
+ background-color: #f85771;
+ color: white;
}
diff --git a/src/components/Filter/ColorFilter.js b/src/components/Filter/ColorFilter.js
new file mode 100644
index 0000000..692771a
--- /dev/null
+++ b/src/components/Filter/ColorFilter.js
@@ -0,0 +1,72 @@
+import { useDispatch, useSelector } from 'react-redux';
+import React, { useState } from 'react';
+import { setColorFilterAction } from '../../store/filter/filterSlice';
+import { DEFAULT_COLOR } from '../../constans/ColorConstans';
+
+import style from './Filter.module.css';
+
+const ColorFilter = () => {
+ const dispatch = useDispatch();
+ const [activeColor, setActiveColor] = useState('');
+
+ const { contacts } = useSelector(state => state.contacts);
+
+ const uniqueColors = [...new Set(contacts.map(contact => contact.color))];
+
+ const isDefaultColorContacts = contacts.some(
+ contact => contact.color === DEFAULT_COLOR
+ );
+
+ const makeActiveButtonClass = color => {
+ const buttonClass = [style.colorFilterButton];
+ activeColor === color && buttonClass.push(style.active);
+ return buttonClass.join(' ');
+ };
+
+ const handleColorFilter = event => {
+ const selectedColor = event.currentTarget.value;
+ setActiveColor(selectedColor);
+ dispatch(setColorFilterAction(selectedColor));
+ };
+
+ return (
+
+
+
+ {uniqueColors.map(
+ (color, index) =>
+ color !== DEFAULT_COLOR && (
+
+ )
+ )}
+ {isDefaultColorContacts && (
+
+ )}
+
+ );
+};
+
+export default ColorFilter;
diff --git a/src/components/Filter/Filter.jsx b/src/components/Filter/Filter.jsx
deleted file mode 100644
index adfc09c..0000000
--- a/src/components/Filter/Filter.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import style from './Filter.module.css';
-
-const Filter = ({ value, onChange }) => (
-
-);
-
-export default Filter;
diff --git a/src/components/Filter/Filter.module.css b/src/components/Filter/Filter.module.css
index f964c1d..e8d6f4b 100644
--- a/src/components/Filter/Filter.module.css
+++ b/src/components/Filter/Filter.module.css
@@ -1,5 +1,46 @@
-.filter{
- max-width: 800px;
- width: 100%;
- margin: 0 auto;
+.filter {
+ max-width: calc(100% - 80px);
+ width: 100%;
+ margin: 0 auto;
+ padding: 10px 20px;
+ border: 1px solid #98d8ff;
+ border-radius: 16px;
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+}
+
+.filter::placeholder {
+ font-weight: 500;
+ font-size: 18px;
+ color: #ccc;
+ font-style: italic;
+}
+
+.colorFilter {
+ padding-left: 12px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ max-width: calc(100% - 110px);
+ width: 100%;
+}
+
+.colorFilterButton {
+ margin-left: -10px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ height: 40px;
+ border: none;
+ cursor: pointer;
+ border-top-left-radius: 16px;
+ transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.active {
+ border-top-right-radius: 16px;
+ transform: scale(1.1);
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
+
}
diff --git a/src/components/Filter/NameFilter.jsx b/src/components/Filter/NameFilter.jsx
new file mode 100644
index 0000000..80e4986
--- /dev/null
+++ b/src/components/Filter/NameFilter.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { setNameFilterAction } from '../../store/filter/filterSlice';
+import style from './Filter.module.css';
+
+const NameFilter = () => {
+ const dispatch = useDispatch();
+ const { contacts } = useSelector(state => state.contacts);
+ const { nameFilter } = useSelector(state => state.filter);
+
+ const handleChange = event => {
+ dispatch(setNameFilterAction(event.target.value));
+ };
+
+ return (
+ <>
+ {contacts.length > 0 && (
+
+ )}
+ >
+ );
+};
+
+export default NameFilter;
diff --git a/src/components/Filter/index.js b/src/components/Filter/index.js
index d484ed3..2040eae 100644
--- a/src/components/Filter/index.js
+++ b/src/components/Filter/index.js
@@ -1 +1 @@
-export { default } from './Filter.jsx';
+export { default } from './NameFilter.jsx';
diff --git a/src/components/Notification/Notification.module.css b/src/components/Notification/Notification.module.css
index ab2599d..c8917f1 100644
--- a/src/components/Notification/Notification.module.css
+++ b/src/components/Notification/Notification.module.css
@@ -1,15 +1,16 @@
-.notification{
- margin: 0 auto;
- padding: 40px;
- width: 100%;
- max-width: 800px;
- min-height: 350px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #C6455A;
- font-weight: 700;
- font-size: 26px;
- border: 1px solid #ccc;
- border-radius: 4px;
+.notification {
+ margin: 0 auto;
+ padding: 40px;
+ width: 100%;
+ max-width: calc(100% - 80px);
+ min-height: 350px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #c6455a;
+ font-weight: 700;
+ font-size: 26px;
+
+ border-radius: 16px;
+ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
diff --git a/src/components/Section/Section.module.css b/src/components/Section/Section.module.css
index a6f5ed4..b66746c 100644
--- a/src/components/Section/Section.module.css
+++ b/src/components/Section/Section.module.css
@@ -1,25 +1,11 @@
.section {
- padding: 20px 0;
+ padding: 40px 0;
display: grid;
gap: 20px;
}
.section__title {
- font-size: 64px;
- font-weight: 700;
- text-align: center;
- text-shadow:
- -1px -1px #fff,
- -2px -2px #fff,
- -1px 1px #fff,
- -2px 2px #fff,
- 1px 1px #fff,
- 2px 2px #fff,
- 1px -1px #fff,
- 2px -2px #fff,
- -3px -3px 2px #bbb,
- -3px 3px 2px #bbb,
- 3px 3px 2px #bbb,
- 3px -3px 2px #bbb;
- color: steelblue;
+ color: #331200;
+ line-height:40px;
+
}
diff --git a/src/constans/ColorConstans.js b/src/constans/ColorConstans.js
new file mode 100644
index 0000000..718efd9
--- /dev/null
+++ b/src/constans/ColorConstans.js
@@ -0,0 +1,8 @@
+export const colorPickerOptions = [
+ { label: 'yellow', color: '#faae20' },
+ { label: 'green', color: '#4CAF50' },
+ { label: 'blue', color: '#2196F3' },
+ { label: 'pink', color: '#E91E63' },
+];
+
+export const DEFAULT_COLOR = '#008080';
diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js
deleted file mode 100644
index 46cd7f7..0000000
--- a/src/hooks/useLocalStorage.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useEffect, useState } from 'react';
-
-const useLocalStorage = (key, defaultValue) => {
- const [state, setState] = useState(
- () => JSON.parse(window.localStorage.getItem(key)) ?? defaultValue,
- );
- useEffect(() => {
- window.localStorage.setItem(key, JSON.stringify(state));
- }, [key, state]);
-
- return [state, setState];
-};
-
-export default useLocalStorage;
diff --git a/src/images/phonebook.png b/src/images/phonebook.png
new file mode 100644
index 0000000..2bcf794
Binary files /dev/null and b/src/images/phonebook.png differ
diff --git a/src/index.css b/src/index.css
index 895c0e0..7680035 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,13 +1,11 @@
-
-
body {
margin: 0;
+ background-color: #fedecc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-
}
code {
@@ -15,67 +13,43 @@ code {
monospace;
}
-p, h1, h2, ul {
+p,
+h1,
+h2,
+ul {
margin: 0;
- padding: 0;
+ padding: 0;
}
-ul{
- list-style: none;
+ul {
+ list-style: none;
}
.container {
+ width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
-.form {
- margin: 0 auto;
- padding: 20px;
- display: grid;
- gap: 20px;
- justify-content: center;
- width: 100%;
- max-width: 600px;
- color: #2a5d8a;
- font-weight: 700;
- font-size: 22px;
- border: 1px solid #ccc;
- border-radius: 4px;
-}
-.form__label {
- display: grid;
- grid-template-columns: 0.5fr 1fr;
- gap: 10px;
- align-items: center;
- color: #2a5d8a;
- font-weight: 700;
- font-size: 22px;
-}
+/*.active{*/
+/* border: 2px solid red;*/
+/*}*/
-.form__input {
- padding: 10px;
- border: 1px solid #ccc;
- border-radius: 4px;
+.wrapper {
+ display: grid;
+ gap: 20px;
+ grid-template-columns: 1fr 0.6fr;
+ align-items: start;
}
-.form button{
- background-color: #2a5d8a;
- color: white;
- border: 2px solid #2a5d8a;
- padding: 16px;
- border-radius: 4px;
- font-size: 24px;
- font-weight: 700;
- cursor: pointer;
- transition: all 250ms ease-in-out;
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 0;
}
-
-.form button:hover{
- background-color: white;
- color: #2a5d8a;
+.pageTitle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
}
-
-
-
-
diff --git a/src/index.js b/src/index.js
index f7996c3..edd3429 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,11 +1,18 @@
import React from 'react';
+import { Provider } from 'react-redux';
import ReactDOM from 'react-dom/client';
import App from 'App';
import 'modern-normalize/modern-normalize.css';
import './index.css';
+import { persistor, store } from './store/store';
+import { PersistGate } from 'redux-persist/integration/react';
ReactDOM.createRoot(document.getElementById('root')).render(
-
+
+ Loader...} persistor={persistor}>
+
+
+
);
diff --git a/src/store/contacts/contactsSlice.js b/src/store/contacts/contactsSlice.js
new file mode 100644
index 0000000..bbfdbd1
--- /dev/null
+++ b/src/store/contacts/contactsSlice.js
@@ -0,0 +1,59 @@
+import { createSlice, nanoid } from '@reduxjs/toolkit';
+
+const contactsSlice = createSlice({
+ name: 'contacts',
+ initialState: {
+ contacts: [
+ {
+ id: 'id-1',
+ name: 'Rosie Simpson',
+ number: '459-12-56',
+ color: '#faae20',
+ },
+ {
+ id: 'id-2',
+ name: 'Hermione Kline',
+ number: '443-89-12',
+ color: '#4CAF50',
+ },
+ {
+ id: 'id-3',
+ name: 'Eden Clements',
+ number: '645-17-79',
+ color: '#2196F3',
+ },
+ {
+ id: 'id-4',
+ name: 'Annie Copeland',
+ number: '227-91-26',
+ color: '#E91E63',
+ },
+ ],
+ },
+ reducers: {
+ addContactAction: {
+ prepare: contact => {
+ return {
+ payload: {
+ ...contact,
+ id: nanoid(),
+ },
+ };
+ },
+ reducer: (state, action) => {
+ return {
+ ...state,
+ contacts: [action.payload, ...state.contacts],
+ };
+ },
+ },
+ deleteContactAction: (state, action) => {
+ state.contacts = state.contacts.filter(
+ contact => contact.id !== action.payload
+ );
+ },
+ },
+});
+
+export const { addContactAction, deleteContactAction } = contactsSlice.actions;
+export const contactsReducer = contactsSlice.reducer;
diff --git a/src/store/filter/filterSlice.js b/src/store/filter/filterSlice.js
new file mode 100644
index 0000000..48c0d77
--- /dev/null
+++ b/src/store/filter/filterSlice.js
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const filterSlice = createSlice({
+ name: 'filter',
+ initialState: {
+ nameFilter: '',
+ colorFilter: '',
+ },
+ reducers: {
+ setNameFilterAction: (state, action) => {
+ state.nameFilter = action.payload;
+ },
+ setColorFilterAction: (state, action) => {
+ state.colorFilter = action.payload;
+ },
+ },
+});
+
+export const { setNameFilterAction, setColorFilterAction } =
+ filterSlice.actions;
+export const filterReducer = filterSlice.reducer;
diff --git a/src/store/reducer.js b/src/store/reducer.js
new file mode 100644
index 0000000..382ba16
--- /dev/null
+++ b/src/store/reducer.js
@@ -0,0 +1,16 @@
+import storage from 'redux-persist/lib/storage';
+import { persistReducer } from 'redux-persist';
+import { contactsReducer } from './contacts/contactsSlice';
+import { filterReducer } from './filter/filterSlice';
+
+const persistConfig = {
+ key: 'contacts',
+ storage,
+ whitelist: ['contacts'],
+};
+const persistedReducer = persistReducer(persistConfig, contactsReducer);
+
+export const reducer = {
+ contacts: persistedReducer,
+ filter: filterReducer,
+};
diff --git a/src/store/store.js b/src/store/store.js
new file mode 100644
index 0000000..7221f4a
--- /dev/null
+++ b/src/store/store.js
@@ -0,0 +1,23 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { reducer } from './reducer';
+import {
+ persistStore,
+ FLUSH,
+ PAUSE,
+ PERSIST,
+ PURGE,
+ REGISTER,
+ REHYDRATE,
+} from 'redux-persist';
+
+export const store = configureStore({
+ reducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: {
+ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
+ },
+ }),
+});
+
+export const persistor = persistStore(store);
diff --git a/src/utils/ToastNotification.js b/src/utils/ToastNotification.js
index e7bc770..6e45237 100644
--- a/src/utils/ToastNotification.js
+++ b/src/utils/ToastNotification.js
@@ -25,3 +25,27 @@ export function showWarning(message) {
theme: 'colored',
});
}
+export function showSuccess(message) {
+ toast.success(message, {
+ position: 'top-center',
+ autoClose: 1000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'colored',
+ });
+}
+export function showInfo(message) {
+ toast.info(message, {
+ position: 'top-center',
+ autoClose: 1000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: 'colored',
+ });
+}