├── frontend ├── src │ ├── App.css │ ├── assets │ │ ├── sounds │ │ │ └── notification.mp3 │ │ └── react.svg │ ├── zustand │ │ └── useConversation.js │ ├── utils │ │ ├── extractTime.js │ │ └── emojis.js │ ├── pages │ │ ├── home │ │ │ └── Home.jsx │ │ ├── signup │ │ │ ├── GenderCheckbox.jsx │ │ │ └── SignUp.jsx │ │ └── login │ │ │ └── Login.jsx │ ├── components │ │ ├── sidebar │ │ │ ├── LogoutButton.jsx │ │ │ ├── Sidebar.jsx │ │ │ ├── Conversations.jsx │ │ │ ├── SearchInput.jsx │ │ │ └── Conversation.jsx │ │ ├── skeletons │ │ │ └── MessageSkeleton.jsx │ │ └── messages │ │ │ ├── Message.jsx │ │ │ ├── Messages.jsx │ │ │ ├── MessageInput.jsx │ │ │ └── MessageContainer.jsx │ ├── context │ │ ├── AuthContext.jsx │ │ └── SocketContext.jsx │ ├── main.jsx │ ├── hooks │ │ ├── useListenMessages.js │ │ ├── useGetConversations.js │ │ ├── useLogout.js │ │ ├── useGetMessages.js │ │ ├── useSendMessage.js │ │ ├── useLogin.js │ │ └── useSignup.js │ ├── App.jsx │ └── index.css ├── public │ ├── bg.png │ └── vite.svg ├── postcss.config.js ├── tailwind.config.js ├── vite.config.js ├── index.html ├── README.md ├── .eslintrc.cjs └── package.json ├── backend ├── routes │ ├── user.routes.js │ ├── auth.routes.js │ └── message.routes.js ├── db │ └── connectToMongoDB.js ├── controllers │ ├── user.controller.js │ ├── message.controller.js │ └── auth.controller.js ├── models │ ├── conversation.model.js │ ├── message.model.js │ └── user.model.js ├── utils │ └── generateToken.js ├── middleware │ └── protectRoute.js ├── socket │ └── socket.js └── server.js ├── .gitignore ├── package.json ├── README.md └── LICENSE /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shyboy1044/mern-chart-app/HEAD/frontend/public/bg.png -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/assets/sounds/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shyboy1044/mern-chart-app/HEAD/frontend/src/assets/sounds/notification.mp3 -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | // eslint-disable-next-line no-undef 8 | plugins: [require("daisyui")], 9 | }; 10 | -------------------------------------------------------------------------------- /backend/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import protectRoute from "../middleware/protectRoute.js"; 3 | import { getUsersForSidebar } from "../controllers/user.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", protectRoute, getUsersForSidebar); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /backend/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { login, logout, signup } from "../controllers/auth.controller.js"; 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/signup", signup); 7 | 8 | router.post("/login", login); 9 | 10 | router.post("/logout", logout); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 3000, 9 | proxy: { 10 | "/api": { 11 | target: "http://localhost:5000", 12 | }, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /backend/db/connectToMongoDB.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectToMongoDB = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGO_DB_URI); 6 | console.log("Connected to MongoDB"); 7 | } catch (error) { 8 | console.log("Error connecting to MongoDB", error.message); 9 | } 10 | }; 11 | 12 | export default connectToMongoDB; 13 | -------------------------------------------------------------------------------- /frontend/src/zustand/useConversation.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | const useConversation = create((set) => ({ 4 | selectedConversation: null, 5 | setSelectedConversation: (selectedConversation) => set({ selectedConversation }), 6 | messages: [], 7 | setMessages: (messages) => set({ messages }), 8 | })); 9 | 10 | export default useConversation; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /backend/routes/message.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getMessages, sendMessage } from "../controllers/message.controller.js"; 3 | import protectRoute from "../middleware/protectRoute.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/:id", protectRoute, getMessages); 8 | router.post("/send/:id", protectRoute, sendMessage); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /frontend/src/utils/extractTime.js: -------------------------------------------------------------------------------- 1 | export function extractTime(dateString) { 2 | const date = new Date(dateString); 3 | const hours = padZero(date.getHours()); 4 | const minutes = padZero(date.getMinutes()); 5 | return `${hours}:${minutes}`; 6 | } 7 | 8 | // Helper function to pad single-digit numbers with a leading zero 9 | function padZero(number) { 10 | return number.toString().padStart(2, "0"); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/pages/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import MessageContainer from "../../components/messages/MessageContainer"; 2 | import Sidebar from "../../components/sidebar/Sidebar"; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | export default Home; 13 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /backend/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import User from "../models/user.model.js"; 2 | 3 | export const getUsersForSidebar = async (req, res) => { 4 | try { 5 | const loggedInUserId = req.user._id; 6 | 7 | const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } }).select("-password"); 8 | 9 | res.status(200).json(filteredUsers); 10 | } catch (error) { 11 | console.error("Error in getUsersForSidebar: ", error.message); 12 | res.status(500).json({ error: "Internal server error" }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/LogoutButton.jsx: -------------------------------------------------------------------------------- 1 | import { BiLogOut } from "react-icons/bi"; 2 | import useLogout from "../../hooks/useLogout"; 3 | 4 | const LogoutButton = () => { 5 | const { loading, logout } = useLogout(); 6 | 7 | return ( 8 |
9 | {!loading ? ( 10 | 11 | ) : ( 12 | 13 | )} 14 |
15 | ); 16 | }; 17 | export default LogoutButton; 18 | -------------------------------------------------------------------------------- /backend/models/conversation.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const conversationSchema = new mongoose.Schema( 4 | { 5 | participants: [ 6 | { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "User", 9 | }, 10 | ], 11 | messages: [ 12 | { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: "Message", 15 | default: [], 16 | }, 17 | ], 18 | }, 19 | { timestamps: true } 20 | ); 21 | 22 | const Conversation = mongoose.model("Conversation", conversationSchema); 23 | 24 | export default Conversation; 25 | -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const generateTokenAndSetCookie = (userId, res) => { 4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, { 5 | expiresIn: "15d", 6 | }); 7 | 8 | res.cookie("jwt", token, { 9 | maxAge: 15 * 24 * 60 * 60 * 1000, // MS 10 | httpOnly: true, // prevent XSS attacks cross-site scripting attacks 11 | sameSite: "strict", // CSRF attacks cross-site request forgery attacks 12 | secure: process.env.NODE_ENV !== "development", 13 | }); 14 | }; 15 | 16 | export default generateTokenAndSetCookie; 17 | -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from "react"; 2 | 3 | export const AuthContext = createContext(); 4 | 5 | // eslint-disable-next-line react-refresh/only-export-components 6 | export const useAuthContext = () => { 7 | return useContext(AuthContext); 8 | }; 9 | 10 | export const AuthContextProvider = ({ children }) => { 11 | const [authUser, setAuthUser] = useState(JSON.parse(localStorage.getItem("chat-user")) || null); 12 | 13 | return {children}; 14 | }; 15 | -------------------------------------------------------------------------------- /backend/models/message.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const messageSchema = new mongoose.Schema( 4 | { 5 | senderId: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "User", 8 | required: true, 9 | }, 10 | receiverId: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "User", 13 | required: true, 14 | }, 15 | message: { 16 | type: String, 17 | required: true, 18 | }, 19 | // createdAt, updatedAt 20 | }, 21 | { timestamps: true } 22 | ); 23 | 24 | const Message = mongoose.model("Message", messageSchema); 25 | 26 | export default Message; 27 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react/jsx-runtime", 8 | "plugin:react-hooks/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 12 | settings: { react: { version: "18.2" } }, 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 16 | "react/prop-types": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { AuthContextProvider } from "./context/AuthContext.jsx"; 7 | import { SocketContextProvider } from "./context/SocketContext.jsx"; 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app-yt", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "nodemon backend/server.js", 8 | "start": "node backend/server.js", 9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend" 10 | }, 11 | "type": "module", 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cookie-parser": "^1.4.6", 18 | "dotenv": "^16.4.1", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^8.1.1", 22 | "socket.io": "^4.7.4" 23 | }, 24 | "devDependencies": { 25 | "nodemon": "^3.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/skeletons/MessageSkeleton.jsx: -------------------------------------------------------------------------------- 1 | const MessageSkeleton = () => { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | ); 19 | }; 20 | export default MessageSkeleton; 21 | -------------------------------------------------------------------------------- /backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | fullName: { 6 | type: String, 7 | required: true, 8 | }, 9 | username: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | minlength: 6, 18 | }, 19 | gender: { 20 | type: String, 21 | required: true, 22 | enum: ["male", "female"], 23 | }, 24 | profilePic: { 25 | type: String, 26 | default: "", 27 | }, 28 | // createdAt, updatedAt => Member since 29 | }, 30 | { timestamps: true } 31 | ); 32 | 33 | const User = mongoose.model("User", userSchema); 34 | 35 | export default User; 36 | -------------------------------------------------------------------------------- /frontend/src/hooks/useListenMessages.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import { useSocketContext } from "../context/SocketContext"; 4 | import useConversation from "../zustand/useConversation"; 5 | 6 | import notificationSound from "../assets/sounds/notification.mp3"; 7 | 8 | const useListenMessages = () => { 9 | const { socket } = useSocketContext(); 10 | const { messages, setMessages } = useConversation(); 11 | 12 | useEffect(() => { 13 | socket?.on("newMessage", (newMessage) => { 14 | newMessage.shouldShake = true; 15 | const sound = new Audio(notificationSound); 16 | sound.play(); 17 | setMessages([...messages, newMessage]); 18 | }); 19 | 20 | return () => socket?.off("newMessage"); 21 | }, [socket, setMessages, messages]); 22 | }; 23 | export default useListenMessages; 24 | -------------------------------------------------------------------------------- /frontend/src/utils/emojis.js: -------------------------------------------------------------------------------- 1 | export const funEmojis = [ 2 | "👾", 3 | "⭐", 4 | "🌟", 5 | "🎉", 6 | "🎊", 7 | "🎈", 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 | "🏑", 34 | "🏏", 35 | "⛳", 36 | "🏹", 37 | "🎣", 38 | "🥊", 39 | "🥋", 40 | "🎽", 41 | "⛸", 42 | "🥌", 43 | "🛷", 44 | "🎿", 45 | "⛷", 46 | "🏂", 47 | "🏋️", 48 | "🤼", 49 | "🤸", 50 | "🤺", 51 | "⛹️", 52 | "🤾", 53 | "🏌️", 54 | "🏇", 55 | "🧘", 56 | ]; 57 | 58 | export const getRandomEmoji = () => { 59 | return funEmojis[Math.floor(Math.random() * funEmojis.length)]; 60 | }; 61 | -------------------------------------------------------------------------------- /frontend/src/hooks/useGetConversations.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import toast from "react-hot-toast"; 3 | 4 | const useGetConversations = () => { 5 | const [loading, setLoading] = useState(false); 6 | const [conversations, setConversations] = useState([]); 7 | 8 | useEffect(() => { 9 | const getConversations = async () => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch("/api/users"); 13 | const data = await res.json(); 14 | if (data.error) { 15 | throw new Error(data.error); 16 | } 17 | setConversations(data); 18 | } catch (error) { 19 | toast.error(error.message); 20 | } finally { 21 | setLoading(false); 22 | } 23 | }; 24 | 25 | getConversations(); 26 | }, []); 27 | 28 | return { loading, conversations }; 29 | }; 30 | export default useGetConversations; 31 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom"; 2 | import "./App.css"; 3 | import Home from "./pages/home/Home"; 4 | import Login from "./pages/login/Login"; 5 | import SignUp from "./pages/signup/SignUp"; 6 | import { Toaster } from "react-hot-toast"; 7 | import { useAuthContext } from "./context/AuthContext"; 8 | 9 | function App() { 10 | const { authUser } = useAuthContext(); 11 | return ( 12 |
13 | 14 | : } /> 15 | : } /> 16 | : } /> 17 | 18 | 19 |
20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLogout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAuthContext } from "../context/AuthContext"; 3 | import toast from "react-hot-toast"; 4 | 5 | const useLogout = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const logout = async () => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch("/api/auth/logout", { 13 | method: "POST", 14 | headers: { "Content-Type": "application/json" }, 15 | }); 16 | const data = await res.json(); 17 | if (data.error) { 18 | throw new Error(data.error); 19 | } 20 | 21 | localStorage.removeItem("chat-user"); 22 | setAuthUser(null); 23 | } catch (error) { 24 | toast.error(error.message); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | return { loading, logout }; 31 | }; 32 | export default useLogout; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN Stack Project: Build and Deploy a Real Time Chat App | JWT, Socket.io 2 | 3 | ![Demo App](https://i.ibb.co/fXmZdnz/Screenshot-10.png) 4 | 5 | [Video Tutorial on Youtube](https://youtu.be/HwCqsOis894) 6 | 7 | Some Features: 8 | 9 | - 🌟 Tech stack: MERN + Socket.io + TailwindCSS + Daisy UI 10 | - 🎃 Authentication && Authorization with JWT 11 | - 👾 Real-time messaging with Socket.io 12 | - 🚀 Online user status (Socket.io and React Context) 13 | - 👌 Global state management with Zustand 14 | - 🐞 Error handling both on the server and on the client 15 | - ⭐ At the end Deployment like a pro for FREE! 16 | - ⏳ And much more! 17 | 18 | ### Setup .env file 19 | 20 | ```js 21 | PORT=... 22 | MONGO_DB_URI=... 23 | JWT_SECRET=... 24 | NODE_ENV=... 25 | ``` 26 | 27 | ### Build the app 28 | 29 | ```shell 30 | npm run build 31 | ``` 32 | 33 | ### Start the app 34 | 35 | ```shell 36 | npm start 37 | ``` 38 | -------------------------------------------------------------------------------- /backend/middleware/protectRoute.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from "../models/user.model.js"; 3 | 4 | const protectRoute = async (req, res, next) => { 5 | try { 6 | const token = req.cookies.jwt; 7 | 8 | if (!token) { 9 | return res.status(401).json({ error: "Unauthorized - No Token Provided" }); 10 | } 11 | 12 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 13 | 14 | if (!decoded) { 15 | return res.status(401).json({ error: "Unauthorized - Invalid Token" }); 16 | } 17 | 18 | const user = await User.findById(decoded.userId).select("-password"); 19 | 20 | if (!user) { 21 | return res.status(404).json({ error: "User not found" }); 22 | } 23 | 24 | req.user = user; 25 | 26 | next(); 27 | } catch (error) { 28 | console.log("Error in protectRoute middleware: ", error.message); 29 | res.status(500).json({ error: "Internal server error" }); 30 | } 31 | }; 32 | 33 | export default protectRoute; 34 | -------------------------------------------------------------------------------- /frontend/src/hooks/useGetMessages.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import useConversation from "../zustand/useConversation"; 3 | import toast from "react-hot-toast"; 4 | 5 | const useGetMessages = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { messages, setMessages, selectedConversation } = useConversation(); 8 | 9 | useEffect(() => { 10 | const getMessages = async () => { 11 | setLoading(true); 12 | try { 13 | const res = await fetch(`/api/messages/${selectedConversation._id}`); 14 | const data = await res.json(); 15 | if (data.error) throw new Error(data.error); 16 | setMessages(data); 17 | } catch (error) { 18 | toast.error(error.message); 19 | } finally { 20 | setLoading(false); 21 | } 22 | }; 23 | 24 | if (selectedConversation?._id) getMessages(); 25 | }, [selectedConversation?._id, setMessages]); 26 | 27 | return { messages, loading }; 28 | }; 29 | export default useGetMessages; 30 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import Conversations from "./Conversations"; 2 | import LogoutButton from "./LogoutButton"; 3 | import SearchInput from "./SearchInput"; 4 | 5 | const Sidebar = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | export default Sidebar; 16 | 17 | // STARTER CODE FOR THIS FILE 18 | // import Conversations from "./Conversations"; 19 | // import LogoutButton from "./LogoutButton"; 20 | // import SearchInput from "./SearchInput"; 21 | 22 | // const Sidebar = () => { 23 | // return ( 24 | //
25 | // 26 | //
27 | // 28 | // 29 | //
30 | // ); 31 | // }; 32 | // export default Sidebar; 33 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSendMessage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import useConversation from "../zustand/useConversation"; 3 | import toast from "react-hot-toast"; 4 | 5 | const useSendMessage = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { messages, setMessages, selectedConversation } = useConversation(); 8 | 9 | const sendMessage = async (message) => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch(`/api/messages/send/${selectedConversation._id}`, { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json", 16 | }, 17 | body: JSON.stringify({ message }), 18 | }); 19 | const data = await res.json(); 20 | if (data.error) throw new Error(data.error); 21 | 22 | setMessages([...messages, data]); 23 | } catch (error) { 24 | toast.error(error.message); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | return { sendMessage, loading }; 31 | }; 32 | export default useSendMessage; 33 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "chat-app-yt": "file:..", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-hot-toast": "^2.4.1", 17 | "react-icons": "^5.0.1", 18 | "react-router-dom": "^6.21.3", 19 | "socket.io-client": "^4.7.4", 20 | "zustand": "^4.5.0" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.43", 24 | "@types/react-dom": "^18.2.17", 25 | "@vitejs/plugin-react": "^4.2.1", 26 | "autoprefixer": "^10.4.17", 27 | "daisyui": "^4.6.1", 28 | "eslint": "^8.55.0", 29 | "eslint-plugin-react": "^7.33.2", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.4.5", 32 | "postcss": "^8.4.33", 33 | "tailwindcss": "^3.4.1", 34 | "vite": "^5.0.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Burak 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. 22 | -------------------------------------------------------------------------------- /backend/socket/socket.js: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import http from "http"; 3 | import express from "express"; 4 | 5 | const app = express(); 6 | 7 | const server = http.createServer(app); 8 | const io = new Server(server, { 9 | cors: { 10 | origin: ["http://localhost:3000"], 11 | methods: ["GET", "POST"], 12 | }, 13 | }); 14 | 15 | export const getReceiverSocketId = (receiverId) => { 16 | return userSocketMap[receiverId]; 17 | }; 18 | 19 | const userSocketMap = {}; // {userId: socketId} 20 | 21 | io.on("connection", (socket) => { 22 | console.log("a user connected", socket.id); 23 | 24 | const userId = socket.handshake.query.userId; 25 | if (userId != "undefined") userSocketMap[userId] = socket.id; 26 | 27 | // io.emit() is used to send events to all the connected clients 28 | io.emit("getOnlineUsers", Object.keys(userSocketMap)); 29 | 30 | // socket.on() is used to listen to the events. can be used both on client and server side 31 | socket.on("disconnect", () => { 32 | console.log("user disconnected", socket.id); 33 | delete userSocketMap[userId]; 34 | io.emit("getOnlineUsers", Object.keys(userSocketMap)); 35 | }); 36 | }); 37 | 38 | export { app, io, server }; 39 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Conversations.jsx: -------------------------------------------------------------------------------- 1 | import useGetConversations from "../../hooks/useGetConversations"; 2 | import { getRandomEmoji } from "../../utils/emojis"; 3 | import Conversation from "./Conversation"; 4 | 5 | const Conversations = () => { 6 | const { loading, conversations } = useGetConversations(); 7 | return ( 8 |
9 | {conversations.map((conversation, idx) => ( 10 | 16 | ))} 17 | 18 | {loading ? : null} 19 |
20 | ); 21 | }; 22 | export default Conversations; 23 | 24 | // STARTER CODE SNIPPET 25 | // import Conversation from "./Conversation"; 26 | 27 | // const Conversations = () => { 28 | // return ( 29 | //
30 | // 31 | // 32 | // 33 | // 34 | // 35 | // 36 | //
37 | // ); 38 | // }; 39 | // export default Conversations; 40 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url("/bg.png"); 7 | background-repeat: no-repeat; 8 | background-size: cover; 9 | background-position: center; 10 | } 11 | 12 | /* dark mode looking scrollbar */ 13 | ::-webkit-scrollbar { 14 | width: 8px; 15 | } 16 | 17 | ::-webkit-scrollbar-track { 18 | background: #555; 19 | } 20 | 21 | ::-webkit-scrollbar-thumb { 22 | background: #121212; 23 | border-radius: 5px; 24 | } 25 | 26 | ::-webkit-scrollbar-thumb:hover { 27 | background: #242424; 28 | } 29 | 30 | /* SHAKE ANIMATION ON HORIZONTAL DIRECTION */ 31 | .shake { 32 | animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.2s both; 33 | transform: translate3d(0, 0, 0); 34 | backface-visibility: hidden; 35 | perspective: 1000px; 36 | } 37 | 38 | @keyframes shake { 39 | 10%, 40 | 90% { 41 | transform: translate3d(-1px, 0, 0); 42 | } 43 | 44 | 20%, 45 | 80% { 46 | transform: translate3d(2px, 0, 0); 47 | } 48 | 49 | 30%, 50 | 50%, 51 | 70% { 52 | transform: translate3d(-4px, 0, 0); 53 | } 54 | 55 | 40%, 56 | 60% { 57 | transform: translate3d(4px, 0, 0); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/context/SocketContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect, useContext } from "react"; 2 | import { useAuthContext } from "./AuthContext"; 3 | import io from "socket.io-client"; 4 | 5 | const SocketContext = createContext(); 6 | 7 | export const useSocketContext = () => { 8 | return useContext(SocketContext); 9 | }; 10 | 11 | export const SocketContextProvider = ({ children }) => { 12 | const [socket, setSocket] = useState(null); 13 | const [onlineUsers, setOnlineUsers] = useState([]); 14 | const { authUser } = useAuthContext(); 15 | 16 | useEffect(() => { 17 | if (authUser) { 18 | const socket = io("https://chat-app-yt.onrender.com", { 19 | query: { 20 | userId: authUser._id, 21 | }, 22 | }); 23 | 24 | setSocket(socket); 25 | 26 | // socket.on() is used to listen to the events. can be used both on client and server side 27 | socket.on("getOnlineUsers", (users) => { 28 | setOnlineUsers(users); 29 | }); 30 | 31 | return () => socket.close(); 32 | } else { 33 | if (socket) { 34 | socket.close(); 35 | setSocket(null); 36 | } 37 | } 38 | }, [authUser]); 39 | 40 | return {children}; 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/src/components/messages/Message.jsx: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from "../../context/AuthContext"; 2 | import { extractTime } from "../../utils/extractTime"; 3 | import useConversation from "../../zustand/useConversation"; 4 | 5 | const Message = ({ message }) => { 6 | const { authUser } = useAuthContext(); 7 | const { selectedConversation } = useConversation(); 8 | const fromMe = message.senderId === authUser._id; 9 | const formattedTime = extractTime(message.createdAt); 10 | const chatClassName = fromMe ? "chat-end" : "chat-start"; 11 | const profilePic = fromMe ? authUser.profilePic : selectedConversation?.profilePic; 12 | const bubbleBgColor = fromMe ? "bg-blue-500" : ""; 13 | 14 | const shakeClass = message.shouldShake ? "shake" : ""; 15 | 16 | return ( 17 |
18 |
19 |
20 | Tailwind CSS chat bubble component 21 |
22 |
23 |
{message.message}
24 |
{formattedTime}
25 |
26 | ); 27 | }; 28 | export default Message; 29 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import toast from "react-hot-toast"; 3 | import { useAuthContext } from "../context/AuthContext"; 4 | 5 | const useLogin = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const login = async (username, password) => { 10 | const success = handleInputErrors(username, password); 11 | if (!success) return; 12 | setLoading(true); 13 | try { 14 | const res = await fetch("/api/auth/login", { 15 | method: "POST", 16 | headers: { "Content-Type": "application/json" }, 17 | body: JSON.stringify({ username, password }), 18 | }); 19 | 20 | const data = await res.json(); 21 | if (data.error) { 22 | throw new Error(data.error); 23 | } 24 | 25 | localStorage.setItem("chat-user", JSON.stringify(data)); 26 | setAuthUser(data); 27 | } catch (error) { 28 | toast.error(error.message); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | return { loading, login }; 35 | }; 36 | export default useLogin; 37 | 38 | function handleInputErrors(username, password) { 39 | if (!username || !password) { 40 | toast.error("Please fill in all fields"); 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import express from "express"; 3 | import dotenv from "dotenv"; 4 | import cookieParser from "cookie-parser"; 5 | 6 | import authRoutes from "./routes/auth.routes.js"; 7 | import messageRoutes from "./routes/message.routes.js"; 8 | import userRoutes from "./routes/user.routes.js"; 9 | 10 | import connectToMongoDB from "./db/connectToMongoDB.js"; 11 | import { app, server } from "./socket/socket.js"; 12 | 13 | dotenv.config(); 14 | 15 | const __dirname = path.resolve(); 16 | // PORT should be assigned after calling dotenv.config() because we need to access the env variables. Didn't realize while recording the video. Sorry for the confusion. 17 | const PORT = process.env.PORT || 5000; 18 | 19 | app.use(express.json()); // to parse the incoming requests with JSON payloads (from req.body) 20 | app.use(cookieParser()); 21 | 22 | app.use("/api/auth", authRoutes); 23 | app.use("/api/messages", messageRoutes); 24 | app.use("/api/users", userRoutes); 25 | 26 | app.use(express.static(path.join(__dirname, "/frontend/dist"))); 27 | 28 | app.get("*", (req, res) => { 29 | res.sendFile(path.join(__dirname, "frontend", "dist", "index.html")); 30 | }); 31 | 32 | server.listen(PORT, () => { 33 | connectToMongoDB(); 34 | console.log(`Server Running on port ${PORT}`); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/messages/Messages.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import useGetMessages from "../../hooks/useGetMessages"; 3 | import MessageSkeleton from "../skeletons/MessageSkeleton"; 4 | import Message from "./Message"; 5 | import useListenMessages from "../../hooks/useListenMessages"; 6 | 7 | const Messages = () => { 8 | const { messages, loading } = useGetMessages(); 9 | useListenMessages(); 10 | const lastMessageRef = useRef(); 11 | 12 | useEffect(() => { 13 | setTimeout(() => { 14 | lastMessageRef.current?.scrollIntoView({ behavior: "smooth" }); 15 | }, 100); 16 | }, [messages]); 17 | 18 | return ( 19 |
20 | {!loading && 21 | messages.length > 0 && 22 | messages.map((message) => ( 23 |
24 | 25 |
26 | ))} 27 | 28 | {loading && [...Array(3)].map((_, idx) => )} 29 | {!loading && messages.length === 0 && ( 30 |

Send a message to start the conversation

31 | )} 32 |
33 | ); 34 | }; 35 | export default Messages; 36 | 37 | // STARTER CODE SNIPPET 38 | // import Message from "./Message"; 39 | 40 | // const Messages = () => { 41 | // return ( 42 | //
43 | // 44 | // 45 | // 46 | // 47 | // 48 | // 49 | // 50 | // 51 | // 52 | // 53 | // 54 | // 55 | //
56 | // ); 57 | // }; 58 | // export default Messages; 59 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSignup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import toast from "react-hot-toast"; 3 | import { useAuthContext } from "../context/AuthContext"; 4 | 5 | const useSignup = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const signup = async ({ fullName, username, password, confirmPassword, gender }) => { 10 | const success = handleInputErrors({ fullName, username, password, confirmPassword, gender }); 11 | if (!success) return; 12 | 13 | setLoading(true); 14 | try { 15 | const res = await fetch("/api/auth/signup", { 16 | method: "POST", 17 | headers: { "Content-Type": "application/json" }, 18 | body: JSON.stringify({ fullName, username, password, confirmPassword, gender }), 19 | }); 20 | 21 | const data = await res.json(); 22 | if (data.error) { 23 | throw new Error(data.error); 24 | } 25 | localStorage.setItem("chat-user", JSON.stringify(data)); 26 | setAuthUser(data); 27 | } catch (error) { 28 | toast.error(error.message); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | return { loading, signup }; 35 | }; 36 | export default useSignup; 37 | 38 | function handleInputErrors({ fullName, username, password, confirmPassword, gender }) { 39 | if (!fullName || !username || !password || !confirmPassword || !gender) { 40 | toast.error("Please fill in all fields"); 41 | return false; 42 | } 43 | 44 | if (password !== confirmPassword) { 45 | toast.error("Passwords do not match"); 46 | return false; 47 | } 48 | 49 | if (password.length < 6) { 50 | toast.error("Password must be at least 6 characters"); 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/messages/MessageInput.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { BsSend } from "react-icons/bs"; 3 | import useSendMessage from "../../hooks/useSendMessage"; 4 | 5 | const MessageInput = () => { 6 | const [message, setMessage] = useState(""); 7 | const { loading, sendMessage } = useSendMessage(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | if (!message) return; 12 | await sendMessage(message); 13 | setMessage(""); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | setMessage(e.target.value)} 25 | /> 26 | 29 |
30 |
31 | ); 32 | }; 33 | export default MessageInput; 34 | 35 | // STARTER CODE SNIPPET 36 | // import { BsSend } from "react-icons/bs"; 37 | 38 | // const MessageInput = () => { 39 | // return ( 40 | //
41 | //
42 | // 47 | // 50 | //
51 | //
52 | // ); 53 | // }; 54 | // export default MessageInput; 55 | -------------------------------------------------------------------------------- /frontend/src/pages/signup/GenderCheckbox.jsx: -------------------------------------------------------------------------------- 1 | const GenderCheckbox = ({ onCheckboxChange, selectedGender }) => { 2 | return ( 3 |
4 |
5 | 14 |
15 |
16 | 25 |
26 |
27 | ); 28 | }; 29 | export default GenderCheckbox; 30 | 31 | // STARTER CODE FOR THIS FILE 32 | // const GenderCheckbox = () => { 33 | // return ( 34 | //
35 | //
36 | // 40 | //
41 | //
42 | // 46 | //
47 | //
48 | // ); 49 | // }; 50 | // export default GenderCheckbox; 51 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/SearchInput.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { IoSearchSharp } from "react-icons/io5"; 3 | import useConversation from "../../zustand/useConversation"; 4 | import useGetConversations from "../../hooks/useGetConversations"; 5 | import toast from "react-hot-toast"; 6 | 7 | const SearchInput = () => { 8 | const [search, setSearch] = useState(""); 9 | const { setSelectedConversation } = useConversation(); 10 | const { conversations } = useGetConversations(); 11 | 12 | const handleSubmit = (e) => { 13 | e.preventDefault(); 14 | if (!search) return; 15 | if (search.length < 3) { 16 | return toast.error("Search term must be at least 3 characters long"); 17 | } 18 | 19 | const conversation = conversations.find((c) => c.fullName.toLowerCase().includes(search.toLowerCase())); 20 | 21 | if (conversation) { 22 | setSelectedConversation(conversation); 23 | setSearch(""); 24 | } else toast.error("No such user found!"); 25 | }; 26 | return ( 27 |
28 | setSearch(e.target.value)} 34 | /> 35 | 38 |
39 | ); 40 | }; 41 | export default SearchInput; 42 | 43 | // STARTER CODE SNIPPET 44 | // import { IoSearchSharp } from "react-icons/io5"; 45 | 46 | // const SearchInput = () => { 47 | // return ( 48 | //
49 | // 50 | // 53 | //
54 | // ); 55 | // }; 56 | // export default SearchInput; 57 | -------------------------------------------------------------------------------- /backend/controllers/message.controller.js: -------------------------------------------------------------------------------- 1 | import Conversation from "../models/conversation.model.js"; 2 | import Message from "../models/message.model.js"; 3 | import { getReceiverSocketId, io } from "../socket/socket.js"; 4 | 5 | export const sendMessage = async (req, res) => { 6 | try { 7 | const { message } = req.body; 8 | const { id: receiverId } = req.params; 9 | const senderId = req.user._id; 10 | 11 | let conversation = await Conversation.findOne({ 12 | participants: { $all: [senderId, receiverId] }, 13 | }); 14 | 15 | if (!conversation) { 16 | conversation = await Conversation.create({ 17 | participants: [senderId, receiverId], 18 | }); 19 | } 20 | 21 | const newMessage = new Message({ 22 | senderId, 23 | receiverId, 24 | message, 25 | }); 26 | 27 | if (newMessage) { 28 | conversation.messages.push(newMessage._id); 29 | } 30 | 31 | // await conversation.save(); 32 | // await newMessage.save(); 33 | 34 | // this will run in parallel 35 | await Promise.all([conversation.save(), newMessage.save()]); 36 | 37 | // SOCKET IO FUNCTIONALITY WILL GO HERE 38 | const receiverSocketId = getReceiverSocketId(receiverId); 39 | if (receiverSocketId) { 40 | // io.to().emit() used to send events to specific client 41 | io.to(receiverSocketId).emit("newMessage", newMessage); 42 | } 43 | 44 | res.status(201).json(newMessage); 45 | } catch (error) { 46 | console.log("Error in sendMessage controller: ", error.message); 47 | res.status(500).json({ error: "Internal server error" }); 48 | } 49 | }; 50 | 51 | export const getMessages = async (req, res) => { 52 | try { 53 | const { id: userToChatId } = req.params; 54 | const senderId = req.user._id; 55 | 56 | const conversation = await Conversation.findOne({ 57 | participants: { $all: [senderId, userToChatId] }, 58 | }).populate("messages"); // NOT REFERENCE BUT ACTUAL MESSAGES 59 | 60 | if (!conversation) return res.status(200).json([]); 61 | 62 | const messages = conversation.messages; 63 | 64 | res.status(200).json(messages); 65 | } catch (error) { 66 | console.log("Error in getMessages controller: ", error.message); 67 | res.status(500).json({ error: "Internal server error" }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /frontend/src/components/messages/MessageContainer.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import useConversation from "../../zustand/useConversation"; 3 | import MessageInput from "./MessageInput"; 4 | import Messages from "./Messages"; 5 | import { TiMessages } from "react-icons/ti"; 6 | import { useAuthContext } from "../../context/AuthContext"; 7 | 8 | const MessageContainer = () => { 9 | const { selectedConversation, setSelectedConversation } = useConversation(); 10 | 11 | useEffect(() => { 12 | // cleanup function (unmounts) 13 | return () => setSelectedConversation(null); 14 | }, [setSelectedConversation]); 15 | 16 | return ( 17 |
18 | {!selectedConversation ? ( 19 | 20 | ) : ( 21 | <> 22 | {/* Header */} 23 |
24 | To:{" "} 25 | {selectedConversation.fullName} 26 |
27 | 28 | 29 | 30 | )} 31 |
32 | ); 33 | }; 34 | export default MessageContainer; 35 | 36 | const NoChatSelected = () => { 37 | const { authUser } = useAuthContext(); 38 | return ( 39 |
40 |
41 |

Welcome 👋 {authUser.fullName} ❄

42 |

Select a chat to start messaging

43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | // STARTER CODE SNIPPET 50 | // import MessageInput from "./MessageInput"; 51 | // import Messages from "./Messages"; 52 | 53 | // const MessageContainer = () => { 54 | // return ( 55 | //
56 | // <> 57 | // {/* Header */} 58 | //
59 | // To: John doe 60 | //
61 | 62 | // 63 | // 64 | // 65 | //
66 | // ); 67 | // }; 68 | // export default MessageContainer; 69 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Conversation.jsx: -------------------------------------------------------------------------------- 1 | import { useSocketContext } from "../../context/SocketContext"; 2 | import useConversation from "../../zustand/useConversation"; 3 | 4 | const Conversation = ({ conversation, lastIdx, emoji }) => { 5 | const { selectedConversation, setSelectedConversation } = useConversation(); 6 | 7 | const isSelected = selectedConversation?._id === conversation._id; 8 | const { onlineUsers } = useSocketContext(); 9 | const isOnline = onlineUsers.includes(conversation._id); 10 | 11 | return ( 12 | <> 13 |
setSelectedConversation(conversation)} 18 | > 19 |
20 |
21 | user avatar 22 |
23 |
24 | 25 |
26 |
27 |

{conversation.fullName}

28 | {emoji} 29 |
30 |
31 |
32 | 33 | {!lastIdx &&
} 34 | 35 | ); 36 | }; 37 | export default Conversation; 38 | 39 | // STARTER CODE SNIPPET 40 | // const Conversation = () => { 41 | // return ( 42 | // <> 43 | //
44 | //
45 | //
46 | // user avatar 50 | //
51 | //
52 | 53 | //
54 | //
55 | //

