├── .gitignore
├── LICENSE
├── README.md
├── client
├── .gitignore
├── .netlify
│ └── state.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.png
│ ├── index.html
│ ├── logo256.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── backendUrl.js
│ ├── components
│ ├── AddContactModal.js
│ ├── ContactCard.js
│ ├── ContactsDisplay.js
│ ├── ContactsLoader.js
│ ├── DeleteModal.js
│ ├── DemoCredsInfo.js
│ ├── DisplayPictureModal.js
│ ├── EditContactModal.js
│ ├── FormError.js
│ ├── LinkFormModal.js
│ ├── LoginForm.js
│ ├── NavBar.js
│ ├── RegisterForm.js
│ ├── Routes.js
│ └── Search.js
│ ├── data
│ └── demoCreds.js
│ ├── index.css
│ ├── index.js
│ ├── services
│ ├── auth.js
│ └── contacts.js
│ └── utils
│ ├── arraysAndFuncs.js
│ └── localStorageHelpers.js
├── screenshots
├── auth-forms.png
├── desktop-tablet.png
├── mobile-ui-1.png
├── mobile-ui-2.png
└── modals.png
└── server
├── .eslintrc.json
├── app.js
├── controllers
├── auth.js
└── contact.js
├── db.js
├── index.js
├── models
├── contact.js
└── user.js
├── package-lock.json
├── package.json
├── routes
├── auth.js
└── contact.js
└── utils
├── config.js
└── middleware.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | build
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Amandeep Singh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Profile Store | MERN
2 |
3 | A MERN stack app for storing profile links of people you admire, at one place.
4 |
5 | ## Demo
6 |
7 | [Deployed on Netlify (front-end) & Heroku (back-end)](https://profile-store.netlify.app)
8 |
9 | ## Built using
10 |
11 | #### Front-end
12 |
13 | - [ReactJS](https://reactjs.org/) - Frontend framework
14 | - [useState hook & props](https://reactjs.org/docs/hooks-state.html) - For state management
15 | - [React Router](https://reactrouter.com/) - For general routing & navigation
16 | - [Semantic-UI w/ normal CSS for customisations](https://react.semantic-ui.com/) - UI library
17 | - [React toast notifications](https://jossmac.github.io/react-toast-notifications/) - For toast notifications (duh :P)
18 |
19 | #### Back-end
20 |
21 | - [Node.js](https://nodejs.org/en/) - Runtime environment for JS
22 | - [Express.js](https://expressjs.com/) - Node.js framework, makes process of building APIs easier & faster
23 | - [MongoDB](https://www.mongodb.com/) - Database to store document-based data
24 | - [Mongoose](https://mongoosejs.com/) - MongoDB object modeling for Node.js
25 | - [Cloudinary](https://cloudinary.com/) - For image uploading & related API
26 | - [JSON Web Token](https://jwt.io/) - A standard to secure/authenticate HTTP requests
27 | - [Bcrypt.js](https://www.npmjs.com/package/bcryptjs) - For hashing passwords
28 | - [Validator.js](https://www.npmjs.com/package/validator) - For validation of JSON data
29 | - [Mongoose Unique Validator](https://www.npmjs.com/package/mongoose-unique-validator) - Plugin for better error handling of unique fields within Mongoose schema
30 | - [Dotenv](https://www.npmjs.com/package/dotenv) - To load environment variables from a .env file
31 |
32 | ## Features
33 |
34 | - Authentication (login/register with email-password)
35 | - Upload images for display pictures of contacts
36 | - Add/update/delete contacts & change display picture
37 | - Add/update/delete profile links of individual contacts
38 | - Search contacts by name or profile links
39 | - Toast notifications for actions - adding/updating/deleting contact, or welcome message etc.
40 | - Dark mode toggle w/ local storage save
41 | - Responsive UI for all screens
42 |
43 | ## Screenshots
44 |
45 | #### Desktop/Tablet Home
46 |
47 | 
48 |
49 | #### Auth Forms
50 |
51 | 
52 |
53 | #### Pop-up windows (modals)
54 |
55 | 
56 |
57 | #### Mobile UI
58 |
59 | 
60 |
61 | 
62 |
63 | ## Usage
64 |
65 | Notes:
66 |
67 | - For image API, make account at cloudinary.com & get API keys from account dashboard.
68 | - For upload preset usage, if you want to organize images separately at cloudinary.com, you have to create it from account settings first. If you don't want to, just don't put anything or use .env key - `UPLOAD_PRESET`.
69 |
70 | #### Env variable:
71 |
72 | Create .env file in server directory and add the following:
73 |
74 | ```
75 | MONGODB_URI = "Your Mongo URI"
76 | PORT = 3005
77 | SECRET = "Your JWT secret"
78 | CLOUDINARY_NAME = "From your cloudinary dashboard"
79 | CLOUDINARY_API_KEY = "From your cloudinary dashboard"
80 | CLOUDINARY_API_SECRET = "From your cloudinary dashboard"
81 | UPLOAD_PRESET = "Folder/preset name from your cloudinary account" (OPTIONAL)
82 | ```
83 |
84 | #### Client:
85 |
86 | Open client/src/backendUrl.js & change "backend" variable to `"http://localhost:3005"`
87 |
88 | ```
89 | cd client
90 | npm install
91 | npm start
92 | ```
93 |
94 | #### Server:
95 |
96 | Note: Make sure that you have installed 'nodemon' as global package.
97 |
98 | ```
99 | cd server
100 | npm install
101 | npm run dev
102 | ```
103 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/client/.netlify/state.json:
--------------------------------------------------------------------------------
1 | {
2 | "siteId": "0b69803b-6b3d-4f71-9619-e3f961f6b99c"
3 | }
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "profile-store-by-amand33p",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "react": "^16.13.1",
8 | "react-dom": "^16.13.1",
9 | "react-responsive": "^8.1.0",
10 | "react-router-dom": "^5.2.0",
11 | "react-scripts": "3.4.1",
12 | "react-toast-notifications": "^2.4.0",
13 | "semantic-ui-react": "^1.0.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "predeploy": "rm -rf build && npm run build"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/favicon.png
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
20 |
29 | Profile Store | amand33p
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/client/public/logo256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/logo256.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/logo512.png
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Profile Store",
3 | "name": "Profile Store by amand33p",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
7 | "sizes": "64x64",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "logo256.png",
12 | "type": "image/png",
13 | "sizes": "256x256"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#9edacf",
24 | "background_color": "#e5f5f2"
25 | }
26 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import NavBar from './components/NavBar';
3 | import Routes from './components/Routes';
4 | import contactService from './services/contacts';
5 | import { optionsArray } from './utils/arraysAndFuncs';
6 | import storageService from './utils/localStorageHelpers';
7 | import { useToasts } from 'react-toast-notifications';
8 | import { Container, Segment } from 'semantic-ui-react';
9 |
10 | const App = () => {
11 | const [contacts, setContacts] = useState([]);
12 | const [user, setUser] = useState(null);
13 | const [options, setOptions] = useState(optionsArray);
14 | const [isLoading, setIsLoading] = useState(false);
15 | const [search, setSearch] = useState('');
16 | const [isDarkMode, setIsDarkMode] = useState(false);
17 |
18 | const { addToast: notify } = useToasts();
19 |
20 | useEffect(() => {
21 | const loggedUser = storageService.loadUser();
22 |
23 | if (loggedUser) {
24 | setUser(loggedUser);
25 | contactService.setToken(loggedUser.token);
26 | }
27 | }, []);
28 |
29 | useEffect(() => {
30 | const getAllContacts = async () => {
31 | try {
32 | setIsLoading(true);
33 | const contacts = await contactService.getAll();
34 | setContacts(contacts);
35 | setIsLoading(false);
36 | } catch (err) {
37 | setIsLoading(false);
38 | notify(err.message, {
39 | appearance: 'error',
40 | });
41 | }
42 | };
43 |
44 | if (user) {
45 | getAllContacts();
46 | }
47 | // eslint-disable-next-line react-hooks/exhaustive-deps
48 | }, [user]);
49 |
50 | useEffect(() => {
51 | const darkMode = storageService.loadDarkMode();
52 | if (darkMode === 'true') {
53 | setIsDarkMode(true);
54 | }
55 | }, []);
56 |
57 | const handleOptionAddition = (e, data) => {
58 | setOptions((prevState) => [
59 | {
60 | key: data.value,
61 | text: data.value,
62 | value: data.value,
63 | icon: 'linkify',
64 | },
65 | ...prevState,
66 | ]);
67 | };
68 |
69 | return (
70 |
71 |
72 |
78 |
91 |
92 |
93 | );
94 | };
95 |
96 | export default App;
97 |
--------------------------------------------------------------------------------
/client/src/backendUrl.js:
--------------------------------------------------------------------------------
1 | const backendUrl = 'https://profile-store.herokuapp.com';
2 |
3 | export default backendUrl;
4 |
--------------------------------------------------------------------------------
/client/src/components/AddContactModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import FormError from './FormError';
3 | import contactService from '../services/contacts';
4 | import { generateBase64Encode } from '../utils/arraysAndFuncs';
5 | import { useMediaQuery } from 'react-responsive';
6 | import { Modal, Header, Form, Button, Icon, Image } from 'semantic-ui-react';
7 |
8 | const AddContactModal = ({
9 | setContacts,
10 | options,
11 | handleOptionAddition,
12 | notify,
13 | isDarkMode,
14 | }) => {
15 | const [modalOpen, setModalOpen] = useState(false);
16 | const [name, setName] = useState('');
17 | const [url, setUrl] = useState('');
18 | const [site, setSite] = useState('');
19 | const [displayPicture, setDisplayPicture] = useState('');
20 | const [fileName, setFileName] = useState('');
21 | const [error, setError] = useState(null);
22 | const [isLoading, setIsLoading] = useState(false);
23 |
24 | const isMobile = useMediaQuery({ maxWidth: 767 });
25 |
26 | const handleOpen = () => {
27 | setModalOpen(true);
28 | };
29 |
30 | const handleClose = () => {
31 | setModalOpen(false);
32 | };
33 |
34 | const addNewContact = async (e) => {
35 | e.preventDefault();
36 |
37 | const contactObject = {
38 | name,
39 | contacts: {
40 | url,
41 | site,
42 | },
43 | displayPicture,
44 | };
45 |
46 | try {
47 | setIsLoading(true);
48 | const returnedObject = await contactService.addNew(contactObject);
49 | setContacts((prevState) => prevState.concat(returnedObject));
50 | setIsLoading(false);
51 | setError(null);
52 |
53 | notify(`Added new contact named as "${returnedObject.name}"`, {
54 | appearance: 'success',
55 | });
56 | handleClose();
57 |
58 | setName('');
59 | setUrl('');
60 | setSite('');
61 | setDisplayPicture('');
62 | setFileName('');
63 | } catch (err) {
64 | setIsLoading(false);
65 | const errRes = err?.response?.data;
66 |
67 | if (errRes?.error) {
68 | return setError(errRes.error);
69 | } else {
70 | return setError(err.message);
71 | }
72 | }
73 | };
74 |
75 | const fileInputOnChange = (e) => {
76 | const file = e.target.files[0];
77 | setFileName(file.name);
78 | generateBase64Encode(file, setDisplayPicture);
79 | };
80 |
81 | const clearfileSelection = () => {
82 | setDisplayPicture('');
83 | setFileName('');
84 | };
85 |
86 | return (
87 |
98 |
99 | Add New Contact
100 |
101 | }
102 | onOpen={handleOpen}
103 | onClose={handleClose}
104 | className={isDarkMode ? 'dark-mode-modal modal' : 'modal'}
105 | >
106 |
107 | {error && }
108 |
109 | setName(e.target.value)}
117 | icon="user secret"
118 | iconPosition="left"
119 | />
120 | setUrl(e.target.value)}
127 | icon="linkify"
128 | iconPosition="left"
129 | />
130 | setSite(data.value)}
141 | />
142 |
143 |
156 |
163 | {displayPicture && (
164 |
173 |
174 | Un-select Image
175 |
176 | )}
177 | {displayPicture && (
178 |
184 | )}
185 |
186 |
192 |
193 | Submit
194 |
195 |
196 |
197 |
198 |
199 | );
200 | };
201 |
202 | export default AddContactModal;
203 |
--------------------------------------------------------------------------------
/client/src/components/ContactCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LinkFormModal from './LinkFormModal';
3 | import DeleteModal from './DeleteModal';
4 | import DisplayPictureModal from './DisplayPictureModal';
5 | import EditContactModal from './EditContactModal';
6 | import { siteIconsArray, randomColor } from '../utils/arraysAndFuncs';
7 | import { useMediaQuery } from 'react-responsive';
8 | import { Header, Card, List, Label } from 'semantic-ui-react';
9 |
10 | const ContactCard = ({
11 | contact,
12 | contacts,
13 | setContacts,
14 | options,
15 | handleOptionAddition,
16 | notify,
17 | isDarkMode,
18 | }) => {
19 | const isMobile = useMediaQuery({ maxWidth: 767 });
20 |
21 | const linkCharCount = isMobile ? '30' : '70';
22 |
23 | const formattedLink = (link) => {
24 | return link.length > linkCharCount
25 | ? link.slice(0, linkCharCount).concat('...')
26 | : link;
27 | };
28 |
29 | return (
30 |
31 |
32 |
71 |
72 |
73 |
74 | {contact.contacts.map((c) => (
75 |
76 |
85 |
86 |
87 |
94 | {formattedLink(c.url).startsWith('http')
95 | ? formattedLink(c.url).split('//')[1]
96 | : formattedLink(c.url)}
97 |
98 |
110 |
123 |
124 | {c.site}
125 |
126 |
127 | ))}
128 |
129 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default ContactCard;
145 |
--------------------------------------------------------------------------------
/client/src/components/ContactsDisplay.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ContactCard from './ContactCard';
3 | import ContactsLoader from './ContactsLoader';
4 | import { Header, Icon } from 'semantic-ui-react';
5 |
6 | const ContactsDisplay = ({
7 | contacts,
8 | setContacts,
9 | search,
10 | options,
11 | handleOptionAddition,
12 | notify,
13 | isLoading,
14 | isDarkMode,
15 | }) => {
16 | const filterByName = (contact, search) => {
17 | return contact.name.toLowerCase().includes(search.toLowerCase());
18 | };
19 |
20 | const filterByProfileLinks = (contact, search) => {
21 | const urlsArray = contact.contacts.map((c) => c.url);
22 |
23 | return urlsArray.find((u) =>
24 | u.toLowerCase().includes(search.toLowerCase())
25 | );
26 | };
27 |
28 | const contactsToDisplay = contacts.filter(
29 | (c) => filterByName(c, search) || filterByProfileLinks(c, search)
30 | );
31 |
32 | return (
33 |
34 | {search !== '' && contactsToDisplay.length !== 0 && (
35 |
36 |
37 | Showing search results for query "{search}"
38 |
39 | )}
40 | {search !== '' && contactsToDisplay.length === 0 && (
41 |
46 |
47 | Search: No matches found for "{search}"
48 |
49 | )}
50 | {!isLoading && search === '' && contactsToDisplay.length === 0 && (
51 |
56 |
57 | No contacts added yet.
58 |
59 | )}
60 | {isLoading ? (
61 |
62 | ) : (
63 | contactsToDisplay.map((contact) => (
64 |
74 | ))
75 | )}
76 |
77 | );
78 | };
79 |
80 | export default ContactsDisplay;
81 |
--------------------------------------------------------------------------------
/client/src/components/ContactsLoader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Placeholder, Segment } from 'semantic-ui-react';
3 |
4 | const ContactsLoader = ({ isDarkMode }) => {
5 | return (
6 |
7 | {Array.from(new Array(3)).map((_, i) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default ContactsLoader;
34 |
--------------------------------------------------------------------------------
/client/src/components/DeleteModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import FormError from './FormError';
3 | import contactService from '../services/contacts';
4 | import { useMediaQuery } from 'react-responsive';
5 | import { Button, Header, Icon, Modal } from 'semantic-ui-react';
6 |
7 | const DeleteModal = ({
8 | type,
9 | contacts,
10 | setContacts,
11 | contact,
12 | id,
13 | urlId,
14 | urlLink,
15 | urlName,
16 | notify,
17 | isDarkMode,
18 | }) => {
19 | const [open, setOpen] = useState(false);
20 | const [error, setError] = useState(null);
21 | const [isLoading, setIsLoading] = useState(false);
22 |
23 | const isMobile = useMediaQuery({ maxWidth: 767 });
24 |
25 | const handleContactDelete = async () => {
26 | try {
27 | setIsLoading(true);
28 | await contactService.deleteContact(id);
29 | setContacts(contacts.filter((c) => c.id !== id));
30 | setIsLoading(false);
31 | setError(null);
32 |
33 | notify(`Deleted contact "${contact.name}"`, {
34 | appearance: 'success',
35 | });
36 | } catch (err) {
37 | setIsLoading(false);
38 | const errRes = err.response.data;
39 |
40 | if (errRes && errRes.error) {
41 | return setError(errRes.error);
42 | } else {
43 | return setError(err.message);
44 | }
45 | }
46 | };
47 |
48 | const handleLinkDelete = async () => {
49 | const targetContact = contacts.find((c) => c.id === id);
50 | const updatedContactsKey = targetContact.contacts.filter(
51 | (t) => t.id !== urlId
52 | );
53 | const updatedContact = { ...targetContact, contacts: updatedContactsKey };
54 |
55 | try {
56 | setIsLoading(true);
57 | await contactService.deleteLink(id, urlId);
58 | setContacts(contacts.map((c) => (c.id !== id ? c : updatedContact)));
59 | setIsLoading(false);
60 | setError(null);
61 |
62 | notify(`Deleted ${urlName} link "${urlLink}"`, {
63 | appearance: 'success',
64 | });
65 | } catch (err) {
66 | setIsLoading(false);
67 | const errRes = err.response.data;
68 |
69 | if (errRes && errRes.error) {
70 | return setError(errRes.error);
71 | } else {
72 | return setError(err.message);
73 | }
74 | }
75 | };
76 |
77 | const isTypeContact = type === 'contact';
78 |
79 | return (
80 |
95 | }
96 | onClose={() => setOpen(false)}
97 | onOpen={() => setOpen(true)}
98 | className={isDarkMode ? 'dark-mode-modal' : ''}
99 | >
100 |
101 | {error && }
102 |
103 |
104 | {isTypeContact
105 | ? `Are you sure you want to delete contact named as '${contact.name}'?`
106 | : `Are you sure you want to delete ${urlName} link '${urlLink}'?`}
107 |
108 |
109 |
110 | setOpen(false)} floated="left">
111 | No
112 |
113 |
119 | Yes
120 |
121 |
122 |
123 | );
124 | };
125 |
126 | export default DeleteModal;
127 |
--------------------------------------------------------------------------------
/client/src/components/DemoCredsInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Message } from 'semantic-ui-react';
3 | import demoCredentials from '../data/demoCreds';
4 |
5 | const DemoCredsInfo = () => {
6 | return (
7 |
8 | );
9 | };
10 |
11 | export default DemoCredsInfo;
12 |
--------------------------------------------------------------------------------
/client/src/components/DisplayPictureModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { getCircularAvatar } from '../utils/arraysAndFuncs';
3 |
4 | import { Header, Modal, Image } from 'semantic-ui-react';
5 |
6 | const DisplayPictureModal = ({ imageLink, contactName, isDarkMode }) => {
7 | const [open, setOpen] = useState(false);
8 |
9 | return (
10 |
20 | }
21 | onClose={() => setOpen(false)}
22 | onOpen={() => setOpen(true)}
23 | className={isDarkMode ? 'dark-mode-modal' : ''}
24 | >
25 |
26 |
27 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default DisplayPictureModal;
39 |
--------------------------------------------------------------------------------
/client/src/components/EditContactModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import FormError from './FormError';
3 | import contactService from '../services/contacts';
4 | import { generateBase64Encode } from '../utils/arraysAndFuncs';
5 | import { useMediaQuery } from 'react-responsive';
6 | import { Modal, Header, Form, Button, Icon, Image } from 'semantic-ui-react';
7 |
8 | const EditContactModal = ({ oldName, setContacts, id, notify, isDarkMode }) => {
9 | const [modalOpen, setModalOpen] = useState(false);
10 | const [name, setName] = useState(oldName);
11 | const [displayPicture, setDisplayPicture] = useState('');
12 | const [fileName, setFileName] = useState('');
13 | const [error, setError] = useState(null);
14 | const [isLoading, setIsLoading] = useState(false);
15 |
16 | const isMobile = useMediaQuery({ maxWidth: 767 });
17 |
18 | const handleOpen = () => {
19 | setModalOpen(true);
20 | };
21 |
22 | const handleClose = () => {
23 | setModalOpen(false);
24 | };
25 |
26 | const addNewContact = async (e) => {
27 | e.preventDefault();
28 |
29 | const contactObject = {
30 | name,
31 | displayPicture,
32 | };
33 |
34 | try {
35 | setIsLoading(true);
36 | const returnedObject = await contactService.editContact(
37 | id,
38 | contactObject
39 | );
40 | setContacts((prevState) =>
41 | prevState.map((p) => (p.id !== id ? p : returnedObject))
42 | );
43 | setIsLoading(false);
44 | setError(null);
45 |
46 | let message = `Updated contact "${returnedObject.name}"`;
47 |
48 | if (oldName !== returnedObject.name && displayPicture !== '') {
49 | message = `Updated contact name from "${oldName}" to "${returnedObject.name}" & changed DP`;
50 | } else if (oldName === returnedObject.name && displayPicture !== '') {
51 | message = `Updated DP of contact "${returnedObject.name}"`;
52 | } else if (oldName !== returnedObject.name && displayPicture === '') {
53 | message = `Updated contact name from "${oldName}" to "${returnedObject.name}"`;
54 | }
55 |
56 | notify(message, {
57 | appearance: 'success',
58 | });
59 | handleClose();
60 | setDisplayPicture('');
61 | setFileName('');
62 | } catch (err) {
63 | setIsLoading(false);
64 | const errRes = err?.response?.data;
65 |
66 | if (errRes?.error) {
67 | return setError(errRes.error);
68 | } else {
69 | return setError(err.message);
70 | }
71 | }
72 | };
73 |
74 | const fileInputOnChange = (e) => {
75 | const file = e.target.files[0];
76 | setFileName(file.name);
77 | generateBase64Encode(file, setDisplayPicture);
78 | };
79 |
80 | const clearfileSelection = () => {
81 | setDisplayPicture('');
82 | setFileName('');
83 | };
84 |
85 | return (
86 |
98 | }
99 | onOpen={handleOpen}
100 | onClose={handleClose}
101 | className={isDarkMode ? 'dark-mode-modal modal' : 'modal'}
102 | >
103 |
108 | {error && }
109 |
110 | setName(e.target.value)}
118 | icon="user secret"
119 | iconPosition="left"
120 | />
121 |
134 |
141 | {displayPicture && (
142 |
147 |
148 | Un-select Image
149 |
150 | )}
151 | {displayPicture && (
152 |
158 | )}
159 |
160 |
166 |
167 | Update
168 |
169 |
170 |
171 |
172 |
173 | );
174 | };
175 |
176 | export default EditContactModal;
177 |
--------------------------------------------------------------------------------
/client/src/components/FormError.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Message, Button } from 'semantic-ui-react';
3 |
4 | const FormError = ({ message, setError, title, positive }) => {
5 | return (
6 |
7 |
8 | {title || 'Error'}
9 | setError(null)}
14 | compact
15 | size="mini"
16 | circular
17 | />
18 |
19 | {message}
20 |
21 | );
22 | };
23 |
24 | export default FormError;
25 |
--------------------------------------------------------------------------------
/client/src/components/LinkFormModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import FormError from './FormError';
3 | import contactService from '../services/contacts';
4 | import { useMediaQuery } from 'react-responsive';
5 | import { Modal, Header, Form, Button, Icon } from 'semantic-ui-react';
6 |
7 | const LinkFormModal = ({
8 | id,
9 | urlId,
10 | contacts,
11 | setContacts,
12 | type,
13 | options,
14 | handleOptionAddition,
15 | urlToEdit,
16 | siteToEdit,
17 | notify,
18 | isDarkMode,
19 | }) => {
20 | const [modalOpen, setModalOpen] = useState(false);
21 | const [url, setUrl] = useState(urlToEdit ? urlToEdit : '');
22 | const [site, setSite] = useState(siteToEdit ? siteToEdit : '');
23 | const [error, setError] = useState(null);
24 | const [isLoading, setIsLoading] = useState(false);
25 |
26 | const isMobile = useMediaQuery({ maxWidth: 767 });
27 |
28 | const handleOpen = () => {
29 | setModalOpen(true);
30 | };
31 |
32 | const handleClose = () => {
33 | setModalOpen(false);
34 | };
35 |
36 | const newObject = {
37 | url,
38 | site,
39 | };
40 |
41 | const addNewLink = async (e) => {
42 | e.preventDefault();
43 |
44 | try {
45 | setIsLoading(true);
46 | const returnedObject = await contactService.addLink(id, newObject);
47 | setContacts(contacts.map((c) => (c.id !== id ? c : returnedObject)));
48 | setIsLoading(false);
49 | setError(null);
50 |
51 | notify(`Added new ${newObject.site} link "${newObject.url}"`, {
52 | appearance: 'success',
53 | });
54 | setUrl('');
55 | setSite('');
56 | handleClose();
57 | } catch (err) {
58 | setIsLoading(false);
59 | const errRes = err?.response?.data;
60 |
61 | if (errRes?.error) {
62 | return setError(errRes.error);
63 | } else {
64 | return setError(err.message);
65 | }
66 | }
67 | };
68 |
69 | const editLink = async (e) => {
70 | e.preventDefault();
71 |
72 | const targetContact = contacts.find((c) => c.id === id);
73 |
74 | try {
75 | setIsLoading(true);
76 | const returnedObject = await contactService.editLink(
77 | id,
78 | urlId,
79 | newObject
80 | );
81 |
82 | const updatedContactsKey = targetContact.contacts.map((t) =>
83 | t.id !== urlId ? t : returnedObject
84 | );
85 |
86 | const updatedContact = {
87 | ...targetContact,
88 | contacts: updatedContactsKey,
89 | };
90 |
91 | setContacts(contacts.map((c) => (c.id !== id ? c : updatedContact)));
92 | setIsLoading(false);
93 | setError(null);
94 |
95 | notify(`Edited ${newObject.site} link to "${newObject.url}"`, {
96 | appearance: 'success',
97 | });
98 | handleClose();
99 | } catch (err) {
100 | setIsLoading(false);
101 | const errRes = err.response.data;
102 |
103 | if (errRes && errRes.error) {
104 | return setError(errRes.error);
105 | } else {
106 | return setError(err.message);
107 | }
108 | }
109 | };
110 |
111 | const isTypeEdit = type === 'edit';
112 |
113 | return (
114 |
127 | }
128 | onOpen={handleOpen}
129 | onClose={handleClose}
130 | className={isDarkMode ? 'dark-mode-modal modal' : 'modal'}
131 | >
132 |
136 | {error && }
137 |
138 | setUrl(e.target.value)}
146 | icon="linkify"
147 | iconPosition="left"
148 | />
149 | setSite(data.value)}
159 | onAddItem={handleOptionAddition}
160 | />
161 |
167 |
168 | {isTypeEdit ? 'Update' : 'Add'}
169 |
170 |
171 |
172 |
173 | );
174 | };
175 |
176 | export default LinkFormModal;
177 |
--------------------------------------------------------------------------------
/client/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useHistory } from 'react-router-dom';
3 | import FormError from './FormError';
4 | import DemoCredsInfo from './DemoCredsInfo';
5 | import contactService from '../services/contacts';
6 | import authService from '../services/auth';
7 | import storageService from '../utils/localStorageHelpers';
8 | import { useMediaQuery } from 'react-responsive';
9 | import { Segment, Form, Button, Icon, Header } from 'semantic-ui-react';
10 |
11 | const LoginForm = ({ setUser, notify, isDarkMode }) => {
12 | const [credentials, setCredentials] = useState({
13 | email: '',
14 | password: '',
15 | });
16 | const [error, setError] = useState(null);
17 | const [isLoading, setIsLoading] = useState(false);
18 | const [showPass, setShowPass] = useState(false);
19 | const history = useHistory();
20 | const isMobile = useMediaQuery({ maxWidth: 767 });
21 | const { email, password } = credentials;
22 |
23 | const handleOnChange = (e) => {
24 | setCredentials({ ...credentials, [e.target.name]: e.target.value });
25 | };
26 |
27 | const handleLogin = async (e) => {
28 | e.preventDefault();
29 | try {
30 | setIsLoading(true);
31 | const user = await authService.login(credentials);
32 | setUser(user);
33 | contactService.setToken(user.token);
34 | storageService.saveUser(user);
35 | setIsLoading(false);
36 | setError(null);
37 |
38 | notify(`Welcome ${user.displayName}, you're logged in!`, {
39 | appearance: 'success',
40 | });
41 | history.push('/');
42 | } catch (err) {
43 | setIsLoading(false);
44 | const errRes = err?.response?.data;
45 |
46 | if (errRes?.error) {
47 | return setError({ message: errRes.error });
48 | } else {
49 | return setError({ message: err.message });
50 | }
51 | }
52 | };
53 |
54 | return (
55 |
61 |
62 |
63 | Login to your account
64 |
65 |
80 | setShowPass(!showPass),
94 | }
95 | }
96 | />
97 |
98 |
109 |
110 | Login
111 |
112 |
117 | Don't have an account? Register.
118 |
119 |
120 | {error && (
121 |
127 | )}
128 |
129 |
130 | );
131 | };
132 |
133 | export default LoginForm;
134 |
--------------------------------------------------------------------------------
/client/src/components/NavBar.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useLocation } from 'react-router-dom';
3 | import storageService from '../utils/localStorageHelpers';
4 | import { useMediaQuery } from 'react-responsive';
5 | import { Menu, Icon, Dropdown } from 'semantic-ui-react';
6 |
7 | const NavBar = ({ user, setUser, isDarkMode, setIsDarkMode }) => {
8 | const [iconLoading, setIconLoading] = useState(false);
9 | const location = useLocation();
10 |
11 | const isMobile = useMediaQuery({ maxWidth: 767 });
12 |
13 | const handleLogout = () => {
14 | setUser(null);
15 | storageService.logoutUser();
16 | };
17 |
18 | const handleDarkModeToggle = () => {
19 | setIsDarkMode(!isDarkMode);
20 | setIconLoading(true);
21 | storageService.saveDarkMode(!isDarkMode);
22 | setTimeout(() => setIconLoading(false), 2150);
23 | };
24 |
25 | const logoutMenu = () => {
26 | return isMobile ? (
27 |
28 |
29 |
30 |
31 | {`Hi, ${user.displayName}`}
32 |
33 |
34 |
35 | Logout
36 |
37 |
38 |
42 | Dark Mode: {isDarkMode ? 'ON' : 'OFF'}
43 |
44 |
45 |
46 | ) : (
47 | <>
48 |
49 |
50 | {`Hi, ${user.displayName}`}
51 |
52 |
53 | >
54 | );
55 | };
56 |
57 | const loginRegisterMenu = () => {
58 | return isMobile ? (
59 |
60 |
61 |
62 |
63 | Register
64 |
65 |
66 |
67 | Login
68 |
69 |
70 |
74 | Dark Mode: {isDarkMode ? 'ON' : 'OFF'}
75 |
76 |
77 |
78 | ) : (
79 | <>
80 |
87 |
94 | >
95 | );
96 | };
97 |
98 | return (
99 |
106 |
107 |
108 |
109 | Profile Store
110 |
111 |
112 | Made with by{' '}
113 |
119 | amand33p
120 |
121 |
122 |
123 |
124 | {user ? <>{logoutMenu()}> : <>{loginRegisterMenu()}>}
125 | {!isMobile && (
126 |
127 |
136 |
137 | )}
138 |
139 |
140 | );
141 | };
142 |
143 | export default NavBar;
144 |
--------------------------------------------------------------------------------
/client/src/components/RegisterForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useHistory } from 'react-router-dom';
3 | import FormError from './FormError';
4 | import DemoCredsInfo from './DemoCredsInfo';
5 | import contactService from '../services/contacts';
6 | import authService from '../services/auth';
7 | import storageService from '../utils/localStorageHelpers';
8 | import { useMediaQuery } from 'react-responsive';
9 | import { Segment, Form, Button, Icon, Header } from 'semantic-ui-react';
10 |
11 | const RegisterForm = ({ setUser, notify, isDarkMode }) => {
12 | const [userDetails, setUserDetails] = useState({
13 | displayName: '',
14 | email: '',
15 | password: '',
16 | });
17 | const [confirmPassword, setConfirmPassword] = useState('');
18 | const [error, setError] = useState(null);
19 | const [isLoading, setIsLoading] = useState(false);
20 | const [showPass, setShowPass] = useState(false);
21 | const [showConfirmPass, setShowConfirmPass] = useState(false);
22 | const history = useHistory();
23 | const isMobile = useMediaQuery({ maxWidth: 767 });
24 | const { displayName, email, password } = userDetails;
25 |
26 | const handleOnChange = (e) => {
27 | setUserDetails({ ...userDetails, [e.target.name]: e.target.value });
28 | };
29 |
30 | const handleRegister = async (e) => {
31 | if (password !== confirmPassword)
32 | return setError(`Confirm password failed. Both passwords need to match.`);
33 | e.preventDefault();
34 | try {
35 | setIsLoading(true);
36 | const user = await authService.register(userDetails);
37 | setUser(user);
38 | contactService.setToken(user.token);
39 | storageService.saveUser(user);
40 | setIsLoading(false);
41 | setError(null);
42 |
43 | notify(`Welcome ${user.displayName}, you have successfully registered!`, {
44 | appearance: 'success',
45 | });
46 | history.push('/');
47 | } catch (err) {
48 | setIsLoading(false);
49 | const errRes = err?.response?.data;
50 |
51 | if (errRes?.error) {
52 | return setError(errRes.error);
53 | } else {
54 | return setError(err.message);
55 | }
56 | }
57 | };
58 |
59 | return (
60 |
66 |
67 |
68 | Create an account
69 |
70 |
85 |
96 | setShowPass(!showPass),
110 | }
111 | }
112 | />
113 | setConfirmPassword(target.value)}
120 | icon="lock"
121 | iconPosition="left"
122 | action={
123 | confirmPassword !== '' && {
124 | icon: showConfirmPass ? 'eye slash' : 'eye',
125 | onClick: () => setShowConfirmPass(!showConfirmPass),
126 | }
127 | }
128 | />
129 |
130 |
141 |
142 | Register
143 |
144 |
149 | Already have an account? Login.
150 |
151 |
152 | {error && }
153 |
154 |
155 | );
156 | };
157 |
158 | export default RegisterForm;
159 |
--------------------------------------------------------------------------------
/client/src/components/Routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route, Redirect } from 'react-router-dom';
3 | import Search from './Search';
4 | import AddContactModal from '../components/AddContactModal';
5 | import ContactsDisplay from '../components/ContactsDisplay';
6 | import RegisterForm from '../components/RegisterForm';
7 | import LoginForm from '../components/LoginForm';
8 | import storageService from '../utils/localStorageHelpers';
9 |
10 | const Routes = ({
11 | contacts,
12 | setContacts,
13 | user,
14 | setUser,
15 | search,
16 | setSearch,
17 | options,
18 | handleOptionAddition,
19 | notify,
20 | isLoading,
21 | isDarkMode,
22 | }) => {
23 | return (
24 |
25 |
26 | {storageService.loadUser() || user ? (
27 | <>
28 |
33 |
40 |
50 | >
51 | ) : (
52 |
53 | )}
54 |
55 |
56 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default Routes;
70 |
--------------------------------------------------------------------------------
/client/src/components/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input } from 'semantic-ui-react';
3 |
4 | const Search = ({ search, setSearch, isDarkMode }) => {
5 | const handleSearch = (e) => {
6 | setSearch(e.target.value);
7 | };
8 |
9 | return (
10 |
11 | setSearch(''),
26 | }
27 | }
28 | />
29 |
30 | );
31 | };
32 |
33 | export default Search;
34 |
--------------------------------------------------------------------------------
/client/src/data/demoCreds.js:
--------------------------------------------------------------------------------
1 | const demoCredentials = "Email: 'test@test.com' & password: 'password'";
2 |
3 | export default demoCredentials;
4 |
--------------------------------------------------------------------------------
/client/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 | width: calc(100vw - 34px);
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
16 | .container {
17 | margin-bottom: 2em;
18 | }
19 |
20 | .main-segment {
21 | width: 100vw;
22 | display: flex;
23 | flex-direction: column;
24 | flex: 1;
25 | min-height: 100vh;
26 | padding: 0 !important;
27 | }
28 |
29 | a {
30 | color: #009c95;
31 | }
32 |
33 | a:hover {
34 | color: #06746e;
35 | filter: brightness(60%);
36 | }
37 |
38 | .nav-bar {
39 | height: 4em !important;
40 | position: sticky;
41 | top: 1px;
42 | z-index: 10;
43 | }
44 |
45 | .nav-bar .menu .item {
46 | font-size: 1.2em !important;
47 | }
48 |
49 | .nav-bar .menu a:hover {
50 | color: rgb(7, 63, 63) !important;
51 | }
52 |
53 | .nav-title {
54 | display: flex;
55 | flex-direction: column;
56 | }
57 |
58 | .nav-logo {
59 | height: 25px !important;
60 | font-size: 1.3em;
61 | margin-bottom: 0.2em;
62 | align-self: flex-start;
63 | }
64 |
65 | .nav-link {
66 | color: rgb(9, 85, 85);
67 | }
68 |
69 | .nav-link:hover {
70 | color: rgba(9, 85, 85, 50);
71 | }
72 |
73 | .name-header {
74 | margin-left: 8px;
75 | }
76 |
77 | .search-card {
78 | margin-bottom: 1em !important;
79 | }
80 |
81 | .contacts-display {
82 | margin-top: 20px;
83 | }
84 |
85 | .modal {
86 | padding: 10px;
87 | }
88 |
89 | .avatar-label {
90 | margin-top: 3px !important;
91 | margin-left: 0px !important;
92 | }
93 |
94 | .avatar-image:hover {
95 | cursor: pointer;
96 | }
97 |
98 | .avatar-preview {
99 | margin: 0 auto;
100 | }
101 |
102 | .upload-preview {
103 | display: block;
104 | margin: 1.5em auto;
105 | }
106 |
107 | .clear-preview-btn {
108 | margin-top: 0.3em !important;
109 | }
110 |
111 | .card-header {
112 | display: flex !important;
113 | justify-content: space-between !important;
114 | align-items: center !important;
115 | }
116 |
117 | .contact-edit-btn {
118 | margin-right: 1em !important;
119 | }
120 |
121 | .edit-btn .icon {
122 | color: #49769c !important;
123 | }
124 |
125 | .delete-btn .icon {
126 | color: #db2828 !important;
127 | }
128 |
129 | .login-reg-card {
130 | padding-left: 14em !important;
131 | padding-right: 14em !important;
132 | padding-bottom: 2em !important;
133 | }
134 |
135 | .main-text {
136 | margin-top: 3em !important;
137 | }
138 |
139 | .auth-form {
140 | margin-bottom: 2em;
141 | }
142 |
143 | .dark-mode-input input {
144 | background: #1b1c1d !important;
145 | color: rgb(233, 230, 230) !important;
146 | border: 1px solid rgb(233, 230, 230) !important;
147 | }
148 |
149 | .dark-mode-card .content {
150 | background: #1b1c1d !important;
151 | color: rgb(233, 230, 230) !important;
152 | }
153 |
154 | .dark-mode-card .content strong {
155 | color: rgb(233, 230, 230) !important;
156 | }
157 |
158 | .dark-mode-modal {
159 | background: #1b1c1d !important;
160 | color: rgb(233, 230, 230) !important;
161 | }
162 |
163 | .dark-mode-modal .header {
164 | background: #1b1c1d !important;
165 | color: rgb(233, 230, 230) !important;
166 | }
167 |
168 | .dark-mode-modal .content {
169 | background: #1b1c1d !important;
170 | color: rgb(233, 230, 230) !important;
171 | }
172 |
173 | .dark-mode-modal label {
174 | background: #1b1c1d !important;
175 | color: rgb(233, 230, 230) !important;
176 | }
177 |
178 | .dark-mode-modal input {
179 | background: #1b1c1d !important;
180 | color: rgb(233, 230, 230) !important;
181 | border: 1px solid white !important;
182 | }
183 |
184 | .dark-mode-modal .field .dropdown {
185 | background: #1b1c1d !important;
186 | color: rgb(233, 230, 230) !important;
187 | border: 1px solid white !important;
188 | }
189 |
190 | .dark-mode-modal .field .dropdown i {
191 | color: rgb(233, 230, 230) !important;
192 | border-width: 0 !important;
193 | border-bottom-width: 1px !important;
194 | }
195 |
196 | .dark-mode-modal .field .dropdown .menu .item {
197 | background: #1b1c1d !important;
198 | color: rgb(233, 230, 230) !important;
199 | }
200 |
201 | .dark-mode-modal .field .dropdown .menu .selected {
202 | color: rgb(194, 229, 229) !important;
203 | background-color: rgb(70, 70, 63) !important;
204 | }
205 |
206 | .dark-mode-modal .field .dropdown .menu .item:hover {
207 | background-color: rgb(95, 95, 91) !important;
208 | }
209 |
210 | .dark-mode-modal i {
211 | color: rgb(233, 230, 230) !important;
212 | }
213 |
214 | .dark-mode-modal label i {
215 | color: black !important;
216 | }
217 |
218 | .dark-mode-modal .actions {
219 | background: #1b1c1d !important;
220 | }
221 |
222 | .dark-mode-auth-form label {
223 | color: rgb(233, 230, 230) !important;
224 | }
225 |
226 | .dark-mode-auth-form .header {
227 | color: rgb(233, 230, 230) !important;
228 | }
229 |
230 | .dark-mode-auth-form input {
231 | background: #1b1c1d !important;
232 | color: rgb(233, 230, 230) !important;
233 | border: 1px solid rgb(233, 230, 230) !important;
234 | }
235 |
236 | .dark-mode-auth-form .input i {
237 | color: rgb(233, 230, 230) !important;
238 | }
239 |
240 | .dark-mode-auth-form input:autofill {
241 | color: #1b1c1d !important;
242 | border: 1px solid rgb(233, 230, 230) !important;
243 | }
244 |
245 | .dark-mode-auth-form .input .button i {
246 | color: black !important;
247 | }
248 |
249 | .dark-mode-clear-btn i {
250 | color: black !important;
251 | }
252 |
253 | .dark-mode-info-text {
254 | color: rgb(233, 230, 230) !important;
255 | }
256 |
257 | .dark-mode-menu {
258 | background: #24282c !important;
259 | }
260 |
261 | .dark-mode-menu .item * {
262 | color: rgb(233, 230, 230) !important;
263 | }
264 |
265 | .dark-mode-segment {
266 | border: 1px solid rgb(233, 230, 230) !important;
267 | }
268 |
269 | @media only screen and (max-width: 768px) {
270 | body {
271 | width: 100vw;
272 | }
273 |
274 | .edit-btn {
275 | padding: 5px !important;
276 | }
277 |
278 | .delete-btn {
279 | padding: 5px !important;
280 | }
281 |
282 | .contact-edit-btn {
283 | padding: 8px !important;
284 | }
285 |
286 | .contact-del-btn {
287 | padding: 8px !important;
288 | }
289 |
290 | .login-reg-card {
291 | padding-left: 1em !important;
292 | padding-right: 1em !important;
293 | padding-bottom: 1em !important;
294 | }
295 |
296 | .login-reg-bottom-text {
297 | margin-top: 4em !important;
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/client/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 { BrowserRouter as Router } from 'react-router-dom';
6 | import { ToastProvider } from 'react-toast-notifications';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/client/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import backendUrl from '../backendUrl';
3 |
4 | const login = async (credentials) => {
5 | const response = await axios.post(`${backendUrl}/api/login`, credentials);
6 | return response.data;
7 | };
8 |
9 | const register = async (enteredData) => {
10 | const response = await axios.post(`${backendUrl}/api/register`, enteredData);
11 | return response.data;
12 | };
13 |
14 | export default { login, register };
15 |
--------------------------------------------------------------------------------
/client/src/services/contacts.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import backendUrl from '../backendUrl';
3 |
4 | const baseUrl = `${backendUrl}/api/contacts`;
5 |
6 | let token = null;
7 |
8 | const setToken = (newToken) => {
9 | token = newToken;
10 | };
11 |
12 | const setConfig = () => {
13 | return {
14 | headers: { 'x-auth-token': token },
15 | };
16 | };
17 |
18 | const getAll = async () => {
19 | const response = await axios.get(baseUrl, setConfig());
20 | return response.data;
21 | };
22 |
23 | const addNew = async (contactObj) => {
24 | const response = await axios.post(baseUrl, contactObj, setConfig());
25 | return response.data;
26 | };
27 |
28 | const deleteContact = async (id) => {
29 | const response = await axios.delete(`${baseUrl}/${id}`, setConfig());
30 | return response.data;
31 | };
32 |
33 | const editContact = async (id, contactObj) => {
34 | const response = await axios.patch(
35 | `${baseUrl}/${id}/name_dp`,
36 | contactObj,
37 | setConfig()
38 | );
39 | return response.data;
40 | };
41 |
42 | const addLink = async (id, linkObj) => {
43 | const response = await axios.post(
44 | `${baseUrl}/${id}/url`,
45 | linkObj,
46 | setConfig()
47 | );
48 | return response.data;
49 | };
50 |
51 | const editLink = async (id, urlId, linkObj) => {
52 | const response = await axios.patch(
53 | `${baseUrl}/${id}/url/${urlId}`,
54 | linkObj,
55 | setConfig()
56 | );
57 | return response.data;
58 | };
59 |
60 | const deleteLink = async (id, urlId) => {
61 | const response = await axios.delete(
62 | `${baseUrl}/${id}/url/${urlId}`,
63 | setConfig()
64 | );
65 | return response.data;
66 | };
67 |
68 | export default {
69 | setToken,
70 | getAll,
71 | addNew,
72 | deleteContact,
73 | editContact,
74 | addLink,
75 | editLink,
76 | deleteLink,
77 | };
78 |
--------------------------------------------------------------------------------
/client/src/utils/arraysAndFuncs.js:
--------------------------------------------------------------------------------
1 | export const optionsArray = [
2 | { key: 'fb', text: 'Facebook', value: 'Facebook', icon: 'facebook' },
3 | { key: 'ig', text: 'Instagram', value: 'Instagram', icon: 'instagram' },
4 | { key: 'tw', text: 'Twitter', value: 'Twitter', icon: 'twitter' },
5 | { key: 'gh', text: 'Github', value: 'Github', icon: 'github' },
6 | { key: 'yt', text: 'Youtube', value: 'Youtube', icon: 'youtube' },
7 | ];
8 |
9 | export const siteIconsArray = [
10 | 'facebook',
11 | 'github',
12 | 'youtube',
13 | 'twitter',
14 | 'instagram',
15 | 'blogger',
16 | 'linkedin',
17 | 'reddit',
18 | 'medium',
19 | 'telegram',
20 | 'stack overflow',
21 | 'spotify',
22 | ];
23 |
24 | const labelColors = [
25 | 'red',
26 | 'orange',
27 | 'yellow',
28 | 'olive',
29 | 'green',
30 | 'teal',
31 | 'blue',
32 | 'violet',
33 | 'purple',
34 | 'pink',
35 | 'brown',
36 | 'grey',
37 | 'black',
38 | ];
39 |
40 | export const randomColor = () => {
41 | return labelColors[Math.floor(Math.random() * labelColors.length)];
42 | };
43 |
44 | export const getCircularAvatar = (imageLink) => {
45 | const firstPart = imageLink.split('image/upload/')[0];
46 | const secondPart = imageLink.split('image/upload/')[1];
47 | const transformApi = 'w_200,h_200,c_fill,r_max/e_trim/';
48 |
49 | return [firstPart, transformApi, secondPart].join('');
50 | };
51 |
52 | export const generateBase64Encode = (file, setState) => {
53 | const reader = new FileReader();
54 | reader.readAsDataURL(file);
55 | reader.onloadend = () => {
56 | setState(reader.result);
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/utils/localStorageHelpers.js:
--------------------------------------------------------------------------------
1 | const storageKeyToken = 'profileStoreUserKey';
2 | const storageKeyDarkMode = 'profileStoreDarkMode';
3 |
4 | const saveUser = (user) =>
5 | localStorage.setItem(storageKeyToken, JSON.stringify(user));
6 |
7 | const loadUser = () => JSON.parse(localStorage.getItem(storageKeyToken));
8 |
9 | const logoutUser = () => localStorage.removeItem(storageKeyToken);
10 |
11 | const saveDarkMode = (boolean) =>
12 | localStorage.setItem(storageKeyDarkMode, boolean);
13 |
14 | const loadDarkMode = () => localStorage.getItem(storageKeyDarkMode);
15 |
16 | export default {
17 | saveUser,
18 | loadUser,
19 | logoutUser,
20 | saveDarkMode,
21 | loadDarkMode,
22 | };
23 |
--------------------------------------------------------------------------------
/screenshots/auth-forms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/auth-forms.png
--------------------------------------------------------------------------------
/screenshots/desktop-tablet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/desktop-tablet.png
--------------------------------------------------------------------------------
/screenshots/mobile-ui-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/mobile-ui-1.png
--------------------------------------------------------------------------------
/screenshots/mobile-ui-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/mobile-ui-2.png
--------------------------------------------------------------------------------
/screenshots/modals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/modals.png
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "commonjs": true,
6 | "es6": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "ecmaVersion": 12
11 | },
12 | "rules": {}
13 | }
14 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | require('express-async-errors');
3 | const cors = require('cors');
4 | const middleware = require('./utils/middleware');
5 | const authRoutes = require('./routes/auth');
6 | const contactRoutes = require('./routes/contact');
7 |
8 | const app = express();
9 |
10 | app.use(cors());
11 | app.use(express.json({ limit: '10mb' }));
12 | app.use(express.urlencoded({ limit: '10mb', extended: true }));
13 |
14 | app.use('/api', authRoutes);
15 | app.use('/api/contacts', contactRoutes);
16 |
17 | app.use(middleware.unknownEndpointHandler);
18 | app.use(middleware.errorHandler);
19 |
20 | module.exports = app;
21 |
--------------------------------------------------------------------------------
/server/controllers/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const bcrypt = require('bcrypt');
3 | const User = require('../models/user');
4 | const validator = require('validator');
5 | const { SECRET } = require('../utils/config');
6 |
7 | const loginUser = async (req, res) => {
8 | const { email, password } = req.body;
9 |
10 | const user = await User.findOne({ email });
11 |
12 | if (!user) {
13 | return res
14 | .status(400)
15 | .send({ error: 'No account with this email has been registered.' });
16 | }
17 |
18 | const credentialsValid = await bcrypt.compare(password, user.passwordHash);
19 |
20 | if (!credentialsValid) {
21 | return res.status(401).send({ error: 'Invalid credentials.' });
22 | }
23 |
24 | const payloadForToken = {
25 | id: user._id,
26 | };
27 |
28 | const token = jwt.sign(payloadForToken, SECRET);
29 |
30 | res
31 | .status(200)
32 | .send({ token, displayName: user.displayName, email: user.email });
33 | };
34 |
35 | const registerUser = async (req, res) => {
36 | const { displayName, email, password } = req.body;
37 |
38 | if (!password || password.length < 6) {
39 | return res
40 | .status(400)
41 | .send({ error: 'Password needs to be atleast 6 characters long.' });
42 | }
43 |
44 | if (!email || !validator.isEmail(email)) {
45 | return res.status(400).send({ error: 'Valid email address is required.' });
46 | }
47 |
48 | const existingUser = await User.findOne({ email });
49 |
50 | if (existingUser) {
51 | return res
52 | .status(400)
53 | .send({ error: 'An account with this email already exists.' });
54 | }
55 |
56 | const saltRounds = 10;
57 | const passwordHash = await bcrypt.hash(password, saltRounds);
58 |
59 | const user = new User({
60 | displayName,
61 | email,
62 | passwordHash,
63 | });
64 |
65 | const savedUser = await user.save();
66 |
67 | const payloadForToken = {
68 | id: savedUser._id,
69 | };
70 |
71 | const token = jwt.sign(payloadForToken, SECRET);
72 | res.status(200).send({
73 | token,
74 | displayName: savedUser.displayName,
75 | email: savedUser.email,
76 | });
77 | };
78 |
79 | module.exports = { loginUser, registerUser };
80 |
--------------------------------------------------------------------------------
/server/controllers/contact.js:
--------------------------------------------------------------------------------
1 | const Contact = require('../models/contact');
2 | const User = require('../models/user');
3 | const validator = require('validator');
4 | const { cloudinary, UPLOAD_PRESET } = require('../utils/config');
5 |
6 | const getContacts = async (req, res) => {
7 | const allContacts = await Contact.find({ user: req.user });
8 | res.json(allContacts);
9 | };
10 |
11 | const createNewContact = async (req, res) => {
12 | const { name, contacts, displayPicture } = req.body;
13 |
14 | if (!contacts.url || !validator.isURL(contacts.url)) {
15 | return res
16 | .status(401)
17 | .send({ error: 'Valid URL is required for link field.' });
18 | }
19 |
20 | if (!contacts.site) {
21 | return res.status(401).send({ error: 'Site name is required.' });
22 | }
23 |
24 | const user = await User.findById(req.user);
25 |
26 | if (!user) {
27 | return res.status(404).send({ error: 'User does not exist in database.' });
28 | }
29 |
30 | const newPerson = new Contact({
31 | name,
32 | contacts,
33 | user: user._id,
34 | });
35 |
36 | if (displayPicture) {
37 | const uploadedImage = await cloudinary.uploader.upload(
38 | displayPicture,
39 | {
40 | upload_preset: UPLOAD_PRESET,
41 | },
42 | (error) => {
43 | if (error) return res.status(401).send({ error: error.message });
44 | }
45 | );
46 |
47 | newPerson.displayPicture = {
48 | exists: true,
49 | link: uploadedImage.url,
50 | public_id: uploadedImage.public_id,
51 | };
52 | }
53 |
54 | const savedPerson = await newPerson.save();
55 | return res.status(201).json(savedPerson);
56 | };
57 |
58 | const deleteContact = async (req, res) => {
59 | const { id } = req.params;
60 |
61 | const user = await User.findById(req.user);
62 | const person = await Contact.findById(id);
63 |
64 | if (!user) {
65 | return res.status(404).send({ error: 'User does not exist in database.' });
66 | }
67 |
68 | if (!person) {
69 | return res
70 | .status(404)
71 | .send({ error: `Contact with ID: ${id} does not exist in database.` });
72 | }
73 |
74 | if (person.user.toString() !== user._id.toString()) {
75 | return res.status(401).send({ error: 'Access is denied.' });
76 | }
77 |
78 | if (person.displayPicture.exists === true) {
79 | await cloudinary.uploader.destroy(
80 | person.displayPicture.public_id,
81 | (error) => {
82 | if (error) res.status(401).send({ error: error.message });
83 | }
84 | );
85 | }
86 |
87 | await Contact.findByIdAndDelete(id);
88 | res.status(204).end();
89 | };
90 |
91 | const updateContactNameDP = async (req, res) => {
92 | const { id } = req.params;
93 | const { name, displayPicture } = req.body;
94 |
95 | if (!name) {
96 | return res.status(401).send({ error: 'Name field is required.' });
97 | }
98 |
99 | const user = await User.findById(req.user);
100 | const person = await Contact.findById(id);
101 |
102 | if (!user) {
103 | return res.status(404).send({ error: 'User does not exist in database.' });
104 | }
105 |
106 | if (!person) {
107 | return res
108 | .status(404)
109 | .send({ error: `Contact with ID: ${id} does not exist in database.` });
110 | }
111 |
112 | if (person.user.toString() !== user._id.toString()) {
113 | return res.status(401).send({ error: 'Access is denied.' });
114 | }
115 |
116 | if (displayPicture) {
117 | const uploadedImage = await cloudinary.uploader.upload(
118 | displayPicture,
119 | {
120 | upload_preset: UPLOAD_PRESET,
121 | },
122 | (error) => {
123 | if (error) return res.status(401).send({ error: error.message });
124 | }
125 | );
126 |
127 | await cloudinary.uploader.destroy(
128 | person.displayPicture.public_id,
129 | (error) => {
130 | if (error) res.status(401).send({ error: error.message });
131 | }
132 | );
133 |
134 | person.displayPicture = {
135 | exists: true,
136 | link: uploadedImage.url,
137 | public_id: uploadedImage.public_id,
138 | };
139 | }
140 |
141 | person.name = name;
142 |
143 | await person.save();
144 | res.status(202).json(person);
145 | };
146 |
147 | const addProfileUrl = async (req, res) => {
148 | const { id } = req.params;
149 | const { url, site } = req.body;
150 |
151 | if (!site) {
152 | return res.status(401).send({ error: 'Site name is required.' });
153 | }
154 |
155 | if (!url || !validator.isURL(url)) {
156 | return res
157 | .status(401)
158 | .send({ error: 'Valid URL is required for link field.' });
159 | }
160 |
161 | const user = await User.findById(req.user);
162 | const person = await Contact.findById(id);
163 |
164 | if (!user) {
165 | return res.status(404).send({ error: 'User does not exist in database.' });
166 | }
167 |
168 | if (!person) {
169 | return res
170 | .status(404)
171 | .send({ error: `Contact with ID: ${id} does not exist in database.` });
172 | }
173 |
174 | if (person.user.toString() !== user._id.toString()) {
175 | return res.status(401).send({ error: 'Access is denied.' });
176 | }
177 |
178 | const newContact = {
179 | url,
180 | site,
181 | };
182 |
183 | person.contacts = [...person.contacts, newContact];
184 | const savedPerson = await person.save();
185 |
186 | res.status(201).json(savedPerson);
187 | };
188 |
189 | const updateProfileUrl = async (req, res) => {
190 | const { id } = req.params;
191 | const { urlId } = req.params;
192 | const { url, site } = req.body;
193 |
194 | if (!site) {
195 | return res.status(401).send({ error: 'Site name is required.' });
196 | }
197 |
198 | if (!url || !validator.isURL(url)) {
199 | return res
200 | .status(401)
201 | .send({ error: 'Valid URL is required for link field.' });
202 | }
203 |
204 | const user = await User.findById(req.user);
205 | const person = await Contact.findById(id);
206 |
207 | if (!user) {
208 | return res.status(404).send({ error: 'User does not exist in database.' });
209 | }
210 |
211 | if (!person) {
212 | return res
213 | .status(404)
214 | .send({ error: `Contact with ID: ${id} does not exist in database.` });
215 | }
216 |
217 | if (person.user.toString() !== user._id.toString()) {
218 | return res.status(401).send({ error: 'Access is denied.' });
219 | }
220 |
221 | const urlToUpdate = person.contacts.find((c) => c.id === urlId);
222 |
223 | if (!urlToUpdate) {
224 | return res
225 | .status(404)
226 | .send({ error: `URL with ID: ${urlId} does not exist in database.` });
227 | }
228 |
229 | urlToUpdate.url = url;
230 | urlToUpdate.site = site;
231 |
232 | person.contacts = person.contacts.map((c) =>
233 | c.id !== urlId ? c : urlToUpdate
234 | );
235 |
236 | await person.save();
237 | res.status(202).json(urlToUpdate);
238 | };
239 |
240 | const deleteProfileUrl = async (req, res) => {
241 | const { id } = req.params;
242 | const { urlId } = req.params;
243 |
244 | const user = await User.findById(req.user);
245 | const person = await Contact.findById(id);
246 |
247 | if (!user) {
248 | return res.status(404).send({ error: 'User does not exist in database.' });
249 | }
250 |
251 | if (!person) {
252 | return res
253 | .status(404)
254 | .send({ error: `Contact with ID: ${id} does not exist in database.` });
255 | }
256 |
257 | if (person.user.toString() !== user._id.toString()) {
258 | return res.status(401).send({ error: 'Access is denied.' });
259 | }
260 |
261 | person.contacts = person.contacts.filter((c) => c.id !== urlId);
262 |
263 | await person.save();
264 | res.status(204).end();
265 | };
266 |
267 | module.exports = {
268 | getContacts,
269 | createNewContact,
270 | deleteContact,
271 | updateContactNameDP,
272 | addProfileUrl,
273 | deleteProfileUrl,
274 | updateProfileUrl,
275 | };
276 |
--------------------------------------------------------------------------------
/server/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const { MONGODB_URI: url } = require('./utils/config');
3 |
4 | const connectToDB = async () => {
5 | try {
6 | await mongoose.connect(url, {
7 | useNewUrlParser: true,
8 | useUnifiedTopology: true,
9 | useCreateIndex: true,
10 | useFindAndModify: false,
11 | });
12 |
13 | console.log('Connected to MongoDB!');
14 | } catch (error) {
15 | console.error(`Error while connecting to MongoDB: `, error.message);
16 | }
17 | };
18 |
19 | module.exports = connectToDB;
20 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const app = require('./app');
2 | const http = require('http');
3 | const { PORT } = require('./utils/config');
4 | const connectToDB = require('./db');
5 |
6 | connectToDB();
7 |
8 | const server = http.createServer(app);
9 |
10 | server.listen(PORT, () => {
11 | console.log(`Server running on port ${PORT}`);
12 | });
13 |
--------------------------------------------------------------------------------
/server/models/contact.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const urlSchema = new mongoose.Schema({
5 | url: {
6 | type: String,
7 | required: true,
8 | trim: true,
9 | },
10 | site: {
11 | type: String,
12 | required: true,
13 | trim: true,
14 | },
15 | });
16 |
17 | const contactSchema = new mongoose.Schema({
18 | name: {
19 | type: String,
20 | required: true,
21 | trim: true,
22 | },
23 | contacts: [urlSchema],
24 | user: {
25 | type: mongoose.Schema.Types.ObjectId,
26 | ref: 'User',
27 | },
28 | displayPicture: {
29 | exists: {
30 | type: Boolean,
31 | required: true,
32 | default: 'false',
33 | },
34 | link: {
35 | type: String,
36 | required: true,
37 | default: 'null',
38 | },
39 | public_id: {
40 | type: String,
41 | required: true,
42 | default: 'null',
43 | },
44 | },
45 | });
46 |
47 | contactSchema.plugin(uniqueValidator);
48 |
49 | // replaces _id with id, convert id to string from ObjectID and deletes __v
50 | contactSchema.set('toJSON', {
51 | transform: (_document, returnedObject) => {
52 | returnedObject.id = returnedObject._id.toString();
53 | delete returnedObject._id;
54 | delete returnedObject.__v;
55 | },
56 | });
57 |
58 | urlSchema.set('toJSON', {
59 | transform: (_document, returnedObject) => {
60 | returnedObject.id = returnedObject._id.toString();
61 | delete returnedObject._id;
62 | },
63 | });
64 |
65 | module.exports = mongoose.model('Contact', contactSchema);
66 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const userSchema = new mongoose.Schema({
5 | displayName: {
6 | type: String,
7 | minlength: 3,
8 | maxlength: 15,
9 | required: true,
10 | trim: true,
11 | },
12 | email: {
13 | type: String,
14 | required: true,
15 | unique: true,
16 | trim: true,
17 | },
18 | passwordHash: {
19 | type: String,
20 | required: true,
21 | },
22 | });
23 |
24 | userSchema.plugin(uniqueValidator);
25 |
26 | // replaces _id with id, convert id to string from ObjectID and deletes __v
27 | userSchema.set('toJSON', {
28 | transform: (_document, returnedObject) => {
29 | returnedObject.id = returnedObject._id.toString();
30 | delete returnedObject._id;
31 | delete returnedObject.__v;
32 | delete returnedObject.passwordHash;
33 | },
34 | });
35 |
36 | module.exports = mongoose.model('User', userSchema);
37 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "profile_project_repo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "dependencies": {
7 | "bcrypt": "^5.0.0",
8 | "cloudinary": "^1.23.0",
9 | "cors": "^2.8.5",
10 | "dotenv": "^8.2.0",
11 | "express": "^4.17.1",
12 | "express-async-errors": "^3.1.1",
13 | "jsonwebtoken": "^8.5.1",
14 | "mongoose": "^5.9.25",
15 | "mongoose-unique-validator": "^2.0.3",
16 | "validator": "^13.1.1"
17 | },
18 | "scripts": {
19 | "start": "node index.js",
20 | "dev": "nodemon index.js",
21 | "build:ui": "rm -rf build && cd ../client && rm -rf build && npm run build --prod && cp -r build ../server"
22 | },
23 | "keywords": [],
24 | "author": "",
25 | "license": "ISC",
26 | "devDependencies": {
27 | "eslint": "^7.18.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { loginUser, registerUser } = require('../controllers/auth');
3 |
4 | const router = express.Router();
5 |
6 | router.post('/register', registerUser);
7 | router.post('/login', loginUser);
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/server/routes/contact.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const {
3 | getContacts,
4 | createNewContact,
5 | deleteContact,
6 | updateContactNameDP,
7 | addProfileUrl,
8 | deleteProfileUrl,
9 | updateProfileUrl,
10 | } = require('../controllers/contact');
11 | const { auth } = require('../utils/middleware');
12 |
13 | const router = express.Router();
14 |
15 | router.get('/', auth, getContacts);
16 | router.post('/', auth, createNewContact);
17 | router.delete('/:id', auth, deleteContact);
18 | router.patch('/:id/name_dp', auth, updateContactNameDP);
19 | router.post('/:id/url', auth, addProfileUrl);
20 | router.patch('/:id/url/:urlId', auth, updateProfileUrl);
21 | router.delete('/:id/url/:urlId', auth, deleteProfileUrl);
22 |
23 | module.exports = router;
24 |
--------------------------------------------------------------------------------
/server/utils/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const cloudinary = require('cloudinary').v2;
3 |
4 | const PORT = process.env.PORT;
5 | const MONGODB_URI = process.env.MONGODB_URI;
6 | const SECRET = process.env.SECRET;
7 | const UPLOAD_PRESET = process.env.UPLOAD_PRESET || 'ml_default';
8 |
9 | cloudinary.config({
10 | cloud_name: process.env.CLOUDINARY_NAME,
11 | api_key: process.env.CLOUDINARY_API_KEY,
12 | api_secret: process.env.CLOUDINARY_API_SECRET,
13 | });
14 |
15 | module.exports = {
16 | PORT,
17 | MONGODB_URI,
18 | SECRET,
19 | cloudinary,
20 | UPLOAD_PRESET,
21 | };
22 |
--------------------------------------------------------------------------------
/server/utils/middleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { SECRET } = require('../utils/config');
3 |
4 | const auth = (req, res, next) => {
5 | try {
6 | const token = req.header('x-auth-token');
7 |
8 | if (!token) {
9 | return res
10 | .status(401)
11 | .send({ error: 'No auth token found. Authorization denied.' });
12 | }
13 |
14 | const decodedToken = jwt.verify(token, SECRET);
15 |
16 | if (!decodedToken.id) {
17 | return res
18 | .status(401)
19 | .send({ error: 'Token verification failed. Authorization denied.' });
20 | }
21 |
22 | req.user = decodedToken.id;
23 |
24 | next();
25 | } catch (error) {
26 | res.status(500).send({ error: error.message });
27 | }
28 | };
29 |
30 | const unknownEndpointHandler = (_req, res) => {
31 | res.status(404).send({ error: 'Unknown endpoint.' });
32 | };
33 |
34 | const errorHandler = (error, _req, res, next) => {
35 | console.error(error.message);
36 |
37 | if (error.name === 'CastError' && error.kind === 'ObjectId') {
38 | return res.status(400).send({ error: 'Malformatted ID.' });
39 | } else if (error.name === 'ValidationError') {
40 | return res.status(400).send({ error: error.message });
41 | } else if (error.name === 'JsonWebTokenError') {
42 | return res.status(401).send({ error: 'Invalid token.' });
43 | }
44 |
45 | next(error);
46 | };
47 |
48 | module.exports = {
49 | auth,
50 | unknownEndpointHandler,
51 | errorHandler,
52 | };
53 |
--------------------------------------------------------------------------------