├── .gitignore ├── .vscode └── settings.json ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── blob1.svg ├── blob2.svg ├── components └── Contact.js ├── context ├── Context.js ├── action.types.js └── reducer.js ├── index.css ├── index.js ├── layout ├── Footer.js └── Header.js ├── logo.svg ├── pages ├── AddContact.js ├── Contacts.js ├── PageNotFound.js └── ViewContact.js ├── serviceWorker.js ├── setupTests.js ├── usericon.svg └── utils └── config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Project Title 3 | 4 | Contact Application Using Realtime Database 5 | 6 | An application created using ```ReactJS``` to store a contacts in a database using to ```Firebase Realtime Database``` and ```Storage```. 7 | 8 | ## Project Screenshots 9 | 10 | ![Home](https://user-images.githubusercontent.com/61627365/146380307-89949cdd-444d-4ca2-a0f4-4808bfbca179.jpg) 11 | ![Add Contact](https://user-images.githubusercontent.com/61627365/146380299-f2aec512-8c86-4fc2-85cf-05b4d0185966.jpg) 12 | ![View Contact](https://user-images.githubusercontent.com/61627365/146380309-4f9b06c2-c75e-4e36-940a-8a6a22d53979.jpg) 13 | 14 | 15 | ## Installation 16 | 17 | Clone down this repository. You will need ``` node``` and ```npm``` installed globally on your machine. 18 | 19 | 20 | Installation: 21 | ```bash 22 | npm install 23 | ``` 24 | To Run Test Suite: 25 | ```bash 26 | npm test 27 | ``` 28 | To Start Server: 29 | ```bash 30 | npm start 31 | ``` 32 | To Visit App: 33 | ```bash 34 | localhost:3000/ 35 | ``` 36 | ## Run Locally 37 | 38 | Clone the project 39 | 40 | ```bash 41 | git clone https://github.com/theCodeNilesh/realtime-contact-app.git 42 | ``` 43 | 44 | Go to the project directory 45 | 46 | ```bash 47 | cd my-project 48 | ``` 49 | 50 | Install dependencies 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | Start the server 57 | 58 | ```bash 59 | npm run start 60 | ``` 61 | 62 | ```bash 63 | put your Firebase Configuration in /utils/config.js file 64 | ``` 65 | 66 | 67 | ## Reflection 68 | This was a project built during 3rd year of Engineering college as a personal project. The Project goal was to use ```ReactJs``` technology to create a react app to store a contact details in a ```Realtime Database```. 69 | 70 | One of the main challenge I ran into was Database and Storage, here I have used Firebase ```RealeTime Databse``` and ```Storage``` 71 | ## Documentation 72 | 73 | [Firebase](https://firebase.google.com/docs) 74 | 75 | [React](https://beta.reactjs.org/) 76 | 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleven-realtime-database-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "bootstrap": "^4.4.1", 11 | "browser-image-resizer": "^2.1.0", 12 | "firebase": "^7.24.0", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-icons": "^3.9.0", 16 | "react-router-dom": "^5.1.2", 17 | "react-scripts": "3.4.1", 18 | "react-toastify": "^5.5.0", 19 | "reactstrap": "^8.4.1", 20 | "uuid": "^7.0.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theCodeNilesh/realtime-contact-app/98b30b110539ecb32ec0f28d3e304ccc671aac6f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theCodeNilesh/realtime-contact-app/98b30b110539ecb32ec0f28d3e304ccc671aac6f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theCodeNilesh/realtime-contact-app/98b30b110539ecb32ec0f28d3e304ccc671aac6f/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700;800&display=swap"); 2 | *, 3 | *::after, 4 | *::before { 5 | padding: 0; 6 | margin: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | font-family: "Inter", sans-serif; 12 | background-color: #171b2f; 13 | } 14 | 15 | .icon { 16 | font-size: 29px; 17 | font-weight: bold; 18 | } 19 | 20 | .nav { 21 | background-color: #171b2f; 22 | border: #fff !important; 23 | margin-top: 10px; 24 | } 25 | .navbrand { 26 | font-weight: 700; 27 | font-size: 22px; 28 | margin-left: 30px; 29 | letter-spacing: 1px; 30 | } 31 | .navtxt { 32 | font-weight: 400; 33 | font-size: 18px; 34 | margin-right: 30px; 35 | letter-spacing: 1px; 36 | } 37 | 38 | .profile { 39 | border-radius: 50%; 40 | border-color: none !important; 41 | width: 110px; 42 | height: 110px; 43 | background: transparent; 44 | background-color: transparent; 45 | object-fit: cover; 46 | } 47 | 48 | .hidden { 49 | display: none; 50 | } 51 | 52 | .Center { 53 | height: 75vh; 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | flex-direction: column; 58 | } 59 | 60 | .fab { 61 | position: fixed; 62 | bottom: 50px; 63 | right: 50px; 64 | border-radius: 10px; 65 | background: linear-gradient(110deg, rgba(187, 20, 226), rgba(21, 32, 227)); 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | border-color: none; 70 | border: none; 71 | padding: 15px; 72 | padding-left: 40px; 73 | padding-right: 40px; 74 | color: #f9f9f9; 75 | font-size: 18px; 76 | font-weight: 500; 77 | letter-spacing: 1px; 78 | z-index: 3; 79 | } 80 | 81 | .fab:hover { 82 | color: #f9f9f9; 83 | } 84 | 85 | .icon { 86 | font-size: 25px; 87 | color: #fff; 88 | font-weight: bold; 89 | } 90 | 91 | .text-large { 92 | font-size: 25px; 93 | } 94 | 95 | .card { 96 | margin-top: 125px; 97 | } 98 | 99 | .listcard { 100 | background: white; 101 | background: linear-gradient( 102 | to right bottom, 103 | rgba(255, 255, 255, 0.1), 104 | rgba(255, 255, 255, 0.06) 105 | ); 106 | border-top: 1px solid rgba(255, 255, 255, 0.3); 107 | border-left: 1px solid rgba(255, 255, 255, 0.3); 108 | border-right: 1px solid rgba(255, 255, 255, 0.1); 109 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 110 | background-clip: padding-box; 111 | border-radius: 10px !important; 112 | z-index: 2; 113 | backdrop-filter: blur(20px); 114 | padding: 30px; 115 | padding-left: 40px; 116 | padding-right: 60px; 117 | } 118 | 119 | /* for home page */ 120 | .cirlce1 { 121 | height: 20rem; 122 | width: 20rem; 123 | position: absolute; 124 | top: 50%; 125 | left: 73%; 126 | } 127 | 128 | .cirlce2 { 129 | height: 20rem; 130 | width: 20rem; 131 | position: absolute; 132 | top: 20%; 133 | left: 12%; 134 | } 135 | 136 | .iconbtn { 137 | padding: 13px; 138 | background-color: #1e1d2b; 139 | border-radius: 12px; 140 | margin-right: 10px; 141 | backdrop-filter: blur(30px); 142 | background: linear-gradient( 143 | to right bottom, 144 | rgba(30, 29, 43, 1), 145 | rgba(30, 29, 43, 0.8) 146 | ); 147 | } 148 | 149 | .name { 150 | font-weight: 500; 151 | font-size: 20px; 152 | color: white; 153 | text-transform: capitalize; 154 | letter-spacing: 1px; 155 | } 156 | 157 | .phone { 158 | font-weight: 400; 159 | font-size: 16px; 160 | color: #f9f9f9; 161 | text-transform: capitalize; 162 | letter-spacing: 1px; 163 | 164 | margin-top: 5px; 165 | } 166 | 167 | .mail { 168 | font-weight: 400; 169 | font-size: 14px; 170 | color: #f9f9f9; 171 | letter-spacing: 1px; 172 | } 173 | 174 | .location { 175 | font-weight: 400; 176 | font-size: 14px; 177 | color: #f9f9f9; 178 | letter-spacing: 1px; 179 | } 180 | 181 | .Toastify__toast--success { 182 | background: black; 183 | color: #54eafe; 184 | } 185 | 186 | .Toastify__toast--info { 187 | background: black; 188 | color: #e3d39f; 189 | border-radius: 5px !important; 190 | } 191 | 192 | .Toastify__toast--error { 193 | background: black; 194 | color: #ff6370; 195 | } 196 | .Toastify__toast--warning { 197 | background: black; 198 | color: #ff6370; 199 | } 200 | .Toastify__progress-bar { 201 | background: #54eafe !important; 202 | color: #54eafe; 203 | } 204 | .Toastify__progress-bar--warning { 205 | background: #ff6370 !important; 206 | color: #ff6370; 207 | } 208 | 209 | .Toastify__progress-bar--error { 210 | background: #ff6370 !important; 211 | color: #ff6370; 212 | } 213 | 214 | .Toastify__progress-bar--info { 215 | background: #e3d39f !important; 216 | color: #e3d39f; 217 | } 218 | 219 | .formcard { 220 | background: white; 221 | background: linear-gradient( 222 | to right bottom, 223 | rgba(255, 255, 255, 0.1), 224 | rgba(255, 255, 255, 0.06) 225 | ); 226 | border-top: 1px solid rgba(255, 255, 255, 0.3); 227 | border-left: 1px solid rgba(255, 255, 255, 0.3); 228 | border-right: 1px solid rgba(255, 255, 255, 0.1); 229 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 230 | background-clip: padding-box; 231 | border-radius: 10px !important; 232 | z-index: 2; 233 | backdrop-filter: blur(20px); 234 | padding: 60px; 235 | padding-bottom: 70px; 236 | padding-left: 80px; 237 | padding-right: 80px; 238 | } 239 | 240 | .input { 241 | font-weight: 400; 242 | outline: none !important; 243 | border-radius: 8px !important; 244 | height: 100%; 245 | width: 100%; 246 | color: #f9f9f9; 247 | padding-left: 20px; 248 | padding-right: 10px; 249 | padding-top: 12px; 250 | padding-bottom: 12px; 251 | 252 | background: white; 253 | background: linear-gradient( 254 | to right bottom, 255 | rgba(255, 255, 255, 0.1), 256 | rgba(255, 255, 255, 0.06) 257 | ); 258 | border-top: 2px solid rgba(255, 255, 255, 0.5); 259 | border-left: 2px solid rgba(255, 255, 255, 0.5); 260 | border-right: 2px solid rgba(255, 255, 255, 0.5); 261 | border-bottom: 2px solid rgba(255, 255, 255, 0.5); 262 | background-clip: padding-box; 263 | border-radius: 10px !important; 264 | z-index: 4; 265 | backdrop-filter: blur(40px); 266 | } 267 | 268 | .input:focus { 269 | font-weight: 400; 270 | outline: none !important; 271 | border-radius: 8px !important; 272 | box-shadow: none; 273 | height: 100%; 274 | width: 100%; 275 | color: #f9f9f9; 276 | padding-left: 20px; 277 | padding-right: 10px; 278 | padding-top: 12px; 279 | padding-bottom: 12px; 280 | 281 | background: white; 282 | background: linear-gradient( 283 | to right bottom, 284 | rgba(255, 255, 255, 0.1), 285 | rgba(255, 255, 255, 0.06) 286 | ); 287 | border-top: 2px solid rgba(255, 255, 255, 0.5); 288 | border-left: 2px solid rgba(255, 255, 255, 0.5); 289 | border-right: 2px solid rgba(255, 255, 255, 0.5); 290 | border-bottom: 2px solid rgba(255, 255, 255, 0.5); 291 | background-clip: padding-box; 292 | border-radius: 10px !important; 293 | z-index: 4; 294 | backdrop-filter: blur(40px); 295 | } 296 | 297 | input:-webkit-autofill, 298 | input:-webkit-autofill:hover, 299 | input:-webkit-autofill:focus, 300 | input:-webkit-autofill:active { 301 | transition: background-color 5000s ease-in-out 0s; 302 | } 303 | 304 | input::-webkit-input-placeholder, 305 | textarea::-webkit-input-placeholder { 306 | color: #c3c5de !important; 307 | 308 | font-weight: 300 !important; 309 | } 310 | 311 | input:-moz-placeholder, 312 | textarea:-moz-placeholder { 313 | color: #c3c5de !important; 314 | 315 | font-weight: 300 !important; 316 | } 317 | 318 | .button { 319 | border-radius: 10px; 320 | background: linear-gradient( 321 | 110deg, 322 | rgba(187, 20, 226), 323 | rgba(187, 20, 226), 324 | rgba(21, 32, 227) 325 | ); 326 | display: flex; 327 | justify-content: center; 328 | align-items: center; 329 | border-color: none; 330 | border: none; 331 | 332 | color: #f9f9f9; 333 | box-shadow: none !important; 334 | 335 | font-weight: 500; 336 | letter-spacing: 2px; 337 | z-index: 3; 338 | } 339 | 340 | .button:hover { 341 | color: #f9f9f9; 342 | } 343 | 344 | .showcontact { 345 | background: white; 346 | background: linear-gradient( 347 | to right bottom, 348 | rgba(255, 255, 255, 0.1), 349 | rgba(255, 255, 255, 0.06) 350 | ); 351 | border-top: 1px solid rgba(255, 255, 255, 0.3); 352 | border-left: 1px solid rgba(255, 255, 255, 0.3); 353 | border-right: 1px solid rgba(255, 255, 255, 0.1); 354 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 355 | background-clip: padding-box; 356 | border-radius: 10px !important; 357 | z-index: 2; 358 | backdrop-filter: blur(20px); 359 | padding: 20px; 360 | } 361 | 362 | .cardtxt { 363 | color: #f9f9f9; 364 | letter-spacing: 1px; 365 | } 366 | 367 | /* for view contact */ 368 | .cirlce3 { 369 | height: 18rem; 370 | width: 18rem; 371 | position: absolute; 372 | top: 30%; 373 | left: 7%; 374 | } 375 | 376 | .cirlce4 { 377 | height: 8rem; 378 | width: 8rem; 379 | position: absolute; 380 | top: 10%; 381 | left: 75%; 382 | } 383 | 384 | .cirlce5 { 385 | height: 20rem; 386 | width: 20rem; 387 | position: absolute; 388 | top: 49%; 389 | left: 55%; 390 | } 391 | 392 | /* for form page */ 393 | 394 | .cirlce6 { 395 | height: 8rem; 396 | width: 8rem; 397 | position: absolute; 398 | top: 12%; 399 | left: 65%; 400 | } 401 | 402 | .cirlce7 { 403 | height: 30rem; 404 | width: 30rem; 405 | position: absolute; 406 | top: 49%; 407 | left: 20%; 408 | } 409 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useEffect } from "react"; 2 | 3 | import { Container, Col, Row } from "reactstrap"; 4 | 5 | // react-router-dom3 6 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 7 | 8 | // react toastify stuffs 9 | import { ToastContainer, toast } from "react-toastify"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | 12 | // bootstrap css 13 | import "bootstrap/dist/css/bootstrap.min.css"; 14 | import "./App.css"; 15 | 16 | // firebase stuffs 17 | //TODO: DONE import firebase config and firebase database 18 | import { firebaseConfig } from "./utils/config"; 19 | import firebase from "firebase/app"; 20 | import "firebase/database"; 21 | import "firebase/storage"; 22 | 23 | // components 24 | import AddContact from "./pages/AddContact"; 25 | import Contacts from "./pages/Contacts"; 26 | import Header from "./layout/Header"; 27 | import Footer from "./layout/Footer"; 28 | import ViewContact from "./pages/ViewContact"; 29 | import PageNotFound from "./pages/PageNotFound"; 30 | 31 | // context api stuffs 32 | //TODO: DONE import reducers and contexts 33 | import reducer from "./context/reducer"; 34 | import { ContactContext } from "./context/Context"; 35 | import { SET_CONTACT, SET_LOADING } from "./context/action.types"; 36 | 37 | //initlizeing firebase app with the firebase config which are in ./utils/firebaseConfig 38 | //TODO:DONE initialize FIREBASE 39 | firebase.initializeApp(firebaseConfig); 40 | 41 | // first state to provide in react reducer 42 | const initialState = { 43 | contacts: [], 44 | contact: {}, 45 | contactToUpdate: null, 46 | contactToUpdateKey: null, 47 | isLoading: false, 48 | }; 49 | 50 | const App = () => { 51 | const [state, dispatch] = useReducer(reducer, initialState); 52 | 53 | // will get contacts from firebase and set it on state contacts array 54 | const getContacts = async () => { 55 | // TODO: load existing data 56 | dispatch({ 57 | type: SET_LOADING, 58 | payload: true, 59 | }); 60 | 61 | const contactsRef = await firebase.database().ref("/contacts"); 62 | contactsRef.on("value", (snapshot) => { 63 | dispatch({ 64 | type: SET_CONTACT, 65 | payload: snapshot.val(), 66 | }); 67 | dispatch({ 68 | type: SET_LOADING, 69 | payload: false, 70 | }); 71 | }); 72 | }; 73 | 74 | // getting contact when component did mount 75 | useEffect(() => { 76 | getContacts(); 77 | }, []); 78 | 79 | return ( 80 | 81 | 82 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default App; 98 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/blob1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/blob2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Contact.js: -------------------------------------------------------------------------------- 1 | // https://firebase.google.com/docs/database/web/read-and-write?authuser=1#read_data_once 2 | 3 | import React, { useContext } from "react"; 4 | import { Row, Col } from "reactstrap"; 5 | 6 | // icons 7 | import { FaRegStar, FaStar } from "react-icons/fa"; 8 | import { MdDelete, MdEdit } from "react-icons/md"; 9 | 10 | //TODO: DONE add firebase 11 | import firebase from "firebase/app"; 12 | 13 | // context stuffs 14 | //TODO: DONE import context and action: update and single_contact 15 | import { ContactContext } from "../context/Context"; 16 | import { CONTACT_TO_UPDATE, SET_SINGLE_CONTACT } from "../context/action.types"; 17 | 18 | import { useHistory } from "react-router-dom"; 19 | 20 | import { toast } from "react-toastify"; 21 | 22 | import usericon from "../usericon.svg"; 23 | 24 | const Contact = ({ contact, contactKey }) => { 25 | //TODO: DONE destructuring dispatch from the context 26 | const { dispatch } = useContext(ContactContext); 27 | 28 | // history hooks to get history 29 | const history = useHistory(); 30 | 31 | // to delete the contact when delete contact is clicked 32 | const deleteContact = () => { 33 | //TODO: DONE create this method from firebase 34 | firebase 35 | .database() 36 | .ref(`/contacts/${contactKey}`) 37 | .remove() 38 | .then(() => { 39 | toast("Deleted Successfully", { type: "warning" }); 40 | }) 41 | .catch((err) => console.log(err)); 42 | }; 43 | 44 | // update the star/important contact ,ie, star it or unstar the single contact 45 | const updateImpContact = () => { 46 | //TODO: DONE update (star) contact, use contactKey 47 | firebase 48 | .database() 49 | .ref(`/contacts/${contactKey}`) 50 | .update( 51 | { 52 | star: !contact.star, 53 | }, 54 | (err) => { 55 | console.log(err); 56 | } 57 | ) 58 | .then(() => { 59 | toast("Contact Updated", { type: "info" }); 60 | }) 61 | .catch((err) => console.log(err)); 62 | }; 63 | 64 | // when the update icon/ pen ion is clicked 65 | const updateContact = () => { 66 | // dispatching one action to update contact 67 | //TODO: DONE use dispatch to update 68 | dispatch({ 69 | type: CONTACT_TO_UPDATE, 70 | payload: contact, 71 | key: contactKey, 72 | }); 73 | 74 | // and pushing to the add contact screen 75 | history.push("/contact/add"); 76 | }; 77 | 78 | // to view a single contact in the contact/view screen 79 | const viewSingleContact = (contact) => { 80 | // setting single contact in state 81 | //TODO: use dispatch to view single contact 82 | dispatch({ 83 | type: SET_SINGLE_CONTACT, 84 | payload: contact, 85 | }); 86 | 87 | // sending... 88 | history.push("/contact/view"); 89 | }; 90 | 91 | return ( 92 | <> 93 | 94 | 98 |
updateImpContact()}> 99 | {contact.star ? ( 100 | 101 | ) : ( 102 | 107 | )} 108 |
109 | 110 | 114 | 119 | 120 | viewSingleContact(contact)}> 121 |
{contact.name}
122 | 123 |
124 | {contact.phoneNumber} 125 |
126 |
127 | {contact.email} 128 |
129 | 130 |
131 | {contact.address} 132 |
133 | 134 | 138 |
139 | deleteContact()} 141 | color="#FF6370" 142 | className=" icon" 143 | style={{ zIndex: "1" }} 144 | /> 145 |
146 |
147 | updateContact()} 151 | />{" "} 152 |
153 | 154 |
155 | 156 | ); 157 | }; 158 | 159 | export default Contact; 160 | -------------------------------------------------------------------------------- /src/context/Context.js: -------------------------------------------------------------------------------- 1 | //TODO: DONE: Create context: ContactContext 2 | import { createContext } from "react"; 3 | 4 | export const ContactContext = createContext(); 5 | -------------------------------------------------------------------------------- /src/context/action.types.js: -------------------------------------------------------------------------------- 1 | //TODO: SET_LOADING, SET_CONTACT, 2 | // CONTACT_TO_UPDATE, SET_SINGLE_CONTACT 3 | 4 | export const SET_LOADING = "SET_LOADING"; 5 | export const SET_CONTACT = "SET_CONTACT"; 6 | export const CONTACT_TO_UPDATE = "CONTACT_TP_UPDATE"; 7 | export const SET_SINGLE_CONTACT = "SET_SINGLE_CONTACT"; 8 | -------------------------------------------------------------------------------- /src/context/reducer.js: -------------------------------------------------------------------------------- 1 | //TODO: DONE create contact using all actions 2 | 3 | import { 4 | SET_CONTACT, 5 | SET_LOADING, 6 | CONTACT_TO_UPDATE, 7 | SET_SINGLE_CONTACT, 8 | } from "./action.types"; 9 | 10 | //TODO: DONE use switch case 11 | export default (state, action) => { 12 | switch (action.type) { 13 | case SET_CONTACT: 14 | return action.payload == null 15 | ? { ...state, contacts: [] } 16 | : { ...state, contacts: action.payload }; 17 | case SET_LOADING: 18 | return { ...state, isLoading: action.payload }; 19 | case CONTACT_TO_UPDATE: 20 | return { 21 | ...state, 22 | contactToUpdate: action.payload, 23 | contactToUpdateKey: action.key, 24 | }; 25 | case SET_SINGLE_CONTACT: 26 | return { 27 | ...state, 28 | contact: action.payload, 29 | }; 30 | 31 | default: 32 | return state; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/layout/Footer.js: -------------------------------------------------------------------------------- 1 | //TODO: DONE Export the Footer 2 | import React from "react"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 | A simple Contact App 8 |
9 | ); 10 | }; 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /src/layout/Header.js: -------------------------------------------------------------------------------- 1 | //TODO: DONE set NavbarBrand to go to home page and export Header 2 | 3 | import React from "react"; 4 | import { Navbar, NavbarBrand, NavbarText } from "reactstrap"; 5 | import { Link } from "react-router-dom"; 6 | 7 | const Header = () => { 8 | return ( 9 | 10 | 11 | LCO Contact App 12 | 13 | 14 | A simple Contact app 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/AddContact.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React, { useState, useContext, useEffect } from "react"; 4 | import firebase from "firebase/app"; 5 | 6 | import { 7 | Container, 8 | Form, 9 | FormGroup, 10 | Label, 11 | Input, 12 | Button, 13 | Spinner, 14 | Row, 15 | Col, 16 | } from "reactstrap"; 17 | 18 | // to compress image before uploading to the server 19 | import { readAndCompressImage } from "browser-image-resizer"; 20 | 21 | // configs for image resizing 22 | //TODO: DONE add image configurations 23 | import { imageConfig } from "../utils/config"; 24 | 25 | import { MdAddCircleOutline } from "react-icons/md"; 26 | 27 | import { v4 } from "uuid"; 28 | 29 | // context stuffs 30 | import { ContactContext } from "../context/Context"; 31 | import { CONTACT_TO_UPDATE } from "../context/action.types"; 32 | 33 | import { useHistory } from "react-router-dom"; 34 | 35 | import { toast } from "react-toastify"; 36 | 37 | import usericon from "../usericon.svg"; 38 | 39 | import blob1 from "../blob1.svg"; 40 | import blob2 from "../blob2.svg"; 41 | 42 | const AddContact = () => { 43 | // destructuring state and dispatch from context state 44 | const { state, dispatch } = useContext(ContactContext); 45 | 46 | const { contactToUpdate, contactToUpdateKey } = state; 47 | 48 | // history hooks from react router dom to send to different page 49 | const history = useHistory(); 50 | 51 | // simple state of all component 52 | const [name, setName] = useState(""); 53 | const [email, setEmail] = useState(""); 54 | const [phoneNumber, setPhoneNumber] = useState(""); 55 | const [address, setAddress] = useState(""); 56 | const [isUploading, setIsUploading] = useState(false); 57 | const [downloadUrl, setDownloadUrl] = useState(null); 58 | const [star, setStar] = useState(false); 59 | const [isUpdate, setIsUpdate] = useState(false); 60 | 61 | // when their is the contact to update in the Context state 62 | // then setting state with the value of the contact 63 | // will changes only when the contact to update changes 64 | useEffect(() => { 65 | if (contactToUpdate) { 66 | setName(contactToUpdate.name); 67 | setEmail(contactToUpdate.email); 68 | setPhoneNumber(contactToUpdate.phoneNumber); 69 | setAddress(contactToUpdate.address); 70 | setStar(contactToUpdate.star); 71 | setDownloadUrl(contactToUpdate.picture); 72 | 73 | // also setting is update to true to make the update action instead the addContact action 74 | setIsUpdate(true); 75 | } 76 | }, [contactToUpdate]); 77 | 78 | // To upload image to firebase and then set the the image link in the state of the app 79 | const imagePicker = async (e) => { 80 | // TODO: upload image and set D-URL to state 81 | 82 | try { 83 | const file = e.target.files[0]; 84 | 85 | var metadata = { 86 | contentType: file.type, 87 | }; 88 | 89 | let resizedImage = await readAndCompressImage(file, imageConfig); 90 | 91 | const storageRef = await firebase.storage().ref(); 92 | var uploadTask = storageRef 93 | .child("images/" + file.name) 94 | .put(resizedImage, metadata); 95 | 96 | uploadTask.on( 97 | firebase.storage.TaskEvent.STATE_CHANGED, 98 | (snapshot) => { 99 | setIsUploading(true); 100 | var progress = 101 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 102 | 103 | switch (snapshot.state) { 104 | case firebase.storage.TaskState.PAUSED: 105 | setIsUploading(false); 106 | console.log("UPloading is paused"); 107 | break; 108 | case firebase.storage.TaskState.RUNNING: 109 | console.log("UPloading is in progress..."); 110 | break; 111 | } 112 | if (progress === 100) { 113 | setIsUploading(false); 114 | toast("uploaded", { type: "success" }); 115 | } 116 | }, 117 | (error) => { 118 | toast("something is wrong in state change", { type: "error" }); 119 | }, 120 | () => { 121 | uploadTask.snapshot.ref 122 | .getDownloadURL() 123 | .then((downloadURL) => { 124 | setDownloadUrl(downloadURL); 125 | }) 126 | .catch((err) => console.log(err)); 127 | } 128 | ); 129 | } catch (error) { 130 | console.error(error); 131 | toast("Something went wrong", { type: "error" }); 132 | } 133 | }; 134 | 135 | // setting contact to firebase DB 136 | const addContact = async () => { 137 | //TODO: add contact method 138 | try { 139 | firebase 140 | .database() 141 | .ref("contacts/" + v4()) 142 | .set({ 143 | name, 144 | email, 145 | phoneNumber, 146 | address, 147 | picture: downloadUrl, 148 | star, 149 | }); 150 | } catch (error) { 151 | console.log(error); 152 | } 153 | }; 154 | 155 | // to handle update the contact when there is contact in state and the user had came from clicking the contact update icon 156 | const updateContact = async () => { 157 | //TODO: update contact method 158 | 159 | try { 160 | firebase 161 | .database() 162 | .ref("contacts/" + contactToUpdateKey) 163 | .set({ 164 | name, 165 | email, 166 | phoneNumber, 167 | address, 168 | picture: downloadUrl ? downloadUrl : null, 169 | star, 170 | }); 171 | } catch (error) { 172 | console.log(error); 173 | toast("Oppss.. you forgot to upload photo", { type: "error" }); 174 | } 175 | }; 176 | 177 | // firing when the user click on submit button or the form has been submitted 178 | const handleSubmit = (e) => { 179 | e.preventDefault(); 180 | isUpdate ? updateContact() : addContact(); 181 | 182 | toast("Contacts Updated", { type: "success" }); 183 | // isUpdate wll be true when the user came to update the contact 184 | // when their is contact then updating and when no contact to update then adding contact 185 | //TODO: set isUpdate value 186 | 187 | // to handle the bug when the user visit again to add contact directly by visiting the link 188 | dispatch({ 189 | type: CONTACT_TO_UPDATE, 190 | payload: null, 191 | key: null, 192 | }); 193 | 194 | // after adding/updating contact then sending to the contacts 195 | // TODO :- also sending when their is any errors 196 | history.push("/"); 197 | }; 198 | 199 | // return the spinner when the image has been added in the storage 200 | // showing the update / add contact based on the state 201 | return ( 202 | 203 | blob1 204 | blob2 205 | 206 | 207 |
208 |
209 | {isUploading ? ( 210 | 211 | ) : ( 212 |
213 | 220 | imagePicker(e)} 227 | className="hidden" 228 | /> 229 |
230 | )} 231 |
232 | 233 | 234 | setName(e.target.value)} 242 | /> 243 | 244 | 245 | setEmail(e.target.value)} 252 | placeholder="Email" 253 | /> 254 | 255 | 256 | setPhoneNumber(e.target.value)} 263 | placeholder="phone number" 264 | /> 265 | 266 | 267 | setAddress(e.target.value)} 273 | placeholder="address" 274 | className="input" 275 | /> 276 | 277 | 278 | 298 | 299 | 311 |
312 | 313 |
314 |
315 | ); 316 | }; 317 | 318 | export default AddContact; 319 | -------------------------------------------------------------------------------- /src/pages/Contacts.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import { 4 | Container, 5 | ListGroup, 6 | ListGroupItem, 7 | Spinner, 8 | Button, 9 | } from "reactstrap"; 10 | import Contact from "../components/Contact"; 11 | import { MdAdd } from "react-icons/md"; 12 | import { useHistory } from "react-router-dom"; 13 | import { ContactContext } from "../context/Context"; 14 | import { CONTACT_TO_UPDATE } from "../context/action.types"; 15 | import blob1 from "../blob1.svg"; 16 | import blob2 from "../blob2.svg"; 17 | 18 | const Contacts = () => { 19 | const { state, dispatch } = useContext(ContactContext); 20 | 21 | // destructuring contacts and isLoading from state 22 | const { contacts, isLoading } = state; 23 | 24 | // history hooks from react router dom to get history 25 | const history = useHistory(); 26 | 27 | // handle fab icon button click 28 | // will set in state of the contact to update and send it to the contact/add route 29 | const AddContact = () => { 30 | //TODO: use dispatch to send user to add contact screen 31 | dispatch({ 32 | type: CONTACT_TO_UPDATE, 33 | payload: null, 34 | key: null, 35 | }); 36 | history.push("/contact/add"); 37 | }; 38 | 39 | // return loading spinner 40 | if (isLoading) { 41 | return ( 42 |
43 | 44 |
Loading...
45 |
46 | ); 47 | } 48 | 49 | return ( 50 | 51 | blob1 52 | blob2 53 | 54 | {/* TODO: Loop through FIREBASE objects */} 55 | {contacts.length === 0 && !isLoading ? ( 56 |
60 | No Contacts found in firebase ...! 61 |
62 | ) : ( 63 | 64 | {Object.entries(contacts).map(([key, value]) => ( 65 | 66 | 67 | 68 | ))} 69 | 70 | )} 71 | 78 |
79 | ); 80 | }; 81 | 82 | export default Contacts; 83 | -------------------------------------------------------------------------------- /src/pages/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PageNotFound = () => { 4 | return ( 5 |
6 |

404 page not found

7 |
8 | ); 9 | }; 10 | 11 | //FIXME: missing keywords 12 | export default PageNotFound; 13 | -------------------------------------------------------------------------------- /src/pages/ViewContact.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import usericon from "../usericon.svg"; 3 | 4 | import { 5 | Container, 6 | Row, 7 | Col, 8 | Card, 9 | CardBody, 10 | CardTitle, 11 | CardSubtitle, 12 | } from "reactstrap"; 13 | import { FaEnvelope, FaMapMarkerAlt, FaPhone } from "react-icons/fa"; 14 | import { ContactContext } from "../context/Context"; 15 | import blob1 from "../blob1.svg"; 16 | import blob2 from "../blob2.svg"; 17 | 18 | const ViewContact = () => { 19 | const { state } = useContext(ContactContext); 20 | // destructuring contact from the state 21 | // and rendering it in state 22 | //FIXME: DONE destructure contact from state 23 | const { contact } = state; 24 | return ( 25 | 26 | blob1 27 | blob2 28 | blob2 29 | 30 | 31 | 35 | 36 | 42 | 43 |

51 | {contact?.name} 52 |

53 |
54 | 55 |

63 | 64 | {contact?.phoneNumber} 65 |

66 |
67 | 77 | 78 | {contact?.email} 79 | 80 | 81 | 92 | 93 | {contact?.address} 94 | 95 |
96 |
97 | 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default ViewContact; 104 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/usericon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | //TODO: DONE add firebase configuration and export it 2 | export const firebaseConfig = { 3 | // put your firebase config here 4 | }; 5 | 6 | //image configuration 7 | export const imageConfig = { 8 | quality: 0.2, 9 | maxWidth: 800, 10 | maxHeight: 600, 11 | autoRotate: true, 12 | }; 13 | --------------------------------------------------------------------------------