diff --git a/package.json b/package.json index 5bdac3e7..a1e69871 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,11 @@ "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^12.8.3", + "bootstrap": "^4.6.0", + "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "web-vitals": "^1.1.1" }, @@ -19,8 +22,7 @@ }, "eslintConfig": { "extends": [ - "react-app", - "react-app/jest" + "wesbos" ] }, "browserslist": { @@ -34,5 +36,19 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "babel-eslint": "^10.1.0", + "eslint": "^7.8.1", + "eslint-config-airbnb": "^18.2.0", + "eslint-config-prettier": "^6.11.0", + "eslint-config-wesbos": "^1.0.1", + "eslint-plugin-html": "^6.1.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.20.6", + "eslint-plugin-react-hooks": "^4.1.2", + "prettier": "^2.1.1" } } diff --git a/src/App.css b/src/App.css index 74b5e053..c061a9f4 100644 --- a/src/App.css +++ b/src/App.css @@ -2,37 +2,7 @@ text-align: center; } -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +img { + width: 100%; + max-width: 200px; } diff --git a/src/App.js b/src/App.js index 37845757..b052feeb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,25 +1,79 @@ -import logo from './logo.svg'; import './App.css'; +import { Switch, Route, Redirect } from 'react-router-dom'; +import { useState } from 'react'; +import Home from './components/Home'; +import IndividualContact from './components/IndividualContact'; +import NewContact from './components/NewContact'; +import EditContact from './components/EditContact'; +import Header from './components/Header'; -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
+const Header = () => ( +
+

Your Contact List

+
+

Your Contact List

+
+); + +const App = () => { + const [contacts, setContacts] = useState([]); + + const addContact = (newContact) => { + setContacts(contacts.concat([newContact])); + }; + + const editContact = (newContactInfo) => { + setContacts((prevState) => + [...prevState].map((c) => + c.id === newContactInfo.id ? newContactInfo : c + ) + ); + }; + + const deleteContact = (id) => { + setContacts((prevState) => + [...prevState].filter((c) => c.id !== parseInt(id)) + ); + }; + + + return ( +
+
+ + ( + + )} + /> + } + /> + ( + + )} + /> + ( + + )} + /> + + +
); -} +}; -export default App; +export default App; \ No newline at end of file diff --git a/src/components/Contact.js b/src/components/Contact.js new file mode 100644 index 00000000..654eb903 --- /dev/null +++ b/src/components/Contact.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link, Redirect } from 'react-router-dom'; + +const Contact = ({ contact, deleteContact }) => { + const [redirect, setRedirect] = useState(false); + + const handleContactClick = () => { + setRedirect(true); + }; + + const handleDeleteButtonClick = (e) => { + e.stopPropagation(); + deleteContact(contact.id); + }; + + if (redirect) { + return ; + } + + return ( + + + ... + + {contact.name} + {contact.email} + {contact.phone_number} + + Edit +
+ + + + ); +}; + +Contact.propTypes = { + contact: PropTypes.object, + deleteContact: PropTypes.func, +}; + +export default Contact; \ No newline at end of file diff --git a/src/components/EditContact.js b/src/components/EditContact.js new file mode 100644 index 00000000..49c53a47 --- /dev/null +++ b/src/components/EditContact.js @@ -0,0 +1,114 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link, Redirect } from 'react-router-dom'; + +const EditContact = ({ match, contacts, editContact }) => { + const { id } = match.params; + const [contact] = contacts.filter((c) => c.id === parseInt(id)); + + const [nameInput, setNameInput] = useState(contact.name); + const [emailInput, setEmailInput] = useState(contact.email); + const [phoneNumberInput, setPhoneNumberInput] = useState( + contact.phone_number + ); + const [imageURLInput, setImageURLInput] = useState(contact.image_url); + const [redirect, setRedirect] = useState(false); + + const handleEditContactClick = () => { + if (!nameInput || !imageURLInput || !emailInput || !phoneNumberInput) { + alert('No field can be left blank'); + return; + } + const updatedContactInfo = { + name: nameInput, + image_url: imageURLInput, + email: emailInput, + phone_number: phoneNumberInput, + id: parseInt(id), + }; + editContact(updatedContactInfo); + setRedirect(true); + }; + + if (redirect) { + return ; + } + + return ( +
+
+

Edit Contact

+

+ Go Back +

+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ ); +}; + +EditContact.propTypes = { + match: PropTypes.object, + contacts: PropTypes.array, + editContact: PropTypes.func, +}; + +export default EditContact; \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 00000000..f391e173 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,11 @@ +import React from 'react'; + +const Header = () => ( +
+
+

Your Contact List

+
+
+); + +export default Header; \ No newline at end of file diff --git a/src/components/Home.js b/src/components/Home.js new file mode 100644 index 00000000..8667b586 --- /dev/null +++ b/src/components/Home.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Redirect } from 'react-router-dom'; +import Contact from './Contact'; + +const Home = ({ contacts, deleteContact }) => { + const [redirect, setRedirect] = useState(false); + + const contactList = contacts.map((c) => ( + + )); + + const handleAddContactClick = () => { + setRedirect(true); + }; + + if (redirect) { + return ; + } + + return ( +
+
+ + + + + + + + + + + + {contactList} +
Profile PicNameEmailPhone NumberOptions:
+
+
+ ); +}; + +Home.propTypes = { + contacts: PropTypes.array.isRequired, + deleteContact: PropTypes.func, +}; + +export default Home; \ No newline at end of file diff --git a/src/components/IndividualContact.js b/src/components/IndividualContact.js new file mode 100644 index 00000000..f387a155 --- /dev/null +++ b/src/components/IndividualContact.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +const IndividualContact = ({ match, contacts }) => { + const { id } = match.params; + const [contact] = contacts.filter((c) => c.id === parseInt(id)); + return ( +
+
+

+ Go Back +

+ ... +

{contact.name}

+

{contact.email}

+

{contact.phone_number}

+
+
+ ); + }; + + IndividualContact.propTypes = { + match: PropTypes.object, + contacts: PropTypes.array, + }; + +export default IndividualContact; \ No newline at end of file diff --git a/src/components/NewContact.js b/src/components/NewContact.js new file mode 100644 index 00000000..d0b34288 --- /dev/null +++ b/src/components/NewContact.js @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link, Redirect } from 'react-router-dom'; + +const NewContact = ({ addContact }) => { + const [nameInput, setNameInput] = useState(''); + const [emailInput, setEmailInput] = useState(''); + const [phoneNumberInput, setPhoneNumberInput] = useState(''); + const [imageURLInput, setImageURLInput] = useState(''); + const [redirect, setRedirect] = useState(false); + + const generateId = () => Math.round(Math.random() * 100000000); + + const handleAddContactClick = () => { + if (!nameInput || !imageURLInput || !emailInput || !phoneNumberInput) { + alert('All Fields are Required'); + return; + } + const newContact = { + name: nameInput, + image_url: imageURLInput, + email: emailInput, + phone_number: phoneNumberInput, + id: generateId(), + }; + addContact(newContact); + setRedirect(true); + }; + + if (redirect) { + return ; + } + + return ( +
+
+

Add New Contact

+

+ Go Back +

+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ ); +}; + +NewContact.propTypes = { + addContact: PropTypes.func.isRequired, +}; + +export default NewContact; \ No newline at end of file diff --git a/src/index.js b/src/index.js index ef2edf8e..4c4ce5cb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import 'bootstrap/dist/css/bootstrap.min.css'; import './index.css'; +import { BrowserRouter } from 'react-router-dom'; import App from './App'; -import reportWebVitals from './reportWebVitals'; ReactDOM.render( + + + , document.getElementById('root') -); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +); \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad..00000000 --- a/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals;