Repository: burakorkmez/chatify Branch: master Commit: 6fa3b3af325e Files: 55 Total size: 65.7 KB Directory structure: gitextract_8axr8dvu/ ├── .gitignore ├── README.MD ├── backend/ │ ├── package.json │ └── src/ │ ├── controllers/ │ │ ├── auth.controller.js │ │ └── message.controller.js │ ├── emails/ │ │ ├── emailHandlers.js │ │ └── emailTemplates.js │ ├── lib/ │ │ ├── arcjet.js │ │ ├── cloudinary.js │ │ ├── db.js │ │ ├── env.js │ │ ├── resend.js │ │ ├── socket.js │ │ └── utils.js │ ├── middleware/ │ │ ├── arcjet.middleware.js │ │ ├── auth.middleware.js │ │ └── socket.auth.middleware.js │ ├── models/ │ │ ├── Message.js │ │ └── User.js │ ├── routes/ │ │ ├── auth.route.js │ │ └── message.route.js │ └── server.js ├── frontend/ │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── App.jsx │ │ ├── components/ │ │ │ ├── ActiveTabSwitch.jsx │ │ │ ├── BorderAnimatedContainer.jsx │ │ │ ├── ChatContainer.jsx │ │ │ ├── ChatHeader.jsx │ │ │ ├── ChatsList.jsx │ │ │ ├── ContactList.jsx │ │ │ ├── MessageInput.jsx │ │ │ ├── MessagesLoadingSkeleton.jsx │ │ │ ├── NoChatHistoryPlaceholder.jsx │ │ │ ├── NoChatsFound.jsx │ │ │ ├── NoConversationPlaceholder.jsx │ │ │ ├── PageLoader.jsx │ │ │ ├── ProfileHeader.jsx │ │ │ └── UsersLoadingSkeleton.jsx │ │ ├── hooks/ │ │ │ └── useKeyboardSound.js │ │ ├── index.css │ │ ├── lib/ │ │ │ └── axios.js │ │ ├── main.jsx │ │ ├── pages/ │ │ │ ├── ChatPage.jsx │ │ │ ├── LoginPage.jsx │ │ │ └── SignUpPage.jsx │ │ └── store/ │ │ ├── useAuthStore.js │ │ └── useChatStore.js │ ├── tailwind.config.js │ └── vite.config.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules .env ================================================ FILE: README.MD ================================================

✨ Full-Stack Chat App with Auth & Emails ✨

