├── 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 |
11 |
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 | 
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 |
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 |

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 |
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 | //
52 | // );
53 | // };
54 | // export default MessageInput;
55 |
--------------------------------------------------------------------------------
/frontend/src/pages/signup/GenderCheckbox.jsx:
--------------------------------------------------------------------------------
1 | const GenderCheckbox = ({ onCheckboxChange, selectedGender }) => {
2 | return (
3 |
27 | );
28 | };
29 | export default GenderCheckbox;
30 |
31 | // STARTER CODE FOR THIS FILE
32 | // const GenderCheckbox = () => {
33 | // return (
34 | //
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 |
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 | //
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 |

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 | //

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 |
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 | //
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 |
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 | //
166 | //
167 | //
168 | // );
169 | // };
170 | // export default SignUp;
171 |
--------------------------------------------------------------------------------