├── .DS_Store
├── .gitignore
├── README.md
├── preview.gif
├── public
├── .DS_Store
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── .DS_Store
│ ├── App.js
│ ├── assets
│ ├── loader.gif
│ ├── logo.svg
│ └── robot.gif
│ ├── components
│ ├── ChatContainer.jsx
│ ├── ChatInput.jsx
│ ├── Contacts.jsx
│ ├── Logout.jsx
│ ├── Messages.jsx
│ └── Welcome.jsx
│ ├── index.css
│ ├── index.js
│ ├── pages
│ ├── Chats.jsx
│ ├── Login.jsx
│ ├── Register.jsx
│ └── SetAvatar.jsx
│ └── utils
│ └── APIRoutes.js
└── server
├── .DS_Store
├── .env
├── .gitignore
├── controllers
├── messagesController.js
└── usersController.js
├── index.js
├── models
├── messageModel.js
└── userModel.js
├── package-lock.json
├── package.json
└── routes
├── messagesRoute.js
└── userRoutes.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | server/.env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reactchatapp
2 | Realtime Chat App
3 |
4 |
5 |
6 |
7 | ## Features/Components
8 | - Uses React Js for UI
9 | - Backend using NodeJs
10 | - Authentication using JWT
11 | - MongoDB for database
12 | - Socket.io for realtime communication
13 | - CSS - styled components
14 | - Responsive
15 |
16 | ## Installation
17 | Once you have downloaded or cloned this repository, run `npm install` inside the directory.
18 | Change the mongoDB url.
19 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/preview.gif
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/.DS_Store
--------------------------------------------------------------------------------
/public/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules
3 |
--------------------------------------------------------------------------------
/public/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "axios": "^0.27.2",
10 | "buffer": "^6.0.3",
11 | "emoji-picker-react": "^3.6.1",
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0",
14 | "react-icons": "^4.4.0",
15 | "react-router-dom": "^6.3.0",
16 | "react-scripts": "5.0.1",
17 | "react-toastify": "^9.0.8",
18 | "socket.io-client": "^4.5.1",
19 | "styled-components": "^5.3.5",
20 | "uuid": "^8.3.2",
21 | "web-vitals": "^2.1.4"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/public/favicon.ico
--------------------------------------------------------------------------------
/public/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/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/public/logo192.png
--------------------------------------------------------------------------------
/public/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/public/logo512.png
--------------------------------------------------------------------------------
/public/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/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/src/.DS_Store
--------------------------------------------------------------------------------
/public/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | BrowserRouter,
4 | Route,
5 | Routes
6 | } from "react-router-dom"
7 | import Register from "../src/pages/Register";
8 | import Login from "../src/pages/Login";
9 | import Chats from "../src/pages/Chats";
10 | import SetAvatar from './pages/SetAvatar';
11 |
12 |
13 | export default function App() {
14 | return (
15 |
16 |
17 | }/>
18 | }/>
19 | }/>
20 | }/>
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/public/src/assets/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/src/assets/loader.gif
--------------------------------------------------------------------------------
/public/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
103 |
--------------------------------------------------------------------------------
/public/src/assets/robot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/public/src/assets/robot.gif
--------------------------------------------------------------------------------
/public/src/components/ChatContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect , useRef} from 'react'
2 | import styled from "styled-components"
3 | import ChatInput from './ChatInput';
4 | import Logout from './Logout';
5 | import axios from "axios";
6 | import { getAllMessagesRoute, sendMessageRoute } from '../utils/APIRoutes'
7 | import { v4 as uuidv4} from "uuid";
8 |
9 | export default function ChatContainer({ currentChat, currentUser, socket }) {
10 | const [messages, setMessages] = useState([]);
11 | const [arrivalMessage, setArrivalMessage] = useState(null);
12 | const scrollRef = useRef();
13 |
14 | useEffect(() => {
15 |
16 | const fetchData = async () => {
17 | if(currentChat){
18 | const response = await axios.post(getAllMessagesRoute, {
19 | from: currentUser._id,
20 | to: currentChat._id,
21 | });
22 | setMessages(response.data);
23 | }
24 | }
25 | fetchData();
26 | }, [currentChat]);
27 |
28 | const handleSendMsg = async (msg) => {
29 | await axios.post(sendMessageRoute, {
30 | from: currentUser._id,
31 | to: currentChat._id,
32 | message: msg,
33 | });
34 | socket.current.emit("send-msg", {
35 | to: currentChat._id,
36 | from: currentUser._id,
37 | message: msg,
38 | });
39 |
40 | const msgs = [...messages];
41 | msgs.push({
42 | fromSelf: true,
43 | message: msg,
44 | });
45 | setMessages(msgs);
46 | };
47 |
48 | useEffect(() => {
49 | if (socket.current) {
50 | socket.current.on("msg-recieved", (msg) => {
51 | setArrivalMessage({
52 | fromSelf: false,
53 | message: msg,
54 | });
55 | })
56 | }
57 | }, []);
58 |
59 | useEffect(()=>{
60 | arrivalMessage && setMessages((prev)=>[...prev,arrivalMessage]);
61 | },[arrivalMessage]);
62 |
63 | useEffect(() => {
64 | scrollRef.current?.scrollIntoView({ behavior: "smooth" });
65 | }, [messages]);
66 |
67 | return (
68 | <>
69 | {
70 | currentChat && (
71 |
72 |
73 |
74 |
75 |

78 |
79 |
80 |
{currentChat.username}
81 |
82 |
83 |
84 |
85 |
86 | {messages.map((message) => {
87 | return (
88 |
89 |
95 |
96 |
{message.message}
97 |
98 |
99 |
100 | );
101 | })}
102 |
103 |
104 |
105 | )
106 | }
107 | >
108 | );
109 | }
110 |
111 | const Container = styled.div`
112 | display: grid;
113 | grid-template-rows: 10% 80% 10%;
114 | gap: 0.1rem;
115 | overflow: hidden;
116 | @media screen and (min-width: 720px) and (max-width: 1080px) {
117 | grid-template-rows: 15% 70% 15%;
118 | }
119 | .chat-header {
120 | display: flex;
121 | justify-content: space-between;
122 | align-items: center;
123 | padding: 0 2rem;
124 | .user-details {
125 | display: flex;
126 | align-items: center;
127 | gap: 1rem;
128 | .avatar {
129 | img {
130 | height: 3rem;
131 | }
132 | }
133 | .username {
134 | h3 {
135 | color: white;
136 | text-transform: capitalize;
137 | }
138 | }
139 | }
140 | }
141 | .chat-messages {
142 | padding: 1rem 2rem;
143 | display: flex;
144 | flex-direction: column;
145 | gap: 1rem;
146 | overflow: auto;
147 | &::-webkit-scrollbar {
148 | width: 0.2rem;
149 | &-thumb {
150 | background-color: #ffffff39;
151 | width: 0.1rem;
152 | border-radius: 1rem;
153 | }
154 | }
155 | .message {
156 | display: flex;
157 | align-items: center;
158 | .content {
159 | max-width: 40%;
160 | overflow-wrap: break-word;
161 | padding: 1rem;
162 | font-size: 1.1rem;
163 | border-radius: 1rem;
164 | color: #d1d1d1;
165 | @media screen and (min-width: 720px) and (max-width: 1080px) {
166 | max-width: 70%;
167 | }
168 | }
169 | }
170 | .sended {
171 | justify-content: flex-end;
172 | .content {
173 | background-color: #4f04ff21;
174 | }
175 | }
176 | .recieved {
177 | justify-content: flex-start;
178 | .content {
179 | background-color: #9900ff20;
180 | }
181 | }
182 | }
183 | `;
--------------------------------------------------------------------------------
/public/src/components/ChatInput.jsx:
--------------------------------------------------------------------------------
1 | import React,{useState} from 'react'
2 | import styled from 'styled-components'
3 | import Picker from 'emoji-picker-react'
4 | import {IoMdSend} from 'react-icons/io'
5 | import {BsEmojiSmileFill} from 'react-icons/bs'
6 |
7 | export default function ChatInput({handleSendMsg}) {
8 | const [showEmojiPicker, setShowEmojiPicker] = useState(false);
9 | const [msg, setMsg] = useState("");
10 |
11 | const handleEmojiPickerHideShow = ()=>{
12 | setShowEmojiPicker(!showEmojiPicker);
13 | };
14 |
15 | const handleEmojiClick = (e,emoji)=>{
16 | let message= msg;
17 | message += emoji.emoji;
18 | setMsg(message);
19 | }
20 |
21 | const sendChat = (e)=>{
22 | e.preventDefault();
23 | if(msg.length>0){
24 | handleSendMsg(msg);
25 | setMsg('');
26 | }
27 | }
28 | return (
29 |
30 |
31 |
32 |
33 | { showEmojiPicker && }
34 |
35 |
36 |
42 |
43 | )
44 | }
45 |
46 | const Container = styled.div`
47 | display: grid;
48 | grid-template-columns: 5% 95%;
49 | align-items: center;
50 | background-color: #080420;
51 | padding: 0 2rem;
52 | padding-bottom: 0.3rem;
53 | @media screen and (min-width: 720px) and (max-width: 1080px){
54 | padding: 0 1rem;
55 | gap: 1rem;
56 | }
57 | .button-container{
58 | display: flex;
59 | align-items: center;
60 | color: white;
61 | gap: 1rem;
62 | .emoji{
63 | position: relative;
64 | svg{
65 | font-size: 1.5rem;
66 | color: #ffff00c8;
67 | cursor: pointer;
68 | }
69 | .emoji-picker-react{
70 | position: absolute;
71 | top: -350px;
72 | background-color: #080420;
73 | box-shadow: 0 5px 10px #9a86f3;
74 | border-color: #9186f3;
75 | .emoji-scroll-wrapper::-webkit-scrollbar{
76 | background-color: #080420;
77 | width: 5px;
78 | &-thumb {
79 | background-color: #9a86f3;
80 | }
81 | }
82 | .emoji-categories{
83 | button{
84 | filter: contrast(0);
85 | }
86 | }
87 | .emoji-search{
88 | background-color: transparent;
89 | border-color: #9186f3;
90 | color: white;
91 | }
92 | .emoji-group:before {
93 | background-color: #080420;
94 | }
95 | }
96 | }
97 | }
98 |
99 | .input-container{
100 | width: 100%;
101 | border-radius: 2rem;
102 | display: flex;
103 | align-items: center;
104 | gap: 2rem;
105 | background-color: #ffffff34;
106 | input{
107 | width: 90%;
108 | height: 60%;
109 | background-color: transparent;
110 | color: white;
111 | border: none;
112 | padding-left: 1rem;
113 | font-size: 1.2rem;
114 | &::selection{
115 | background-color: #9186f3;
116 | }
117 | &:focus{
118 | outline: none;
119 | }
120 | }
121 | button{
122 | padding: 0.3rem 2rem;
123 | border-radius: 2rem;
124 | display: flex;
125 | justify-content: center;
126 | align-items: center;
127 | background-color: #9a86f3;
128 | border: none;
129 | cursor: pointer;
130 | @media screen and (min-width: 720px) and (max-width: 1080px){
131 | padding: 0.3rem 1rem;
132 | svg{
133 | font-size: 1rem;
134 | color: white;
135 | }
136 | }
137 | svg{
138 | font-size: 2rem;
139 | color: white;
140 | }
141 | }
142 | }
143 | `;
144 |
--------------------------------------------------------------------------------
/public/src/components/Contacts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import styled from "styled-components"
3 | import Logo from '../assets/logo.svg'
4 |
5 | export default function Contacts({ contacts, currentUser, changeChat }) {
6 | const [currentUserName, setCurrentUserName] = useState(undefined);
7 | const [currentUserImage, setCurrentUserImage] = useState(undefined);
8 | const [currentSelected, setCurrentSelected] = useState(undefined);
9 |
10 | useEffect(() => {
11 | if (currentUser) {
12 | setCurrentUserImage(currentUser.avatarImage);
13 | setCurrentUserName(currentUser.username);
14 | }
15 | }, [currentUser]);
16 |
17 | const changeCurrentChat = (index, contact) => {
18 | setCurrentSelected(index);
19 | changeChat(contact);
20 | };
21 | return (
22 | <>
23 | {
24 | currentUserImage && currentUserName && (
25 |
26 |
27 |

28 |
RChat
29 |
30 |
31 | {
32 | contacts.map((contact, index) => {
33 | return (
34 |
changeCurrentChat(index,contact)}>
40 |
41 |

42 |
43 |
44 |
{contact.username}
45 |
46 |
47 | )
48 | })
49 | }
50 |
51 |
52 |
53 |

54 |
55 |
56 |
{currentUserName}
57 |
58 |
59 |
60 | )
61 | }
62 | >
63 | )
64 | }
65 |
66 |
67 | const Container = styled.div`
68 | display: grid;
69 | grid-template-rows: 10% 75% 15%;
70 | overflow: hidden;
71 | background-color: #080420;
72 | .brand {
73 | display: flex;
74 | align-items: center;
75 | gap: 1rem;
76 | justify-content: center;
77 | img {
78 | height: 2rem;
79 | }
80 | h3 {
81 | color: white;
82 | text-transform: uppercase;
83 | }
84 | }
85 | .contacts {
86 | display: flex;
87 | flex-direction: column;
88 | align-items: center;
89 | overflow: auto;
90 | gap: 0.8rem;
91 | &::-webkit-scrollbar {
92 | width: 0.2rem;
93 | &-thumb {
94 | background-color: #ffffff39;
95 | width: 0.1rem;
96 | border-radius: 1rem;
97 | }
98 | }
99 | .contact {
100 | background-color: #ffffff34;
101 | min-height: 5rem;
102 | cursor: pointer;
103 | width: 90%;
104 | border-radius: 0.2rem;
105 | padding: 0.4rem;
106 | display: flex;
107 | gap: 1rem;
108 | align-items: center;
109 | transition: 0.5s ease-in-out;
110 | .avatar {
111 | img {
112 | height: 3rem;
113 | }
114 | }
115 | .username {
116 | h3 {
117 | color: white;
118 | text-transform: capitalize;
119 | }
120 | }
121 | }
122 | .selected {
123 | background-color: #9a86f3;
124 | }
125 | }
126 | .current-user {
127 | background-color: #0d0d30;
128 | display: flex;
129 | justify-content: center;
130 | align-items: center;
131 | gap: 2rem;
132 | .avatar {
133 | img {
134 | height: 4rem;
135 | max-inline-size: 100%;
136 | }
137 | }
138 | .username {
139 | h2 {
140 | color: white;
141 | text-transform: capitalize;
142 | }
143 | }
144 | @media screen and (min-width: 720px) and (max-width: 1080px) {
145 | gap: 0.5rem;
146 | .username {
147 | h2 {
148 | font-size: 1rem;
149 | }
150 | }
151 | }
152 | }
153 | `;
154 |
155 |
--------------------------------------------------------------------------------
/public/src/components/Logout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { BiPowerOff } from "react-icons/bi";
4 | import styled from "styled-components";
5 | import axios from "axios";
6 | export default function Logout() {
7 | const navigate = useNavigate();
8 | const handleClick = () => {
9 |
10 | localStorage.clear();
11 | navigate("/login");
12 | };
13 |
14 | return (
15 |
18 | );
19 | }
20 |
21 | const Button = styled.button`
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | padding: 0.5rem;
26 | border-radius: 0.5rem;
27 | background-color: #9a86f3;
28 | border: none;
29 | cursor: pointer;
30 | svg {
31 | font-size: 1.3rem;
32 | color: #ebe7ff;
33 | }
34 | `;
--------------------------------------------------------------------------------
/public/src/components/Messages.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export default function Messages() {
5 | return (
6 | Messages
7 | )
8 | }
9 |
10 | const Container = styled.div`
11 | height: 80%;
12 | `;
--------------------------------------------------------------------------------
/public/src/components/Welcome.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Robot from "../assets/robot.gif"
4 |
5 | export default function Welcome({currentUser}) {
6 | return (
7 |
8 |
9 |
10 | Welcome, {currentUser.username}!
11 |
12 | Please select a chat to start Messaging.
13 |
14 | )
15 | }
16 |
17 | const Container = styled.div`
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | flex-direction: column;
22 | color: white;
23 | img{
24 | height: 20rem;
25 | }
26 | span{
27 | color: #4e00ff;
28 | text-transform: capitalize;
29 | }
30 | `;
--------------------------------------------------------------------------------
/public/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@100;200;300;400;500;600&display=swap');
2 | * {
3 | margin: 0;
4 | padding: 0;
5 | box-sizing: border-box;
6 | }
7 |
8 | body,button,input{
9 | font-family: 'Josefin Sans', sans-serif;
10 | }
11 |
12 | body{
13 | max-height: 100vh;
14 | max-width: 100wv;
15 | overflow: hidden;
16 | }
--------------------------------------------------------------------------------
/public/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | const root = ReactDOM.createRoot(document.getElementById('root'));
7 | root.render(
8 |
9 |
10 |
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/public/src/pages/Chats.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { useState, useEffect , useRef} from 'react'
3 | import axios from "axios";
4 | import { useNavigate } from "react-router-dom";
5 | import { allUsersRoute, host } from "../utils/APIRoutes";
6 | import Contacts from "../components/Contacts";
7 | import Welcome from "../components/Welcome";
8 | import ChatContainer from "../components/ChatContainer";
9 | import { io } from "socket.io-client";
10 |
11 |
12 | export default function Chats() {
13 | const socket = useRef();
14 | const navigate = useNavigate();
15 | const [contacts, setContacts] = useState([]);
16 | const [currentUser, setCurrentUser] = useState(undefined);
17 | const [currentChat, setCurrentChat] = useState(undefined);
18 | const [isLoaded, setIsLoaded] = useState(false);
19 |
20 |
21 | useEffect( ()=>{
22 | const navigationTo = async () => {
23 | if (!localStorage.getItem('chat-app-user'))
24 | {
25 | navigate("/login");
26 | }
27 | else {
28 | setCurrentUser(await JSON.parse(localStorage.getItem('chat-app-user')));
29 | setIsLoaded(true);
30 | }
31 | }
32 | navigationTo();
33 | }, []);
34 |
35 | useEffect(()=>{
36 | if(currentUser){
37 | socket.current = io(host);
38 | socket.current.emit("add-user", currentUser._id);
39 | }
40 | },[currentUser]);
41 |
42 | useEffect( () => {
43 | const getCurrentUser = async()=>{
44 | if( currentUser) {
45 | if(currentUser.isAvatarImageSet){
46 | const data = await axios.get(`${allUsersRoute}/${currentUser._id}`);
47 | setContacts(data.data);
48 | } else{
49 | navigate('/setAvatar');
50 | }
51 | }
52 | }
53 | getCurrentUser();
54 | }, [currentUser]);
55 |
56 | const handleChatChange = (chat) =>{
57 | setCurrentChat(chat);
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 | { isLoaded &&
65 | currentChat === undefined ?
66 | :
67 |
68 | }
69 |
70 |
71 | )
72 | }
73 |
74 | const Container = styled.div`
75 | height: 100vh;
76 | width: 100vw;
77 | display: flex;
78 | flex-direction: column;
79 | justify-content: center;
80 | gap: 1rem;
81 | align-items: center;
82 | background-color: #131324;
83 | .container {
84 | height: 85vh;
85 | width: 85vw;
86 | background-color: #00000076;
87 | display: grid;
88 | grid-template-columns: 25% 75%;
89 | @media screen and (min-width: 720px) and (max-width: 1080px) {
90 | grid-template-columns: 35% 65%;
91 | }
92 | }
93 | `;
94 |
--------------------------------------------------------------------------------
/public/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import styled from 'styled-components'
3 | import { Link, useNavigate } from "react-router-dom";
4 | import Logo from "../assets/logo.svg";
5 | import { ToastContainer, toast } from "react-toastify"
6 | import "react-toastify/dist/ReactToastify.css";
7 | import axios from "axios"
8 | import { loginRoute } from '../utils/APIRoutes';
9 |
10 | const Login = () => {
11 | const navigate = useNavigate();
12 |
13 | const [values, setValues] = useState({
14 | username: "",
15 | password: ""
16 | });
17 |
18 | const toastOptions = {
19 | position: 'bottom-right',
20 | autoClose: 8000,
21 | pauseOnHover: true,
22 | draggable: true,
23 | theme: "dark"
24 | };
25 |
26 | useEffect(() => {
27 | if(localStorage.getItem('chat-app-user')) {
28 | navigate('/');
29 | }
30 | }, );
31 |
32 |
33 | const handleSubmit = async (e) => {
34 | e.preventDefault();
35 | if( handleValidation()){
36 | const {password, username} = values;
37 | const {data} = await axios.post(loginRoute, {
38 | username,
39 | password
40 | });
41 | if(data.status === false){
42 | toast.error(data.msg, toastOptions);
43 | } else {
44 | localStorage.setItem('chat-app-user', JSON.stringify(data.user));
45 | }
46 | navigate("/");
47 | };
48 | };
49 |
50 | const handleChange = (e) => {
51 | setValues({ ...values, [e.target.name]: e.target.value });
52 | };
53 |
54 | const handleValidation = () =>{
55 | const {password, username} = values;
56 | if(password === ""){
57 | toast.error("Password required!", toastOptions);
58 | return false;
59 | } else if(username.length === ""){
60 | toast.error("Username required", toastOptions);
61 | return false;
62 | }
63 | return true;
64 | }
65 |
66 | return (
67 | <>
68 |
69 |
80 |
81 |
82 | >
83 | );
84 | }
85 |
86 | const FormContainer = styled.div`
87 | height: 100vh;
88 | width: 100vw;
89 | display: flex;
90 | flex-direction: column;
91 | justify-content: center;
92 | gap: 1rem;
93 | align-items: center;
94 | background-color: #131324;
95 | .brand {
96 | display: flex;
97 | align-items: center;
98 | gap: 1rem;
99 | justify-content: center;
100 | img {
101 | height: 5rem;
102 | }
103 | h1 {
104 | color: white;
105 | text-transform: capitalize;
106 | }
107 | }
108 | form {
109 | display: flex;
110 | flex-direction: column;
111 | gap: 2rem;
112 | background-color: #00000076;
113 | border-radius: 2rem;
114 | padding: 3rem 5rem;
115 | }
116 | input {
117 | background-color: transparent;
118 | padding: 1rem;
119 | border: 0.1rem solid #4e0eff;
120 | border-radius: 0.4rem;
121 | color: white;
122 | width: 100%;
123 | font-size: 1rem;
124 | &:focus {
125 | border: 0.1rem solid #997af0;
126 | outline: none;
127 | }
128 | }
129 | button {
130 | background-color: #4e0eff;
131 | color: white;
132 | padding: 1rem 2rem;
133 | border: none;
134 | font-weight: bold;
135 | cursor: pointer;
136 | border-radius: 0.4rem;
137 | font-size: 1rem;
138 | text-transform: uppercase;
139 | &:hover {
140 | background-color: #3d12b3;
141 | }
142 | }
143 | span {
144 | color: white;
145 | text-transform: uppercase;
146 | a {
147 | color: #4e0eff;
148 | text-decoration: none;
149 | font-weight: bold;
150 | }
151 | }
152 | `;
153 | export default Login
--------------------------------------------------------------------------------
/public/src/pages/Register.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import styled from 'styled-components'
3 | import { Link, useNavigate } from "react-router-dom";
4 | import Logo from "../assets/logo.svg";
5 | import { ToastContainer, toast } from "react-toastify"
6 | import "react-toastify/dist/ReactToastify.css";
7 | import axios from "axios"
8 | import { registerRoute } from '../utils/APIRoutes';
9 |
10 | const Register = () => {
11 | const navigate = useNavigate();
12 |
13 | const [values, setValues] = useState({
14 | username: "",
15 | email: '',
16 | password: "",
17 | confirmPassword: '',
18 | });
19 |
20 | const toastOptions = {
21 | position: 'bottom-right',
22 | autoClose: 8000,
23 | pauseOnHover: true,
24 | draggable: true,
25 | theme: "dark"
26 | };
27 |
28 | const handleSubmit = async (e) => {
29 | e.preventDefault();
30 | if( handleValidation()){
31 | const {password, username, email} = values;
32 | const {data} = await axios.post(registerRoute, {
33 | username,
34 | email,
35 | password,
36 | });
37 | if(data.status === false){
38 | toast.error(data.msg, toastOptions);
39 | } else {
40 | localStorage.setItem('chat-app-user', JSON.stringify(data.user));
41 | }
42 | navigate("/");
43 | };
44 | };
45 |
46 | useEffect(() => {
47 | if(localStorage.getItem('chat-app-user')) {
48 | navigate('/');
49 | }
50 | }, );
51 |
52 | const handleChange = (e) => {
53 | setValues({ ...values, [e.target.name]: e.target.value });
54 | };
55 |
56 | const handleValidation = () =>{
57 | const {password, confirmPassword, username, email} = values;
58 | if(password!==confirmPassword){
59 | toast.error("Enter correct Confirm Password!", toastOptions);
60 | return false;
61 | } else if(username.length < 3){
62 | toast.error("Username must be atleast 3 characters.", toastOptions);
63 | return false;
64 | } else if(password.length < 4){
65 | toast.error("Password must be atleast 4 characters.", toastOptions);
66 | return false;
67 | } else if(email === "") {
68 | toast.error("Email is required", toastOptions);
69 | return false;
70 | }
71 | return true;
72 | }
73 |
74 |
75 | return (
76 | <>
77 |
78 |
91 |
92 |
93 | >
94 | );
95 | }
96 |
97 | const FormContainer = styled.div`
98 | height: 100vh;
99 | width: 100vw;
100 | display: flex;
101 | flex-direction: column;
102 | justify-content: center;
103 | gap: 1rem;
104 | align-items: center;
105 | background-color: #131324;
106 | .brand {
107 | display: flex;
108 | align-items: center;
109 | gap: 1rem;
110 | justify-content: center;
111 | img {
112 | height: 5rem;
113 | }
114 | h1 {
115 | color: white;
116 | text-transform: capitalize;
117 | }
118 | }
119 | form {
120 | display: flex;
121 | flex-direction: column;
122 | gap: 2rem;
123 | background-color: #00000076;
124 | border-radius: 2rem;
125 | padding: 3rem 5rem;
126 | }
127 | input {
128 | background-color: transparent;
129 | padding: 1rem;
130 | border: 0.1rem solid #4e0eff;
131 | border-radius: 0.4rem;
132 | color: white;
133 | width: 100%;
134 | font-size: 1rem;
135 | &:focus {
136 | border: 0.1rem solid #997af0;
137 | outline: none;
138 | }
139 | }
140 | button {
141 | background-color: #4e0eff;
142 | color: white;
143 | padding: 1rem 2rem;
144 | border: none;
145 | font-weight: bold;
146 | cursor: pointer;
147 | border-radius: 0.4rem;
148 | font-size: 1rem;
149 | text-transform: uppercase;
150 | &:hover {
151 | background-color: #3d12b3;
152 | }
153 | }
154 | span {
155 | color: white;
156 | text-transform: uppercase;
157 | a {
158 | color: #4e0eff;
159 | text-decoration: none;
160 | font-weight: bold;
161 | }
162 | }
163 | `;
164 | export default Register
--------------------------------------------------------------------------------
/public/src/pages/SetAvatar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components";
3 | import axios from "axios";
4 | import { Buffer } from "buffer";
5 | import loader from "../assets/loader.gif";
6 | import { ToastContainer, toast } from "react-toastify";
7 | import "react-toastify/dist/ReactToastify.css";
8 | import { useNavigate } from "react-router-dom";
9 | import { setAvatarRoute } from "../utils/APIRoutes";
10 | export default function SetAvatar() {
11 | const api = `https://api.multiavatar.com/45678943`;
12 | const navigate = useNavigate();
13 | const [avatars, setAvatars] = useState([]);
14 | const [isLoading, setIsLoading] = useState(true);
15 | const [selectedAvatar, setSelectedAvatar] = useState(undefined);
16 | const toastOptions = {
17 | position: "bottom-right",
18 | autoClose: 8000,
19 | pauseOnHover: true,
20 | draggable: true,
21 | theme: "dark",
22 | };
23 |
24 | useEffect( () => {
25 | const navigationTo = async () =>{
26 | if (!localStorage.getItem('chat-app-user'))
27 | navigate("/login");
28 | }
29 | navigationTo();
30 | },[]);
31 |
32 | const setProfilePicture = async () => {
33 | if (selectedAvatar === undefined) {
34 | toast.error("Please select an avatar", toastOptions);
35 | } else {
36 | const user = await JSON.parse(
37 | localStorage.getItem('chat-app-user')
38 | );
39 |
40 | const { data } = await axios.post(`${setAvatarRoute}/${user._id}`, {
41 | image: avatars[selectedAvatar],
42 | });
43 |
44 | if (data.isSet) {
45 | user.isAvatarImageSet = true;
46 | user.avatarImage = data.image;
47 | localStorage.setItem(
48 | 'chat-app-user',
49 | JSON.stringify(user)
50 | );
51 | navigate("/");
52 | } else {
53 | toast.error("Error setting avatar. Please try again.", toastOptions);
54 | }
55 | }
56 | };
57 |
58 | useEffect(() => {
59 | const fetchData = async () => {
60 | const data = [];
61 | // foreach doesn't work with APIs
62 | for (let i = 0; i < 4; i++) {
63 | const image = await axios.get(
64 | `${api}/${Math.round(Math.random() * 1000)}`
65 | );
66 | const buffer = new Buffer(image.data);
67 | data.push(buffer.toString("base64"));
68 | }
69 |
70 | setAvatars(data);
71 | setIsLoading(false);
72 | };
73 |
74 | fetchData();
75 | },[]);
76 |
77 | return (
78 | <>
79 | {isLoading ? (
80 |
81 |
82 |
83 | ) : (
84 |
85 |
86 |
Pick an Avatar as your profile picture
87 |
88 |
89 | {avatars.map((avatar, index) => {
90 | return (
91 |
96 |

setSelectedAvatar(index)}
101 | />
102 |
103 | );
104 | })}
105 |
106 |
109 |
110 |
111 | )}
112 | >
113 | );
114 | }
115 |
116 | const Container = styled.div`
117 | display: flex;
118 | justify-content: center;
119 | align-items: center;
120 | flex-direction: column;
121 | gap: 3rem;
122 | background-color: #131324;
123 | height: 100vh;
124 | width: 100vw;
125 |
126 | .loader {
127 | max-inline-size: 100%;
128 | }
129 |
130 | .title-container {
131 | h1 {
132 | color: white;
133 | }
134 | }
135 | .avatars {
136 | display: flex;
137 | gap: 2rem;
138 |
139 | .avatar {
140 | border: 0.4rem solid transparent;
141 | padding: 0.4rem;
142 | border-radius: 5rem;
143 | display: flex;
144 | justify-content: center;
145 | align-items: center;
146 | transition: 0.5s ease-in-out;
147 | img {
148 | height: 6rem;
149 | transition: 0.5s ease-in-out;
150 | }
151 | }
152 | .selected {
153 | border: 0.4rem solid #4e0eff;
154 | }
155 | }
156 | .submit-btn {
157 | background-color: #4e0eff;
158 | color: white;
159 | padding: 1rem 2rem;
160 | border: none;
161 | font-weight: bold;
162 | cursor: pointer;
163 | border-radius: 0.4rem;
164 | font-size: 1rem;
165 | text-transform: uppercase;
166 | &:hover {
167 | background-color: #4e0eff;
168 | }
169 | }
170 | `;
171 |
--------------------------------------------------------------------------------
/public/src/utils/APIRoutes.js:
--------------------------------------------------------------------------------
1 | export const host = "http://localhost:8800";
2 | export const registerRoute = `${host}/api/auth/register`;
3 | export const loginRoute = `${host}/api/auth/login`;
4 | export const setAvatarRoute = `${host}/api/auth/setAvatar`;
5 | export const allUsersRoute = `${host}/api/auth/allUsers`;
6 | export const logoutRoute = `${host}/api/auth/logout`;
7 | export const sendMessageRoute = `${host}/api/message/addmsg`;
8 | export const getAllMessagesRoute = `${host}/api/message/getmsg`;
9 |
--------------------------------------------------------------------------------
/server/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aayushjha5/reactchatapp/ff143bf1e821ca224265e8c38ed751f118ecc20b/server/.DS_Store
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | PORT = 8800
2 | MONGO_URL = "mongodb+srv://aayush:aayush@cluster0.cdgomuh.mongodb.net/chatapp?retryWrites=true&w=majority"
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /.env
3 |
--------------------------------------------------------------------------------
/server/controllers/messagesController.js:
--------------------------------------------------------------------------------
1 | const messageModel = require("../models/messageModel");
2 |
3 | module.exports.addMessage = async (req, res, next) => {
4 | try {
5 | const {from,to,message} = req.body;
6 | const data = await messageModel.create({
7 | message:{
8 | text: message
9 | },
10 | users: [
11 | from,
12 | to
13 | ],
14 | sender:from,
15 | });
16 |
17 | if(data) return res.json({
18 | msg: "Message added successfully!"
19 | });
20 | return res.json({
21 | msg: "Failed to add message to DB"
22 | });
23 |
24 | } catch (err) {
25 | next(err);
26 | }
27 | };
28 | module.exports.getAllMessage = async (req, res, next) => {
29 | try {
30 | const {from,to} = req.body;
31 | const messages = await messageModel.find({
32 | users:{
33 | $all: [from,to],
34 | },
35 | }).sort({ updatedAt: 1 });
36 |
37 | const projectMessages = messages.map((msg)=>{
38 | return{
39 | fromSelf: msg.sender.toString() === from,
40 | message: msg.message.text,
41 | };
42 | });
43 |
44 | res.json(projectMessages);
45 | } catch (error) {
46 | next(error);
47 | }
48 | };
49 |
50 |
--------------------------------------------------------------------------------
/server/controllers/usersController.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/userModel");
2 | const bcrypt = require("bcrypt");
3 |
4 | module.exports.login = async (req, res, next) => {
5 | try {
6 | const { username, password } = req.body;
7 | const user = await User.findOne({ username });
8 | if (!user)
9 | return res.json({ msg: "Incorrect Username ", status: false });
10 | const isPasswordValid = await bcrypt.compare(password, user.password);
11 | if (!isPasswordValid)
12 | return res.json({ msg: "Incorrect Password", status: false });
13 | delete user.password;
14 | return res.json({ status: true, user });
15 | } catch (ex) {
16 | next(ex);
17 | }
18 | };
19 |
20 | module.exports.register = async (req, res, next) => {
21 | try {
22 | const { username, email, password } = req.body;
23 | const usernameCheck = await User.findOne({ username });
24 | if (usernameCheck)
25 | return res.json({ msg: "Username already used", status: false });
26 | const emailCheck = await User.findOne({ email });
27 | if (emailCheck)
28 | return res.json({ msg: "Email already used", status: false });
29 | const hashedPassword = await bcrypt.hash(password, 10);
30 | const user = await User.create({
31 | email,
32 | username,
33 | password: hashedPassword,
34 | });
35 | delete user.password;
36 | return res.json({ status: true, user });
37 | } catch (ex) {
38 | next(ex);
39 | }
40 | };
41 |
42 | module.exports.setAvatar = async (req, res, next) => {
43 | try {
44 | const userId = req.params.id;
45 | const avatarImage = req.body.image;
46 | const userData = await User.findByIdAndUpdate(
47 | userId,
48 | {
49 | isAvatarImageSet: true,
50 | avatarImage,
51 | },
52 | { new: true }
53 | );
54 | return res.json({
55 | isSet: userData.isAvatarImageSet,
56 | image: userData.avatarImage,
57 | });
58 | } catch (ex) {
59 | next(ex);
60 | }
61 | };
62 |
63 | module.exports.getAllUsers = async (req, res, next) => {
64 | try {
65 | const users = await User.find({
66 | _id:{ $ne:req.params.id }
67 | }).select([
68 | "email",
69 | "username",
70 | "avatarImage",
71 | "_id"
72 | ]);
73 | return res.json(users);
74 | } catch (err) {
75 | next(err);
76 | }
77 | };
78 |
79 | module.exports.logOut = (req, res, next) => {
80 | try {
81 | if (!req.params.id) return res.json({ msg: "User id is required " });
82 | onlineUsers.delete(req.params.id);
83 | return res.status(200).send();
84 | } catch (ex) {
85 | next(ex);
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 | const cors = require("cors");
4 | const mongoose = require("mongoose");
5 | const dotenv = require("dotenv");
6 | const userRoutes = require("./routes/userRoutes");
7 | const messageRoute = require("./routes/messagesRoute");
8 | const socket = require("socket.io");
9 |
10 | dotenv.config();
11 | app.use(cors());
12 | app.use(express.json());
13 |
14 | app.use("/api/auth", userRoutes);
15 | app.use("/api/message", messageRoute);
16 |
17 | //mongoose connection
18 | mongoose.connect(process.env.MONGO_URL, {
19 | useNewUrlParser: true,
20 | useUnifiedTopology: true
21 | }).then(() => {
22 | console.log("DB Connection Successful!")
23 | }).catch((err) => console.log(err));
24 |
25 | const server = app.listen(process.env.PORT, ()=>{
26 | console.log(`Server started on Port ${process.env.PORT}`);
27 | });
28 |
29 | const io = socket(server,{
30 | cors: {
31 | origin: "http://localhost:3000",
32 | credentials: true,
33 | },
34 | });
35 | //store all online users inside this map
36 | global.onlineUsers = new Map();
37 |
38 | io.on("connection",(socket)=>{
39 | global.chatSocket = socket;
40 | socket.on("add-user",(userId)=>{
41 | onlineUsers.set(userId,socket.id);
42 | });
43 |
44 | socket.on("send-msg",(data)=>{
45 | const sendUserSocket = onlineUsers.get(data.to);
46 | if(sendUserSocket) {
47 | socket.to(sendUserSocket).emit("msg-recieved",data.message);
48 | }
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/server/models/messageModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const messageSchema = mongoose.Schema(
4 | {
5 | message: {
6 | text: { type: String, required: true },
7 | },
8 | users: Array,
9 | sender: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: "User",
12 | required: true,
13 | },
14 | },
15 | {
16 | timestamps: true,
17 | }
18 | );
19 |
20 | module.exports = mongoose.model("Messages", messageSchema);
--------------------------------------------------------------------------------
/server/models/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const userSchema = new mongoose.Schema({
4 | username:{
5 | type: String,
6 | required: true,
7 | min: 3,
8 | max: 20,
9 | unique: true,
10 | },
11 | email:{
12 | type: String,
13 | required: true,
14 | unique: true,
15 | max: 20,
16 | },
17 | password:{
18 | type: String,
19 | required: true,
20 | min: 4,
21 | },
22 | isAvatarImageSet: {
23 | type: Boolean,
24 | default: false,
25 | },
26 | avatarImage: {
27 | type: String,
28 | default: ""
29 | }
30 | });
31 |
32 | module.exports = mongoose.model("Users", userSchema);
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcrypt": "^5.0.1",
14 | "cors": "^2.8.5",
15 | "dotenv": "^16.0.1",
16 | "express": "^4.18.1",
17 | "mongoose": "^6.5.2",
18 | "nodemon": "^2.0.19",
19 | "socket.io": "^4.5.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/routes/messagesRoute.js:
--------------------------------------------------------------------------------
1 | const { addMessage, getAllMessage } = require("../controllers/messagesController");
2 |
3 |
4 | const router = require("express").Router();
5 |
6 | router.post("/addmsg/", addMessage);
7 | router.post("/getmsg/", getAllMessage);
8 |
9 |
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/server/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | const { register } = require("../controllers/usersController");
2 | const { login } = require("../controllers/usersController");
3 | const { setAvatar } = require("../controllers/usersController");
4 | const { getAllUsers } = require("../controllers/usersController");
5 | const { logOut } = require("../controllers/usersController");
6 |
7 | const router = require("express").Router();
8 |
9 | router.post("/register", register);
10 | router.post("/login", login);
11 | router.post("/setAvatar/:id", setAvatar);
12 | router.get("/allUsers/:id", getAllUsers);
13 | router.get("/logout/:id", logOut);
14 |
15 | module.exports = router;
16 |
--------------------------------------------------------------------------------