![Demo App](/frontend/public/screenshot-for-readme.png) Highlights: - 🔐 Custom JWT Authentication (no 3rd-party auth) - ⚡ Real-time Messaging via Socket.io - 🟢 Online/Offline Presence Indicators - 🔔 Notification & Typing Sounds (with toggle) - 📨 Welcome Emails on Signup (Resend) - 🗂️ Image Uploads (Cloudinary) - 🧰 REST API with Node.js & Express - 🧱 MongoDB for Data Persistence - 🚦 API Rate-Limiting powered by Arcjet - 🎨 Beautiful UI with React, Tailwind CSS & DaisyUI - 🧠 Zustand for State Management - 🧑‍💻 Git & GitHub Workflow (branches, PRs, merges) - 🚀 Easy Deployment (free-tier friendly with Sevalla) --- ## 🧪 .env Setup ### Backend (`/backend`) ```bash PORT=3000 MONGO_URI=your_mongo_uri_here NODE_ENV=development JWT_SECRET=your_jwt_secret RESEND_API_KEY=your_resend_api_key EMAIL_FROM=your_email_from_address EMAIL_FROM_NAME=your_email_from_name CLIENT_URL=http://localhost:5173 CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name CLOUDINARY_API_KEY=your_cloudinary_api_key CLOUDINARY_API_SECRET=your_cloudinary_api_secret ARCJET_KEY=your_arcjet_key ARCJET_ENV=development ``` --- ## 🔧 Run the Backend ```bash cd backend npm install npm run dev ``` ## 💻 Run the Frontend ```bash cd frontend npm install npm run dev ``` ================================================ FILE: backend/package.json ================================================ { "name": "backend", "version": "1.0.0", "main": "index.js", "scripts": { "dev": "nodemon src/server.js", "start": "node src/server.js" }, "keywords": [], "type": "module", "author": "", "license": "ISC", "description": "", "dependencies": { "@arcjet/inspect": "^1.0.0-beta.10", "@arcjet/node": "^1.0.0-beta.10", "bcryptjs": "^2.4.3", "cloudinary": "^2.5.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.10.1", "resend": "^6.0.2", "socket.io": "^4.8.1" }, "devDependencies": { "nodemon": "^3.1.10" } } ================================================ FILE: backend/src/controllers/auth.controller.js ================================================ import { sendWelcomeEmail } from "../emails/emailHandlers.js"; import { generateToken } from "../lib/utils.js"; import User from "../models/User.js"; import bcrypt from "bcryptjs"; import { ENV } from "../lib/env.js"; import cloudinary from "../lib/cloudinary.js"; export const signup = async (req, res) => { const { fullName, email, password } = req.body; try { if (!fullName || !email || !password) { return res.status(400).json({ message: "All fields are required" }); } if (password.length < 6) { return res.status(400).json({ message: "Password must be at least 6 characters" }); } // check if emailis valid: regex const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ message: "Invalid email format" }); } const user = await User.findOne({ email }); if (user) return res.status(400).json({ message: "Email already exists" }); // 123456 => $dnjasdkasj_?dmsakmk const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); const newUser = new User({ fullName, email, password: hashedPassword, }); if (newUser) { // before CR: // generateToken(newUser._id, res); // await newUser.save(); // after CR: // Persist user first, then issue auth cookie const savedUser = await newUser.save(); generateToken(savedUser._id, res); res.status(201).json({ _id: newUser._id, fullName: newUser.fullName, email: newUser.email, profilePic: newUser.profilePic, }); try { await sendWelcomeEmail(savedUser.email, savedUser.fullName, ENV.CLIENT_URL); } catch (error) { console.error("Failed to send welcome email:", error); } } else { res.status(400).json({ message: "Invalid user data" }); } } catch (error) { console.log("Error in signup controller:", error); res.status(500).json({ message: "Internal server error" }); } }; export const login = async (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } try { const user = await User.findOne({ email }); if (!user) return res.status(400).json({ message: "Invalid credentials" }); // never tell the client which one is incorrect: password or email const isPasswordCorrect = await bcrypt.compare(password, user.password); if (!isPasswordCorrect) return res.status(400).json({ message: "Invalid credentials" }); generateToken(user._id, res); res.status(200).json({ _id: user._id, fullName: user.fullName, email: user.email, profilePic: user.profilePic, }); } catch (error) { console.error("Error in login controller:", error); res.status(500).json({ message: "Internal server error" }); } }; export const logout = (_, res) => { res.cookie("jwt", "", { maxAge: 0 }); res.status(200).json({ message: "Logged out successfully" }); }; export const updateProfile = async (req, res) => { try { const { profilePic } = req.body; if (!profilePic) return res.status(400).json({ message: "Profile pic is required" }); const userId = req.user._id; const uploadResponse = await cloudinary.uploader.upload(profilePic); const updatedUser = await User.findByIdAndUpdate( userId, { profilePic: uploadResponse.secure_url }, { new: true } ); res.status(200).json(updatedUser); } catch (error) { console.log("Error in update profile:", error); res.status(500).json({ message: "Internal server error" }); } }; ================================================ FILE: backend/src/controllers/message.controller.js ================================================ import cloudinary from "../lib/cloudinary.js"; import { getReceiverSocketId, io } from "../lib/socket.js"; import Message from "../models/Message.js"; import User from "../models/User.js"; export const getAllContacts = async (req, res) => { try { const loggedInUserId = req.user._id; const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } }).select("-password"); res.status(200).json(filteredUsers); } catch (error) { console.log("Error in getAllContacts:", error); res.status(500).json({ message: "Server error" }); } }; export const getMessagesByUserId = async (req, res) => { try { const myId = req.user._id; const { id: userToChatId } = req.params; const messages = await Message.find({ $or: [ { senderId: myId, receiverId: userToChatId }, { senderId: userToChatId, receiverId: myId }, ], }); res.status(200).json(messages); } catch (error) { console.log("Error in getMessages controller: ", error.message); res.status(500).json({ error: "Internal server error" }); } }; export const sendMessage = async (req, res) => { try { const { text, image } = req.body; const { id: receiverId } = req.params; const senderId = req.user._id; if (!text && !image) { return res.status(400).json({ message: "Text or image is required." }); } if (senderId.equals(receiverId)) { return res.status(400).json({ message: "Cannot send messages to yourself." }); } const receiverExists = await User.exists({ _id: receiverId }); if (!receiverExists) { return res.status(404).json({ message: "Receiver not found." }); } let imageUrl; if (image) { // upload base64 image to cloudinary const uploadResponse = await cloudinary.uploader.upload(image); imageUrl = uploadResponse.secure_url; } const newMessage = new Message({ senderId, receiverId, text, image: imageUrl, }); await newMessage.save(); const receiverSocketId = getReceiverSocketId(receiverId); if (receiverSocketId) { io.to(receiverSocketId).emit("newMessage", newMessage); } res.status(201).json(newMessage); } catch (error) { console.log("Error in sendMessage controller: ", error.message); res.status(500).json({ error: "Internal server error" }); } }; export const getChatPartners = async (req, res) => { try { const loggedInUserId = req.user._id; // find all the messages where the logged-in user is either sender or receiver const messages = await Message.find({ $or: [{ senderId: loggedInUserId }, { receiverId: loggedInUserId }], }); const chatPartnerIds = [ ...new Set( messages.map((msg) => msg.senderId.toString() === loggedInUserId.toString() ? msg.receiverId.toString() : msg.senderId.toString() ) ), ]; const chatPartners = await User.find({ _id: { $in: chatPartnerIds } }).select("-password"); res.status(200).json(chatPartners); } catch (error) { console.error("Error in getChatPartners: ", error.message); res.status(500).json({ error: "Internal server error" }); } }; ================================================ FILE: backend/src/emails/emailHandlers.js ================================================ import { resendClient, sender } from "../lib/resend.js"; import { createWelcomeEmailTemplate } from "../emails/emailTemplates.js"; export const sendWelcomeEmail = async (email, name, clientURL) => { const { data, error } = await resendClient.emails.send({ from: `${sender.name} <${sender.email}>`, to: email, subject: "Welcome to Chatify!", html: createWelcomeEmailTemplate(name, clientURL), }); if (error) { console.error("Error sending welcome email:", error); throw new Error("Failed to send welcome email"); } console.log("Welcome Email sent successfully", data); }; ================================================ FILE: backend/src/emails/emailTemplates.js ================================================ export function createWelcomeEmailTemplate(name, clientURL) { return ` Welcome to Messenger
Messenger Logo