John Doe

56 | // 🎃 57 | //
58 | //
59 | //
60 | 61 | //
62 | // 63 | // ); 64 | // }; 65 | // export default Conversation; 66 | -------------------------------------------------------------------------------- /backend/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import User from "../models/user.model.js"; 3 | import generateTokenAndSetCookie from "../utils/generateToken.js"; 4 | 5 | export const signup = async (req, res) => { 6 | try { 7 | const { fullName, username, password, confirmPassword, gender } = req.body; 8 | 9 | if (password !== confirmPassword) { 10 | return res.status(400).json({ error: "Passwords don't match" }); 11 | } 12 | 13 | const user = await User.findOne({ username }); 14 | 15 | if (user) { 16 | return res.status(400).json({ error: "Username already exists" }); 17 | } 18 | 19 | // HASH PASSWORD HERE 20 | const salt = await bcrypt.genSalt(10); 21 | const hashedPassword = await bcrypt.hash(password, salt); 22 | 23 | // https://avatar-placeholder.iran.liara.run/ 24 | 25 | const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`; 26 | const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${username}`; 27 | 28 | const newUser = new User({ 29 | fullName, 30 | username, 31 | password: hashedPassword, 32 | gender, 33 | profilePic: gender === "male" ? boyProfilePic : girlProfilePic, 34 | }); 35 | 36 | if (newUser) { 37 | // Generate JWT token here 38 | generateTokenAndSetCookie(newUser._id, res); 39 | await newUser.save(); 40 | 41 | res.status(201).json({ 42 | _id: newUser._id, 43 | fullName: newUser.fullName, 44 | username: newUser.username, 45 | profilePic: newUser.profilePic, 46 | }); 47 | } else { 48 | res.status(400).json({ error: "Invalid user data" }); 49 | } 50 | } catch (error) { 51 | console.log("Error in signup controller", error.message); 52 | res.status(500).json({ error: "Internal Server Error" }); 53 | } 54 | }; 55 | 56 | export const login = async (req, res) => { 57 | try { 58 | const { username, password } = req.body; 59 | const user = await User.findOne({ username }); 60 | const isPasswordCorrect = await bcrypt.compare(password, user?.password || ""); 61 | 62 | if (!user || !isPasswordCorrect) { 63 | return res.status(400).json({ error: "Invalid username or password" }); 64 | } 65 | 66 | generateTokenAndSetCookie(user._id, res); 67 | 68 | res.status(200).json({ 69 | _id: user._id, 70 | fullName: user.fullName, 71 | username: user.username, 72 | profilePic: user.profilePic, 73 | }); 74 | } catch (error) { 75 | console.log("Error in login controller", error.message); 76 | res.status(500).json({ error: "Internal Server Error" }); 77 | } 78 | }; 79 | 80 | export const logout = (req, res) => { 81 | try { 82 | res.cookie("jwt", "", { maxAge: 0 }); 83 | res.status(200).json({ message: "Logged out successfully" }); 84 | } catch (error) { 85 | console.log("Error in logout controller", error.message); 86 | res.status(500).json({ error: "Internal Server Error" }); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /frontend/src/pages/login/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import useLogin from "../../hooks/useLogin"; 4 | 5 | const Login = () => { 6 | const [username, setUsername] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | 9 | const { loading, login } = useLogin(); 10 | 11 | const handleSubmit = async (e) => { 12 | e.preventDefault(); 13 | await login(username, password); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

