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 ✨

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
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:
Set up your profile picture
Find and add your contacts
Start a conversation
Share photos, videos, and more
If you need any help or have questions, we're always here to assist you.
Happy messaging!
Best regards, The Messenger Team
`;
}
================================================
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 (
setActiveTab("chats")}
className={`tab ${
activeTab === "chats" ? "bg-cyan-500/20 text-cyan-400" : "text-slate-400"
}`}
>
Chats
setActiveTab("contacts")}
className={`tab ${
activeTab === "contacts" ? "bg-cyan-500/20 text-cyan-400" : "text-slate-400"
}`}
>
Contacts
);
}
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 && (
)}
{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}
{isOnline ? "Online" : "Offline"}
setSelectedUser(null)}>
);
}
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) => (
))}
>
);
}
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)}
>
))}
>
);
}
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 (
);
}
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!
👋 Say Hello
🤝 How are you?
📅 Meet up soon?
);
};
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
setActiveTab("contacts")}
className="px-4 py-2 text-sm text-cyan-400 bg-cyan-500/10 rounded-lg hover:bg-cyan-500/20 transition-colors"
>
Find contacts
);
}
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 */}
{
// play click sound before toggling
mouseClickSound.currentTime = 0; // reset to start
mouseClickSound.play().catch((error) => console.log("Audio play failed:", error));
toggleSound();
}}
>
{isSoundEnabled ? (
) : (
)}
);
}
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 */}
Don't have an account? Sign Up
{/* FORM ILLUSTRATION - RIGHT SIDE */}
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 */}
Already have an account? Login
{/* FORM ILLUSTRATION - RIGHT SIDE */}
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": ""
}