Welcome to Messenger!

Hello ${name},

We're excited to have you join our messaging platform! Messenger connects you with friends, family, and colleagues in real-time, no matter where they are.

Get started in just a few steps:

Open Messenger

If you need any help or have questions, we're always here to assist you.

Happy messaging!

Best regards,
The Messenger Team

© 2025 Messenger. All rights reserved.

Privacy Policy Terms of Service Contact Us

`; } ================================================ FILE: backend/src/lib/arcjet.js ================================================ import arcjet, { shield, detectBot, slidingWindow } from "@arcjet/node"; import { ENV } from "./env.js"; const aj = arcjet({ key: ENV.ARCJET_KEY, rules: [ // Shield protects your app from common attacks e.g. SQL injection shield({ mode: "LIVE" }), // Create a bot detection rule detectBot({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only // Block all bots except the following allow: [ "CATEGORY:SEARCH_ENGINE", // Google, Bing, etc // Uncomment to allow these other common bot categories // See the full list at https://arcjet.com/bot-list //"CATEGORY:MONITOR", // Uptime monitoring services //"CATEGORY:PREVIEW", // Link previews e.g. Slack, Discord ], }), // Create a token bucket rate limit. Other algorithms are supported. slidingWindow({ mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only max: 100, interval: 60, }), ], }); export default aj; ================================================ FILE: backend/src/lib/cloudinary.js ================================================ import { v2 as cloudinary } from "cloudinary"; import { ENV } from "./env.js"; cloudinary.config({ cloud_name: ENV.CLOUDINARY_CLOUD_NAME, api_key: ENV.CLOUDINARY_API_KEY, api_secret: ENV.CLOUDINARY_API_SECRET, }); export default cloudinary; ================================================ FILE: backend/src/lib/db.js ================================================ import mongoose from "mongoose"; import { ENV } from "./env.js"; export const connectDB = async () => { try { const { MONGO_URI } = ENV; if (!MONGO_URI) throw new Error("MONGO_URI is not set"); const conn = await mongoose.connect(ENV.MONGO_URI); console.log("MONGODB CONNECTED:", conn.connection.host); } catch (error) { console.error("Error connection to MONGODB:", error); process.exit(1); // 1 status code means fail, 0 means success } }; ================================================ FILE: backend/src/lib/env.js ================================================ import "dotenv/config"; export const ENV = { PORT: process.env.PORT, MONGO_URI: process.env.MONGO_URI, JWT_SECRET: process.env.JWT_SECRET, NODE_ENV: process.env.NODE_ENV, CLIENT_URL: process.env.CLIENT_URL, RESEND_API_KEY: process.env.RESEND_API_KEY, EMAIL_FROM: process.env.EMAIL_FROM, EMAIL_FROM_NAME: process.env.EMAIL_FROM_NAME, CLOUDINARY_CLOUD_NAME: process.env.CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET: process.env.CLOUDINARY_API_SECRET, ARCJET_KEY: process.env.ARCJET_KEY, ARCJET_ENV: process.env.ARCJET_ENV, }; ================================================ FILE: backend/src/lib/resend.js ================================================ import { Resend } from "resend"; import { ENV } from "./env.js"; export const resendClient = new Resend(ENV.RESEND_API_KEY); export const sender = { email: ENV.EMAIL_FROM, name: ENV.EMAIL_FROM_NAME, }; ================================================ FILE: backend/src/lib/socket.js ================================================ import { Server } from "socket.io"; import http from "http"; import express from "express"; import { ENV } from "./env.js"; import { socketAuthMiddleware } from "../middleware/socket.auth.middleware.js"; const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: [ENV.CLIENT_URL], credentials: true, }, }); // apply authentication middleware to all socket connections io.use(socketAuthMiddleware); // we will use this function to check if the user is online or not export function getReceiverSocketId(userId) { return userSocketMap[userId]; } // this is for storig online users const userSocketMap = {}; // {userId:socketId} io.on("connection", (socket) => { console.log("A user connected", socket.user.fullName); const userId = socket.userId; userSocketMap[userId] = socket.id; // io.emit() is used to send events to all connected clients io.emit("getOnlineUsers", Object.keys(userSocketMap)); // with socket.on we listen for events from clients socket.on("disconnect", () => { console.log("A user disconnected", socket.user.fullName); delete userSocketMap[userId]; io.emit("getOnlineUsers", Object.keys(userSocketMap)); }); }); export { io, app, server }; ================================================ FILE: backend/src/lib/utils.js ================================================ import jwt from "jsonwebtoken"; import { ENV } from "./env.js"; export const generateToken = (userId, res) => { const { JWT_SECRET } = ENV; if (!JWT_SECRET) { throw new Error("JWT_SECRET is not configured"); } const token = jwt.sign({ userId }, JWT_SECRET, { expiresIn: "7d", }); res.cookie("jwt", token, { maxAge: 7 * 24 * 60 * 60 * 1000, // MS httpOnly: true, // prevent XSS attacks: cross-site scripting sameSite: "strict", // CSRF attacks secure: ENV.NODE_ENV === "development" ? false : true, }); return token; }; // http://localhost // https://dsmakmk.com ================================================ FILE: backend/src/middleware/arcjet.middleware.js ================================================ import aj from "../lib/arcjet.js"; import { isSpoofedBot } from "@arcjet/inspect"; export const arcjetProtection = async (req, res, next) => { try { const decision = await aj.protect(req); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return res.status(429).json({ message: "Rate limit exceeded. Please try again later." }); } else if (decision.reason.isBot()) { return res.status(403).json({ message: "Bot access denied." }); } else { return res.status(403).json({ message: "Access denied by security policy.", }); } } // check for spoofed bots if (decision.results.some(isSpoofedBot)) { return res.status(403).json({ error: "Spoofed bot detected", message: "Malicious bot activity detected.", }); } next(); } catch (error) { console.log("Arcjet Protection Error:", error); next(); } }; ================================================ FILE: backend/src/middleware/auth.middleware.js ================================================ import jwt from "jsonwebtoken"; import User from "../models/User.js"; import { ENV } from "../lib/env.js"; export const protectRoute = async (req, res, next) => { try { const token = req.cookies.jwt; if (!token) return res.status(401).json({ message: "Unauthorized - No token provided" }); const decoded = jwt.verify(token, ENV.JWT_SECRET); if (!decoded) return res.status(401).json({ message: "Unauthorized - Invalid token" }); const user = await User.findById(decoded.userId).select("-password"); if (!user) return res.status(404).json({ message: "User not found" }); req.user = user; next(); } catch (error) { console.log("Error in protectRoute middleware:", error); res.status(500).json({ message: "Internal server error" }); } }; ================================================ FILE: backend/src/middleware/socket.auth.middleware.js ================================================ import jwt from "jsonwebtoken"; import User from "../models/User.js"; import { ENV } from "../lib/env.js"; export const socketAuthMiddleware = async (socket, next) => { try { // extract token from http-only cookies const token = socket.handshake.headers.cookie ?.split("; ") .find((row) => row.startsWith("jwt=")) ?.split("=")[1]; if (!token) { console.log("Socket connection rejected: No token provided"); return next(new Error("Unauthorized - No Token Provided")); } // verify the token const decoded = jwt.verify(token, ENV.JWT_SECRET); if (!decoded) { console.log("Socket connection rejected: Invalid token"); return next(new Error("Unauthorized - Invalid Token")); } // find the user fromdb const user = await User.findById(decoded.userId).select("-password"); if (!user) { console.log("Socket connection rejected: User not found"); return next(new Error("User not found")); } // attach user info to socket socket.user = user; socket.userId = user._id.toString(); console.log(`Socket authenticated for user: ${user.fullName} (${user._id})`); next(); } catch (error) { console.log("Error in socket authentication:", error.message); next(new Error("Unauthorized - Authentication failed")); } }; ================================================ FILE: backend/src/models/Message.js ================================================ import mongoose from "mongoose"; const messageSchema = new mongoose.Schema( { senderId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, }, receiverId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, }, text: { type: String, trim: true, maxlength: 2000, }, image: { type: String, }, }, { timestamps: true } ); const Message = mongoose.model("Message", messageSchema); export default Message; ================================================ FILE: backend/src/models/User.js ================================================ import mongoose from "mongoose"; const userSchema = new mongoose.Schema( { email: { type: String, required: true, unique: true, }, fullName: { type: String, required: true, }, password: { type: String, required: true, minlength: 6, }, profilePic: { type: String, default: "", }, }, { timestamps: true } // createdAt & updatedAt ); const User = mongoose.model("User", userSchema); export default User; ================================================ FILE: backend/src/routes/auth.route.js ================================================ import express from "express"; import { signup, login, logout, updateProfile } from "../controllers/auth.controller.js"; import { protectRoute } from "../middleware/auth.middleware.js"; import { arcjetProtection } from "../middleware/arcjet.middleware.js"; const router = express.Router(); router.use(arcjetProtection); router.post("/signup", signup); router.post("/login", login); router.post("/logout", logout); router.put("/update-profile", protectRoute, updateProfile); router.get("/check", protectRoute, (req, res) => res.status(200).json(req.user)); export default router; ================================================ FILE: backend/src/routes/message.route.js ================================================ import express from "express"; import { getAllContacts, getChatPartners, getMessagesByUserId, sendMessage, } from "../controllers/message.controller.js"; import { protectRoute } from "../middleware/auth.middleware.js"; import { arcjetProtection } from "../middleware/arcjet.middleware.js"; const router = express.Router(); // the middlewares execute in order - so requests get rate-limited first, then authenticated. // this is actually more efficient since unauthenticated requests get blocked by rate limiting before hitting the auth middleware. router.use(arcjetProtection, protectRoute); router.get("/contacts", getAllContacts); router.get("/chats", getChatPartners); router.get("/:id", getMessagesByUserId); router.post("/send/:id", sendMessage); export default router; ================================================ FILE: backend/src/server.js ================================================ import express from "express"; import cookieParser from "cookie-parser"; import path from "path"; import cors from "cors"; import authRoutes from "./routes/auth.route.js"; import messageRoutes from "./routes/message.route.js"; import { connectDB } from "./lib/db.js"; import { ENV } from "./lib/env.js"; import { app, server } from "./lib/socket.js"; const __dirname = path.resolve(); const PORT = ENV.PORT || 3000; app.use(express.json({ limit: "5mb" })); // req.body app.use(cors({ origin: ENV.CLIENT_URL, credentials: true })); app.use(cookieParser()); app.use("/api/auth", authRoutes); app.use("/api/messages", messageRoutes); // make ready for deployment if (ENV.NODE_ENV === "production") { app.use(express.static(path.join(__dirname, "../frontend/dist"))); app.get("*", (_, res) => { res.sendFile(path.join(__dirname, "../frontend", "dist", "index.html")); }); } server.listen(PORT, () => { console.log("Server running on port: " + PORT); connectDB(); }); ================================================ FILE: frontend/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: frontend/README.md ================================================ # React + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. ================================================ FILE: frontend/eslint.config.js ================================================ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ globalIgnores(['dist']), { files: ['**/*.{js,jsx}'], extends: [ js.configs.recommended, reactHooks.configs['recommended-latest'], reactRefresh.configs.vite, ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true }, sourceType: 'module', }, }, rules: { 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, ]) ================================================ FILE: frontend/index.html ================================================ Vite + React
================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "axios": "^1.11.0", "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hot-toast": "^2.6.0", "react-router": "^7.8.2", "socket.io-client": "^4.8.1", "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.33.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", "daisyui": "^4.12.24", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "vite": "^7.1.2" } } ================================================ FILE: frontend/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: frontend/src/App.jsx ================================================ import { Navigate, Route, Routes } from "react-router"; import ChatPage from "./pages/ChatPage"; import LoginPage from "./pages/LoginPage"; import SignUpPage from "./pages/SignUpPage"; import { useAuthStore } from "./store/useAuthStore"; import { useEffect } from "react"; import PageLoader from "./components/PageLoader"; import { Toaster } from "react-hot-toast"; function App() { const { checkAuth, isCheckingAuth, authUser } = useAuthStore(); useEffect(() => { checkAuth(); }, [checkAuth]); if (isCheckingAuth) return ; return (
{/* DECORATORS - GRID BG & GLOW SHAPES */}
: } /> : } /> : } />
); } export default App; ================================================ FILE: frontend/src/components/ActiveTabSwitch.jsx ================================================ import { useChatStore } from "../store/useChatStore"; function ActiveTabSwitch() { const { activeTab, setActiveTab } = useChatStore(); return (
); } export default ActiveTabSwitch; ================================================ FILE: frontend/src/components/BorderAnimatedContainer.jsx ================================================ // How to make animated gradient border 👇 // https://cruip-tutorials.vercel.app/animated-gradient-border/ function BorderAnimatedContainer({ children }) { return (
{children}
); } export default BorderAnimatedContainer; ================================================ FILE: frontend/src/components/ChatContainer.jsx ================================================ import { useEffect, useRef } from "react"; import { useAuthStore } from "../store/useAuthStore"; import { useChatStore } from "../store/useChatStore"; import ChatHeader from "./ChatHeader"; import NoChatHistoryPlaceholder from "./NoChatHistoryPlaceholder"; import MessageInput from "./MessageInput"; import MessagesLoadingSkeleton from "./MessagesLoadingSkeleton"; function ChatContainer() { const { selectedUser, getMessagesByUserId, messages, isMessagesLoading, subscribeToMessages, unsubscribeFromMessages, } = useChatStore(); const { authUser } = useAuthStore(); const messageEndRef = useRef(null); useEffect(() => { getMessagesByUserId(selectedUser._id); subscribeToMessages(); // clean up return () => unsubscribeFromMessages(); }, [selectedUser, getMessagesByUserId, subscribeToMessages, unsubscribeFromMessages]); useEffect(() => { if (messageEndRef.current) { messageEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages]); return ( <>
{messages.length > 0 && !isMessagesLoading ? (
{messages.map((msg) => (
{msg.image && ( Shared )} {msg.text &&

{msg.text}

}

{new Date(msg.createdAt).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", })}

))} {/* 👇 scroll target */}
) : isMessagesLoading ? ( ) : ( )}
); } export default ChatContainer; ================================================ FILE: frontend/src/components/ChatHeader.jsx ================================================ import { XIcon } from "lucide-react"; import { useChatStore } from "../store/useChatStore"; import { useEffect } from "react"; import { useAuthStore } from "../store/useAuthStore"; function ChatHeader() { const { selectedUser, setSelectedUser } = useChatStore(); const { onlineUsers } = useAuthStore(); const isOnline = onlineUsers.includes(selectedUser._id); useEffect(() => { const handleEscKey = (event) => { if (event.key === "Escape") setSelectedUser(null); }; window.addEventListener("keydown", handleEscKey); // cleanup function return () => window.removeEventListener("keydown", handleEscKey); }, [setSelectedUser]); return (
{selectedUser.fullName}

{selectedUser.fullName}

{isOnline ? "Online" : "Offline"}

); } export default ChatHeader; ================================================ FILE: frontend/src/components/ChatsList.jsx ================================================ import { useEffect } from "react"; import { useChatStore } from "../store/useChatStore"; import UsersLoadingSkeleton from "./UsersLoadingSkeleton"; import NoChatsFound from "./NoChatsFound"; import { useAuthStore } from "../store/useAuthStore"; function ChatsList() { const { getMyChatPartners, chats, isUsersLoading, setSelectedUser } = useChatStore(); const { onlineUsers } = useAuthStore(); useEffect(() => { getMyChatPartners(); }, [getMyChatPartners]); if (isUsersLoading) return ; if (chats.length === 0) return ; return ( <> {chats.map((chat) => (
setSelectedUser(chat)} >
{chat.fullName}

{chat.fullName}

))} ); } export default ChatsList; ================================================ FILE: frontend/src/components/ContactList.jsx ================================================ import { useEffect } from "react"; import { useChatStore } from "../store/useChatStore"; import UsersLoadingSkeleton from "./UsersLoadingSkeleton"; import { useAuthStore } from "../store/useAuthStore"; function ContactList() { const { getAllContacts, allContacts, setSelectedUser, isUsersLoading } = useChatStore(); const { onlineUsers } = useAuthStore(); useEffect(() => { getAllContacts(); }, [getAllContacts]); if (isUsersLoading) return ; return ( <> {allContacts.map((contact) => (
setSelectedUser(contact)} >

{contact.fullName}

))} ); } export default ContactList; ================================================ FILE: frontend/src/components/MessageInput.jsx ================================================ import { useRef, useState } from "react"; import useKeyboardSound from "../hooks/useKeyboardSound"; import { useChatStore } from "../store/useChatStore"; import toast from "react-hot-toast"; import { ImageIcon, SendIcon, XIcon } from "lucide-react"; function MessageInput() { const { playRandomKeyStrokeSound } = useKeyboardSound(); const [text, setText] = useState(""); const [imagePreview, setImagePreview] = useState(null); const fileInputRef = useRef(null); const { sendMessage, isSoundEnabled } = useChatStore(); const handleSendMessage = (e) => { e.preventDefault(); if (!text.trim() && !imagePreview) return; if (isSoundEnabled) playRandomKeyStrokeSound(); sendMessage({ text: text.trim(), image: imagePreview, }); setText(""); setImagePreview(""); if (fileInputRef.current) fileInputRef.current.value = ""; }; const handleImageChange = (e) => { const file = e.target.files[0]; if (!file.type.startsWith("image/")) { toast.error("Please select an image file"); return; } const reader = new FileReader(); reader.onloadend = () => setImagePreview(reader.result); reader.readAsDataURL(file); }; const removeImage = () => { setImagePreview(null); if (fileInputRef.current) fileInputRef.current.value = ""; }; return (
{imagePreview && (
Preview
)}
{ setText(e.target.value); isSoundEnabled && playRandomKeyStrokeSound(); }} className="flex-1 bg-slate-800/50 border border-slate-700/50 rounded-lg py-2 px-4" placeholder="Type your message..." />
); } export default MessageInput; ================================================ FILE: frontend/src/components/MessagesLoadingSkeleton.jsx ================================================ function MessagesLoadingSkeleton() { return (
{[...Array(6)].map((_, index) => (
))}
); } export default MessagesLoadingSkeleton; ================================================ FILE: frontend/src/components/NoChatHistoryPlaceholder.jsx ================================================ import { MessageCircleIcon } from "lucide-react"; const NoChatHistoryPlaceholder = ({ name }) => { return (

Start your conversation with {name}

This is the beginning of your conversation. Send a message to start chatting!

); }; export default NoChatHistoryPlaceholder; ================================================ FILE: frontend/src/components/NoChatsFound.jsx ================================================ import { MessageCircleIcon } from "lucide-react"; import { useChatStore } from "../store/useChatStore"; function NoChatsFound() { const { setActiveTab } = useChatStore(); return (

No conversations yet

Start a new chat by selecting a contact from the contacts tab

); } export default NoChatsFound; ================================================ FILE: frontend/src/components/NoConversationPlaceholder.jsx ================================================ import { MessageCircleIcon } from "lucide-react"; const NoConversationPlaceholder = () => { return (

Select a conversation

Choose a contact from the sidebar to start chatting or continue a previous conversation.

); }; export default NoConversationPlaceholder; ================================================ FILE: frontend/src/components/PageLoader.jsx ================================================ import { LoaderIcon } from "lucide-react"; function PageLoader() { return (
); } export default PageLoader; ================================================ FILE: frontend/src/components/ProfileHeader.jsx ================================================ import { useState, useRef } from "react"; import { LogOutIcon, VolumeOffIcon, Volume2Icon } from "lucide-react"; import { useAuthStore } from "../store/useAuthStore"; import { useChatStore } from "../store/useChatStore"; const mouseClickSound = new Audio("/sounds/mouse-click.mp3"); function ProfileHeader() { const { logout, authUser, updateProfile } = useAuthStore(); const { isSoundEnabled, toggleSound } = useChatStore(); const [selectedImg, setSelectedImg] = useState(null); const fileInputRef = useRef(null); const handleImageUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.readAsDataURL(file); reader.onloadend = async () => { const base64Image = reader.result; setSelectedImg(base64Image); await updateProfile({ profilePic: base64Image }); }; }; return (
{/* AVATAR */}
{/* USERNAME & ONLINE TEXT */}

{authUser.fullName}

Online

{/* BUTTONS */}
{/* LOGOUT BTN */} {/* SOUND TOGGLE BTN */}
); } export default ProfileHeader; ================================================ FILE: frontend/src/components/UsersLoadingSkeleton.jsx ================================================ function UsersLoadingSkeleton() { return (
{[1, 2, 3].map((item) => (
))}
); } export default UsersLoadingSkeleton; ================================================ FILE: frontend/src/hooks/useKeyboardSound.js ================================================ // audio setup const keyStrokeSounds = [ new Audio("/sounds/keystroke1.mp3"), new Audio("/sounds/keystroke2.mp3"), new Audio("/sounds/keystroke3.mp3"), new Audio("/sounds/keystroke4.mp3"), ]; function useKeyboardSound() { const playRandomKeyStrokeSound = () => { const randomSound = keyStrokeSounds[Math.floor(Math.random() * keyStrokeSounds.length)]; randomSound.currentTime = 0; // this is for a better UX, def add this randomSound.play().catch((error) => console.log("Audio play failed:", error)); }; return { playRandomKeyStrokeSound }; } export default useKeyboardSound; ================================================ FILE: frontend/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @property --border-angle { inherits: false; initial-value: 0deg; syntax: ""; } .input { @apply w-full bg-slate-800/50 border border-slate-700 rounded-lg py-2 pl-10 pr-4 text-slate-200 placeholder-slate-400 focus:ring-2 focus:ring-cyan-500 focus:border-transparent; } .auth-input-icon { @apply absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 size-5; } .auth-input-label { @apply block text-sm font-medium text-slate-300 mb-2; } .auth-badge { @apply inline-flex items-center px-3 py-1 text-xs font-medium rounded-full bg-cyan-500/20 text-cyan-300; } .auth-btn { @apply w-full bg-cyan-500 text-white rounded-lg py-2.5 font-medium hover:bg-cyan-600 focus:ring-2 focus:ring-cyan-500; } .auth-link { @apply px-4 py-2 inline-block bg-cyan-500/10 rounded-lg text-cyan-400 hover:text-cyan-500 text-sm transition-colors; } ================================================ FILE: frontend/src/lib/axios.js ================================================ import axios from "axios"; export const axiosInstance = axios.create({ baseURL: import.meta.env.MODE === "development" ? "http://localhost:3000/api" : "/api", withCredentials: true, }); ================================================ FILE: frontend/src/main.jsx ================================================ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; import { BrowserRouter } from "react-router"; createRoot(document.getElementById("root")).render( ); ================================================ FILE: frontend/src/pages/ChatPage.jsx ================================================ import { useChatStore } from "../store/useChatStore"; import BorderAnimatedContainer from "../components/BorderAnimatedContainer"; import ProfileHeader from "../components/ProfileHeader"; import ActiveTabSwitch from "../components/ActiveTabSwitch"; import ChatsList from "../components/ChatsList"; import ContactList from "../components/ContactList"; import ChatContainer from "../components/ChatContainer"; import NoConversationPlaceholder from "../components/NoConversationPlaceholder"; function ChatPage() { const { activeTab, selectedUser } = useChatStore(); return (
{/* LEFT SIDE */}
{activeTab === "chats" ? : }
{/* RIGHT SIDE */}
{selectedUser ? : }
); } export default ChatPage; ================================================ FILE: frontend/src/pages/LoginPage.jsx ================================================ import { useState } from "react"; import { useAuthStore } from "../store/useAuthStore"; import BorderAnimatedContainer from "../components/BorderAnimatedContainer"; import { MessageCircleIcon, MailIcon, LoaderIcon, LockIcon } from "lucide-react"; import { Link } from "react-router"; function LoginPage() { const [formData, setFormData] = useState({ email: "", password: "" }); const { login, isLoggingIn } = useAuthStore(); const handleSubmit = (e) => { e.preventDefault(); login(formData); }; return (
{/* FORM CLOUMN - LEFT SIDE */}
{/* HEADING TEXT */}

Welcome Back

Login to access to your account

{/* FORM */}
{/* EMAIL INPUT */}
setFormData({ ...formData, email: e.target.value })} className="input" placeholder="johndoe@gmail.com" />
{/* PASSWORD INPUT */}
setFormData({ ...formData, password: e.target.value })} className="input" placeholder="Enter your password" />
{/* SUBMIT BUTTON */}
Don't have an account? Sign Up
{/* FORM ILLUSTRATION - RIGHT SIDE */}
People using mobile devices

Connect anytime, anywhere

Free Easy Setup Private
); } export default LoginPage; ================================================ FILE: frontend/src/pages/SignUpPage.jsx ================================================ import { useState } from "react"; import { useAuthStore } from "../store/useAuthStore"; import BorderAnimatedContainer from "../components/BorderAnimatedContainer"; import { MessageCircleIcon, LockIcon, MailIcon, UserIcon, LoaderIcon } from "lucide-react"; import { Link } from "react-router"; function SignUpPage() { const [formData, setFormData] = useState({ fullName: "", email: "", password: "" }); const { signup, isSigningUp } = useAuthStore(); const handleSubmit = (e) => { e.preventDefault(); signup(formData); }; return (
{/* FORM CLOUMN - LEFT SIDE */}
{/* HEADING TEXT */}

Create Account

Sign up for a new account

{/* FORM */}
{/* FULL NAME */}
setFormData({ ...formData, fullName: e.target.value })} className="input" placeholder="John Doe" />
{/* EMAIL INPUT */}
setFormData({ ...formData, email: e.target.value })} className="input" placeholder="johndoe@gmail.com" />
{/* PASSWORD INPUT */}
setFormData({ ...formData, password: e.target.value })} className="input" placeholder="Enter your password" />
{/* SUBMIT BUTTON */}
Already have an account? Login
{/* FORM ILLUSTRATION - RIGHT SIDE */}
People using mobile devices

Start Your Journey Today

Free Easy Setup Private
); } export default SignUpPage; ================================================ FILE: frontend/src/store/useAuthStore.js ================================================ import { create } from "zustand"; import { axiosInstance } from "../lib/axios"; import toast from "react-hot-toast"; import { io } from "socket.io-client"; const BASE_URL = import.meta.env.MODE === "development" ? "http://localhost:3000" : "/"; export const useAuthStore = create((set, get) => ({ authUser: null, isCheckingAuth: true, isSigningUp: false, isLoggingIn: false, socket: null, onlineUsers: [], checkAuth: async () => { try { const res = await axiosInstance.get("/auth/check"); set({ authUser: res.data }); get().connectSocket(); } catch (error) { console.log("Error in authCheck:", error); set({ authUser: null }); } finally { set({ isCheckingAuth: false }); } }, signup: async (data) => { set({ isSigningUp: true }); try { const res = await axiosInstance.post("/auth/signup", data); set({ authUser: res.data }); toast.success("Account created successfully!"); get().connectSocket(); } catch (error) { toast.error(error.response.data.message); } finally { set({ isSigningUp: false }); } }, login: async (data) => { set({ isLoggingIn: true }); try { const res = await axiosInstance.post("/auth/login", data); set({ authUser: res.data }); toast.success("Logged in successfully"); get().connectSocket(); } catch (error) { toast.error(error.response.data.message); } finally { set({ isLoggingIn: false }); } }, logout: async () => { try { await axiosInstance.post("/auth/logout"); set({ authUser: null }); toast.success("Logged out successfully"); get().disconnectSocket(); } catch (error) { toast.error("Error logging out"); console.log("Logout error:", error); } }, updateProfile: async (data) => { try { const res = await axiosInstance.put("/auth/update-profile", data); set({ authUser: res.data }); toast.success("Profile updated successfully"); } catch (error) { console.log("Error in update profile:", error); toast.error(error.response.data.message); } }, connectSocket: () => { const { authUser } = get(); if (!authUser || get().socket?.connected) return; const socket = io(BASE_URL, { withCredentials: true, // this ensures cookies are sent with the connection }); socket.connect(); set({ socket }); // listen for online users event socket.on("getOnlineUsers", (userIds) => { set({ onlineUsers: userIds }); }); }, disconnectSocket: () => { if (get().socket?.connected) get().socket.disconnect(); }, })); ================================================ FILE: frontend/src/store/useChatStore.js ================================================ import { create } from "zustand"; import { axiosInstance } from "../lib/axios"; import toast from "react-hot-toast"; import { useAuthStore } from "./useAuthStore"; export const useChatStore = create((set, get) => ({ allContacts: [], chats: [], messages: [], activeTab: "chats", selectedUser: null, isUsersLoading: false, isMessagesLoading: false, isSoundEnabled: JSON.parse(localStorage.getItem("isSoundEnabled")) === true, toggleSound: () => { localStorage.setItem("isSoundEnabled", !get().isSoundEnabled); set({ isSoundEnabled: !get().isSoundEnabled }); }, setActiveTab: (tab) => set({ activeTab: tab }), setSelectedUser: (selectedUser) => set({ selectedUser }), getAllContacts: async () => { set({ isUsersLoading: true }); try { const res = await axiosInstance.get("/messages/contacts"); set({ allContacts: res.data }); } catch (error) { toast.error(error.response.data.message); } finally { set({ isUsersLoading: false }); } }, getMyChatPartners: async () => { set({ isUsersLoading: true }); try { const res = await axiosInstance.get("/messages/chats"); set({ chats: res.data }); } catch (error) { toast.error(error.response.data.message); } finally { set({ isUsersLoading: false }); } }, getMessagesByUserId: async (userId) => { set({ isMessagesLoading: true }); try { const res = await axiosInstance.get(`/messages/${userId}`); set({ messages: res.data }); } catch (error) { toast.error(error.response?.data?.message || "Something went wrong"); } finally { set({ isMessagesLoading: false }); } }, sendMessage: async (messageData) => { const { selectedUser, messages } = get(); const { authUser } = useAuthStore.getState(); const tempId = `temp-${Date.now()}`; const optimisticMessage = { _id: tempId, senderId: authUser._id, receiverId: selectedUser._id, text: messageData.text, image: messageData.image, createdAt: new Date().toISOString(), isOptimistic: true, // flag to identify optimistic messages (optional) }; // immidetaly update the ui by adding the message set({ messages: [...messages, optimisticMessage] }); try { const res = await axiosInstance.post(`/messages/send/${selectedUser._id}`, messageData); set({ messages: messages.concat(res.data) }); } catch (error) { // remove optimistic message on failure set({ messages: messages }); toast.error(error.response?.data?.message || "Something went wrong"); } }, subscribeToMessages: () => { const { selectedUser, isSoundEnabled } = get(); if (!selectedUser) return; const socket = useAuthStore.getState().socket; socket.on("newMessage", (newMessage) => { const isMessageSentFromSelectedUser = newMessage.senderId === selectedUser._id; if (!isMessageSentFromSelectedUser) return; const currentMessages = get().messages; set({ messages: [...currentMessages, newMessage] }); if (isSoundEnabled) { const notificationSound = new Audio("/sounds/notification.mp3"); notificationSound.currentTime = 0; // reset to start notificationSound.play().catch((e) => console.log("Audio play failed:", e)); } }); }, unsubscribeFromMessages: () => { const socket = useAuthStore.getState().socket; socket.off("newMessage"); }, })); ================================================ FILE: frontend/tailwind.config.js ================================================ import daisyui from "daisyui"; /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { animation: { border: "border 4s linear infinite", }, keyframes: { border: { to: { "--border-angle": "360deg" }, }, }, }, }, plugins: [daisyui], }; ================================================ FILE: frontend/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: package.json ================================================ { "name": "chatify-app", "version": "1.0.0", "main": "index.js", "engines": { "node": ">=20.0.0" }, "scripts": { "build": "npm install --prefix backend && npm install --prefix frontend && npm run build --prefix frontend", "start": "npm run start --prefix backend" }, "keywords": [], "author": "", "license": "ISC", "description": "" }