20 | Login 21 | ChatApp 22 |

23 | 24 |
25 |
26 | 29 | setUsername(e.target.value)} 35 | /> 36 |
37 | 38 |
39 | 42 | setPassword(e.target.value)} 48 | /> 49 |
50 | 51 | {"Don't"} have an account? 52 | 53 | 54 |
55 | 58 |
59 |
60 |
61 |
62 | ); 63 | }; 64 | export default Login; 65 | 66 | // STARTER CODE FOR THIS FILE 67 | // const Login = () => { 68 | // return ( 69 | //
70 | //
71 | //

72 | // Login 73 | // ChatApp 74 | //

75 | 76 | //
77 | //
78 | // 81 | // 82 | //
83 | 84 | //
85 | // 88 | // 93 | //
94 | // 95 | // {"Don't"} have an account? 96 | // 97 | 98 | //
99 | // 100 | //
101 | //
102 | //
103 | //
104 | // ); 105 | // }; 106 | // export default Login; 107 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/pages/signup/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import GenderCheckbox from "./GenderCheckbox"; 3 | import { useState } from "react"; 4 | import useSignup from "../../hooks/useSignup"; 5 | 6 | const SignUp = () => { 7 | const [inputs, setInputs] = useState({ 8 | fullName: "", 9 | username: "", 10 | password: "", 11 | confirmPassword: "", 12 | gender: "", 13 | }); 14 | 15 | const { loading, signup } = useSignup(); 16 | 17 | const handleCheckboxChange = (gender) => { 18 | setInputs({ ...inputs, gender }); 19 | }; 20 | 21 | const handleSubmit = async (e) => { 22 | e.preventDefault(); 23 | await signup(inputs); 24 | }; 25 | 26 | return ( 27 |
28 |
29 |

30 | Sign Up ChatApp 31 |

32 | 33 |
34 |
35 | 38 | setInputs({ ...inputs, fullName: e.target.value })} 44 | /> 45 |
46 | 47 |
48 | 51 | setInputs({ ...inputs, username: e.target.value })} 57 | /> 58 |
59 | 60 |
61 | 64 | setInputs({ ...inputs, password: e.target.value })} 70 | /> 71 |
72 | 73 |
74 | 77 | setInputs({ ...inputs, confirmPassword: e.target.value })} 83 | /> 84 |
85 | 86 | 87 | 88 | 93 | Already have an account? 94 | 95 | 96 |
97 | 100 |
101 | 102 |
103 |
104 | ); 105 | }; 106 | export default SignUp; 107 | 108 | // STARTER CODE FOR THE SIGNUP COMPONENT 109 | // import GenderCheckbox from "./GenderCheckbox"; 110 | 111 | // const SignUp = () => { 112 | // return ( 113 | //
114 | //
115 | //

116 | // Sign Up ChatApp 117 | //

118 | 119 | //
120 | //
121 | // 124 | // 125 | //
126 | 127 | //
128 | // 131 | // 132 | //
133 | 134 | //
135 | // 138 | // 143 | //
144 | 145 | //
146 | // 149 | // 154 | //
155 | 156 | // 157 | 158 | // 159 | // Already have an account? 160 | // 161 | 162 | //
163 | // 164 | //
165 | // 166 | //
167 | //
168 | // ); 169 | // }; 170 | // export default SignUp; 171 | --------------------------------------------------------------------------------