restored everything after nuking the repo

This commit is contained in:
QkoSad
2025-07-20 22:22:47 +03:00
commit 8ff6e07fba
49 changed files with 7825 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
uploads/*
+3
View File
@@ -0,0 +1,3 @@
PORT=5001
JWT_SECRET="shhhkeepitasecret"
MONGODB_URI="mongodb://localhost:27017/chat-app"
+1762
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"keywords": [],
"author": "",
"type": "module",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.8.1",
"multer": "^2.0.2",
"socket.io": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.1.7"
}
}
+121
View File
@@ -0,0 +1,121 @@
import { generateToken } from "../lib/utils.js";
import User from "../models/user.model.js";
import bcrypt from "bcryptjs";
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" });
}
const user = await User.findOne({ email });
if (user) return res.status(400).json({ message: "Email already exists" });
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const newUser = new User({
fullName,
email,
password: hashedPassword,
});
if (newUser) {
// generate jwt token here
generateToken(newUser._id, res);
await newUser.save();
res.status(201).json({
_id: newUser._id,
fullName: newUser.fullName,
email: newUser.email,
profilePic: newUser.profilePic,
});
} else {
res.status(400).json({ message: "Invalid user data" });
}
} catch (error) {
console.log("Error in signup controller", error.message);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const login = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: "Invalid credentials" });
}
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.log("Error in login controller", error.message);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const logout = (req, res) => {
try {
res.cookie("jwt", "", { maxAge: 0 });
res.status(200).json({ message: "Logged out successfully" });
} catch (error) {
console.log("Error in logout controller", error.message);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const updateProfile = async (req, res) => {
try {
const profilePic = req.file;
const userId = req.user._id;
if (!profilePic) {
return res.status(400).json({ message: "Profile pic is required" });
}
console.log(profilePic);
const updatedUser = await User.findByIdAndUpdate(
userId,
{
profilePic: `http://localhost:${process.env.PORT}/` + profilePic.path,
},
{ new: true },
);
res.status(200).json(updatedUser);
} catch (error) {
console.log("error in update profile:", error);
res.status(500).json({ message: "Internal server error" });
}
};
export const checkAuth = (req, res) => {
try {
res.status(200).json(req.user);
} catch (error) {
console.log("Error in checkAuth controller", error.message);
res.status(500).json({ message: "Internal Server Error" });
}
};
@@ -0,0 +1,69 @@
import User from "../models/user.model.js";
import Message from "../models/message.model.js";
import { getReceiverSocketId, io } from "../lib/socket.js";
export const getUsersForSidebar = 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.error("Error in getUsersForSidebar: ", error.message);
res.status(500).json({ error: "Internal server error" });
}
};
export const getMessages = async (req, res) => {
try {
const { id: userToChatId } = req.params;
const myId = req.user._id;
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;
let imageUrl;
if (image) {
//TODO image
}
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" });
}
};
+43
View File
@@ -0,0 +1,43 @@
import express from "express";
import dotenv from "dotenv";
import cookieParser from "cookie-parser";
import cors from "cors";
import path from "path";
import { connectDB } from "./lib/db.js";
import authRoutes from "./routes/auth.route.js";
import messageRoutes from "./routes/message.route.js";
import { app, server } from "./lib/socket.js";
dotenv.config();
const PORT = process.env.PORT;
const __dirname = path.resolve();
console.log(path.join(__dirname, "uploads"));
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
origin: "http://localhost:5173",
credentials: true,
}),
);
app.use("/api/auth", authRoutes);
app.use("/api/messages", messageRoutes);
if (process.env.NODE_ENV === "production") {
app.use(express.static(path.join(__dirname, "../frontend/dist")));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "../frontend", "dist", "index.html"));
});
}
server.listen(PORT, () => {
console.log("server is running on PORT:" + PORT);
connectDB();
});
+10
View File
@@ -0,0 +1,10 @@
import mongoose from "mongoose";
export const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI);
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (error) {
console.log("MongoDB connection error:", error);
}
};
+15
View File
@@ -0,0 +1,15 @@
import multer from "multer";
import path from "path";
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/"); // make sure this folder exists
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + "-" + uniqueSuffix + ext);
},
});
export const upload = multer({ storage: storage });
+37
View File
@@ -0,0 +1,37 @@
import { Server } from "socket.io";
import http from "http";
import express from "express";
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: ["http://localhost:5173"],
},
});
export function getReceiverSocketId(userId) {
return userSocketMap[userId];
}
// used to store online users
const userSocketMap = {}; // {userId: socketId}
io.on("connection", (socket) => {
console.log("A user connected", socket.id);
const userId = socket.handshake.query.userId;
if (userId) userSocketMap[userId] = socket.id;
// io.emit() is used to send events to all the connected clients
io.emit("getOnlineUsers", Object.keys(userSocketMap));
socket.on("disconnect", () => {
console.log("A user disconnected", socket.id);
delete userSocketMap[userId];
io.emit("getOnlineUsers", Object.keys(userSocketMap));
});
});
export { io, app, server };
+16
View File
@@ -0,0 +1,16 @@
import jwt from "jsonwebtoken";
export const generateToken = (userId, res) => {
const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
res.cookie("jwt", token, {
maxAge: 7 * 24 * 60 * 60 * 1000, // MS
httpOnly: true, // prevent XSS attacks cross-site scripting attacks
sameSite: "strict", // CSRF attacks cross-site request forgery attacks
secure: process.env.NODE_ENV !== "development",
});
return token;
};
+31
View File
@@ -0,0 +1,31 @@
import jwt from "jsonwebtoken";
import User from "../models/user.model.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, process.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.message);
res.status(500).json({ message: "Internal server error" });
}
};
+27
View File
@@ -0,0 +1,27 @@
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,
},
image: {
type: String,
},
},
{ timestamps: true }
);
const Message = mongoose.model("Message", messageSchema);
export default Message;
+29
View File
@@ -0,0 +1,29 @@
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 }
);
const User = mongoose.model("User", userSchema);
export default User;
+27
View File
@@ -0,0 +1,27 @@
import express from "express";
import {
checkAuth,
login,
logout,
signup,
updateProfile,
} from "../controllers/auth.controller.js";
import { protectRoute } from "../middleware/auth.middleware.js";
import { upload } from "../lib/fileStorage.js";
const router = express.Router();
router.post("/signup", signup);
router.post("/login", login);
router.post("/logout", logout);
router.put(
"/update-profile",
protectRoute,
upload.single("image"),
updateProfile,
);
router.get("/check", protectRoute, checkAuth);
export default router;
+12
View File
@@ -0,0 +1,12 @@
import express from "express";
import { protectRoute } from "../middleware/auth.middleware.js";
import { getMessages, getUsersForSidebar, sendMessage } from "../controllers/message.controller.js";
const router = express.Router();
router.get("/users", protectRoute, getUsersForSidebar);
router.get("/:id", protectRoute, getMessages);
router.post("/send/:id", protectRoute, sendMessage);
export default router;
+115
View File
@@ -0,0 +1,115 @@
import { config } from "dotenv";
import { connectDB } from "../lib/db.js";
import User from "../models/user.model.js";
config();
const seedUsers = [
// Female Users
{
email: "emma.thompson@example.com",
fullName: "Emma Thompson",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/1.jpg",
},
{
email: "olivia.miller@example.com",
fullName: "Olivia Miller",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/2.jpg",
},
{
email: "sophia.davis@example.com",
fullName: "Sophia Davis",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/3.jpg",
},
{
email: "ava.wilson@example.com",
fullName: "Ava Wilson",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/4.jpg",
},
{
email: "isabella.brown@example.com",
fullName: "Isabella Brown",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/5.jpg",
},
{
email: "mia.johnson@example.com",
fullName: "Mia Johnson",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/6.jpg",
},
{
email: "charlotte.williams@example.com",
fullName: "Charlotte Williams",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/7.jpg",
},
{
email: "amelia.garcia@example.com",
fullName: "Amelia Garcia",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/women/8.jpg",
},
// Male Users
{
email: "james.anderson@example.com",
fullName: "James Anderson",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/1.jpg",
},
{
email: "william.clark@example.com",
fullName: "William Clark",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/2.jpg",
},
{
email: "benjamin.taylor@example.com",
fullName: "Benjamin Taylor",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/3.jpg",
},
{
email: "lucas.moore@example.com",
fullName: "Lucas Moore",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/4.jpg",
},
{
email: "henry.jackson@example.com",
fullName: "Henry Jackson",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/5.jpg",
},
{
email: "alexander.martin@example.com",
fullName: "Alexander Martin",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/6.jpg",
},
{
email: "daniel.rodriguez@example.com",
fullName: "Daniel Rodriguez",
password: "123456",
profilePic: "https://randomuser.me/api/portraits/men/7.jpg",
},
];
const seedDatabase = async () => {
try {
await connectDB();
await User.insertMany(seedUsers);
console.log("Database seeded successfully");
} catch (error) {
console.error("Error seeding database:", error);
}
};
// Call the function
seedDatabase();
+24
View File
@@ -0,0 +1,24 @@
# 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?
+29
View File
@@ -0,0 +1,29 @@
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_]' }],
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+4019
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.10.0",
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.5.2",
"react-router-dom": "^7.7.0",
"socket.io-client": "^4.8.1",
"zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@tailwindcss/vite": "^4.1.11",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"daisyui": "^5.0.46",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tailwindcss": "^4.1.11",
"vite": "^7.0.4"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+60
View File
@@ -0,0 +1,60 @@
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";
import SignUpPage from "./pages/SignUpPage";
import LoginPage from "./pages/LoginPage";
import SettingsPage from "./pages/SettingsPage";
import ProfilePage from "./pages/ProfilePage";
import { Routes, Route, Navigate } from "react-router-dom";
import { useAuthStore } from "./store/useAuthStore";
import { useThemeStore } from "./store/useThemeStore";
import { useEffect } from "react";
import { Loader } from "lucide-react";
import { Toaster } from "react-hot-toast";
const App = () => {
const { authUser, checkAuth, isCheckingAuth, onlineUsers } = useAuthStore();
const { theme } = useThemeStore();
useEffect(() => {
checkAuth();
}, [checkAuth]);
if (isCheckingAuth && !authUser)
return (
<div className="flex items-center justify-center h-screen">
<Loader className="size-10 animate-spin" />
</div>
);
return (
<div data-theme={theme}>
<Navbar />
<Routes>
<Route
path="/"
element={authUser ? <HomePage /> : <Navigate to="/login" />}
/>
<Route
path="/signup"
element={!authUser ? <SignUpPage /> : <Navigate to="/" />}
/>
<Route
path="/login"
element={!authUser ? <LoginPage /> : <Navigate to="/" />}
/>
<Route path="/settings" element={<SettingsPage />} />
<Route
path="/profile"
element={authUser ? <ProfilePage /> : <Navigate to="/login" />}
/>
</Routes>
<Toaster />
</div>
);
};
export default App;
@@ -0,0 +1,22 @@
const AuthImagePattern = ({ title, subtitle }) => {
return (
<div className="hidden lg:flex items-center justify-center bg-base-200 p-12">
<div className="max-w-md text-center">
<div className="grid grid-cols-3 gap-3 mb-8">
{[...Array(9)].map((_, i) => (
<div
key={i}
className={`aspect-square rounded-2xl bg-primary/10 ${
i % 2 === 0 ? "animate-pulse" : ""
}`}
/>
))}
</div>
<h2 className="text-2xl font-bold mb-4">{title}</h2>
<p className="text-base-content/60">{subtitle}</p>
</div>
</div>
);
};
export default AuthImagePattern;
+92
View File
@@ -0,0 +1,92 @@
import { useChatStore } from "../store/useChatStore";
import { useEffect, useRef } from "react";
import ChatHeader from "./ChatHeader";
import MessageInput from "./MessageInput";
import MessageSkeleton from "./skeletons/MessageSkeleton";
import { useAuthStore } from "../store/useAuthStore";
import { formatMessageTime } from "../lib/utils";
const ChatContainer = () => {
const {
messages,
getMessages,
isMessagesLoading,
selectedUser,
subscribeToMessages,
unsubscribeFromMessages,
} = useChatStore();
const { authUser } = useAuthStore();
const messageEndRef = useRef(null);
useEffect(() => {
getMessages(selectedUser._id);
subscribeToMessages();
return () => unsubscribeFromMessages();
}, [selectedUser._id, getMessages, subscribeToMessages, unsubscribeFromMessages]);
useEffect(() => {
if (messageEndRef.current && messages) {
messageEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [messages]);
if (isMessagesLoading) {
return (
<div className="flex-1 flex flex-col overflow-auto">
<ChatHeader />
<MessageSkeleton />
<MessageInput />
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-auto">
<ChatHeader />
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message._id}
className={`chat ${message.senderId === authUser._id ? "chat-end" : "chat-start"}`}
ref={messageEndRef}
>
<div className=" chat-image avatar">
<div className="size-10 rounded-full border">
<img
src={
message.senderId === authUser._id
? authUser.profilePic || "/avatar.png"
: selectedUser.profilePic || "/avatar.png"
}
alt="profile pic"
/>
</div>
</div>
<div className="chat-header mb-1">
<time className="text-xs opacity-50 ml-1">
{formatMessageTime(message.createdAt)}
</time>
</div>
<div className="chat-bubble flex flex-col">
{message.image && (
<img
src={message.image}
alt="Attachment"
className="sm:max-w-[200px] rounded-md mb-2"
/>
)}
{message.text && <p>{message.text}</p>}
</div>
</div>
))}
</div>
<MessageInput />
</div>
);
};
export default ChatContainer;
+37
View File
@@ -0,0 +1,37 @@
import { X } from "lucide-react";
import { useAuthStore } from "../store/useAuthStore";
import { useChatStore } from "../store/useChatStore";
const ChatHeader = () => {
const { selectedUser, setSelectedUser } = useChatStore();
const { onlineUsers } = useAuthStore();
return (
<div className="p-2.5 border-b border-base-300">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Avatar */}
<div className="avatar">
<div className="size-10 rounded-full relative">
<img src={selectedUser.profilePic || "/avatar.png"} alt={selectedUser.fullName} />
</div>
</div>
{/* User info */}
<div>
<h3 className="font-medium">{selectedUser.fullName}</h3>
<p className="text-sm text-base-content/70">
{onlineUsers.includes(selectedUser._id) ? "Online" : "Offline"}
</p>
</div>
</div>
{/* Close button */}
<button onClick={() => setSelectedUser(null)}>
<X />
</button>
</div>
</div>
);
};
export default ChatHeader;
+109
View File
@@ -0,0 +1,109 @@
import { useRef, useState } from "react";
import { useChatStore } from "../store/useChatStore";
import { Image, Send, X } from "lucide-react";
import toast from "react-hot-toast";
const MessageInput = () => {
const [text, setText] = useState("");
const [imagePreview, setImagePreview] = useState(null);
const fileInputRef = useRef(null);
const { sendMessage } = useChatStore();
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 = "";
};
const handleSendMessage = async (e) => {
e.preventDefault();
if (!text.trim() && !imagePreview) return;
try {
await sendMessage({
text: text.trim(),
image: imagePreview,
});
// Clear form
setText("");
setImagePreview(null);
if (fileInputRef.current) fileInputRef.current.value = "";
} catch (error) {
console.error("Failed to send message:", error);
}
};
return (
<div className="p-4 w-full">
{imagePreview && (
<div className="mb-3 flex items-center gap-2">
<div className="relative">
<img
src={imagePreview}
alt="Preview"
className="w-20 h-20 object-cover rounded-lg border border-zinc-700"
/>
<button
onClick={removeImage}
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-base-300
flex items-center justify-center"
type="button"
>
<X className="size-3" />
</button>
</div>
</div>
)}
<form onSubmit={handleSendMessage} className="flex items-center gap-2">
<div className="flex-1 flex gap-2">
<input
type="text"
className="w-full input input-bordered rounded-lg input-sm sm:input-md"
placeholder="Type a message..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
<input
type="file"
accept="image/*"
className="hidden"
ref={fileInputRef}
onChange={handleImageChange}
/>
<button
type="button"
className={`hidden sm:flex btn btn-circle
${imagePreview ? "text-emerald-500" : "text-zinc-400"}`}
onClick={() => fileInputRef.current?.click()}
>
<Image size={20} />
</button>
</div>
<button
type="submit"
className="btn btn-sm btn-circle"
disabled={!text.trim() && !imagePreview}
>
<Send size={22} />
</button>
</form>
</div>
);
};
export default MessageInput;
+55
View File
@@ -0,0 +1,55 @@
import { Link } from "react-router-dom";
import { useAuthStore } from "../store/useAuthStore";
import { LogOut, MessageSquare, Settings, User } from "lucide-react";
const Navbar = () => {
const { logout, authUser } = useAuthStore();
return (
<header
className="bg-base-100 border-b border-base-300 fixed w-full top-0 z-40
backdrop-blur-lg bg-base-100/80"
>
<div className="container mx-auto px-4 h-16">
<div className="flex items-center justify-between h-full">
<div className="flex items-center gap-8">
<Link to="/" className="flex items-center gap-2.5 hover:opacity-80 transition-all">
<div className="size-9 rounded-lg bg-primary/10 flex items-center justify-center">
<MessageSquare className="w-5 h-5 text-primary" />
</div>
<h1 className="text-lg font-bold">Chatty</h1>
</Link>
</div>
<div className="flex items-center gap-2">
<Link
to={"/settings"}
className={`
btn btn-sm gap-2 transition-colors
`}
>
<Settings className="w-4 h-4" />
<span className="hidden sm:inline">Settings</span>
</Link>
{authUser && (
<>
<Link to={"/profile"} className={`btn btn-sm gap-2`}>
<User className="size-5" />
<span className="hidden sm:inline">Profile</span>
</Link>
<button className="flex gap-2 items-center" onClick={logout}>
<LogOut className="size-5" />
<span className="hidden sm:inline">Logout</span>
</button>
</>
)}
</div>
</div>
</div>
</header>
);
};
export default Navbar;
@@ -0,0 +1,29 @@
import { MessageSquare } from "lucide-react";
const NoChatSelected = () => {
return (
<div className="w-full flex flex-1 flex-col items-center justify-center p-16 bg-base-100/50">
<div className="max-w-md text-center space-y-6">
{/* Icon Display */}
<div className="flex justify-center gap-4 mb-4">
<div className="relative">
<div
className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center
justify-center animate-bounce"
>
<MessageSquare className="w-8 h-8 text-primary " />
</div>
</div>
</div>
{/* Welcome Text */}
<h2 className="text-2xl font-bold">Welcome to Chatty!</h2>
<p className="text-base-content/60">
Select a conversation from the sidebar to start chatting
</p>
</div>
</div>
);
};
export default NoChatSelected;
+87
View File
@@ -0,0 +1,87 @@
import { useEffect, useState } from "react";
import { useChatStore } from "../store/useChatStore";
import { useAuthStore } from "../store/useAuthStore";
import SidebarSkeleton from "./skeletons/SidebarSkeleton";
import { Users } from "lucide-react";
const Sidebar = () => {
const { getUsers, users, selectedUser, setSelectedUser, isUsersLoading } = useChatStore();
const { onlineUsers } = useAuthStore();
const [showOnlineOnly, setShowOnlineOnly] = useState(false);
useEffect(() => {
getUsers();
}, [getUsers]);
const filteredUsers = showOnlineOnly
? users.filter((user) => onlineUsers.includes(user._id))
: users;
if (isUsersLoading) return <SidebarSkeleton />;
return (
<aside className="h-full w-20 lg:w-72 border-r border-base-300 flex flex-col transition-all duration-200">
<div className="border-b border-base-300 w-full p-5">
<div className="flex items-center gap-2">
<Users className="size-6" />
<span className="font-medium hidden lg:block">Contacts</span>
</div>
{/* TODO: Online filter toggle */}
<div className="mt-3 hidden lg:flex items-center gap-2">
<label className="cursor-pointer flex items-center gap-2">
<input
type="checkbox"
checked={showOnlineOnly}
onChange={(e) => setShowOnlineOnly(e.target.checked)}
className="checkbox checkbox-sm"
/>
<span className="text-sm">Show online only</span>
</label>
<span className="text-xs text-zinc-500">({onlineUsers.length - 1} online)</span>
</div>
</div>
<div className="overflow-y-auto w-full py-3">
{filteredUsers.map((user) => (
<button
key={user._id}
onClick={() => setSelectedUser(user)}
className={`
w-full p-3 flex items-center gap-3
hover:bg-base-300 transition-colors
${selectedUser?._id === user._id ? "bg-base-300 ring-1 ring-base-300" : ""}
`}
>
<div className="relative mx-auto lg:mx-0">
<img
src={user.profilePic || "/avatar.png"}
alt={user.name}
className="size-12 object-cover rounded-full"
/>
{onlineUsers.includes(user._id) && (
<span
className="absolute bottom-0 right-0 size-3 bg-green-500
rounded-full ring-2 ring-zinc-900"
/>
)}
</div>
{/* User info - only visible on larger screens */}
<div className="hidden lg:block text-left min-w-0">
<div className="font-medium truncate">{user.fullName}</div>
<div className="text-sm text-zinc-400">
{onlineUsers.includes(user._id) ? "Online" : "Offline"}
</div>
</div>
</button>
))}
{filteredUsers.length === 0 && (
<div className="text-center text-zinc-500 py-4">No online users</div>
)}
</div>
</aside>
);
};
export default Sidebar;
@@ -0,0 +1,28 @@
const MessageSkeleton = () => {
// Create an array of 6 items for skeleton messages
const skeletonMessages = Array(6).fill(null);
return (
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{skeletonMessages.map((_, idx) => (
<div key={idx} className={`chat ${idx % 2 === 0 ? "chat-start" : "chat-end"}`}>
<div className="chat-image avatar">
<div className="size-10 rounded-full">
<div className="skeleton w-full h-full rounded-full" />
</div>
</div>
<div className="chat-header mb-1">
<div className="skeleton h-4 w-16" />
</div>
<div className="chat-bubble bg-transparent p-0">
<div className="skeleton h-16 w-[200px]" />
</div>
</div>
))}
</div>
);
};
export default MessageSkeleton;
@@ -0,0 +1,41 @@
import { Users } from "lucide-react";
const SidebarSkeleton = () => {
// Create 8 skeleton items
const skeletonContacts = Array(8).fill(null);
return (
<aside
className="h-full w-20 lg:w-72 border-r border-base-300
flex flex-col transition-all duration-200"
>
{/* Header */}
<div className="border-b border-base-300 w-full p-5">
<div className="flex items-center gap-2">
<Users className="w-6 h-6" />
<span className="font-medium hidden lg:block">Contacts</span>
</div>
</div>
{/* Skeleton Contacts */}
<div className="overflow-y-auto w-full py-3">
{skeletonContacts.map((_, idx) => (
<div key={idx} className="w-full p-3 flex items-center gap-3">
{/* Avatar skeleton */}
<div className="relative mx-auto lg:mx-0">
<div className="skeleton size-12 rounded-full" />
</div>
{/* User info skeleton - only visible on larger screens */}
<div className="hidden lg:block text-left min-w-0 flex-1">
<div className="skeleton h-4 w-32 mb-2" />
<div className="skeleton h-3 w-16" />
</div>
</div>
))}
</div>
</aside>
);
};
export default SidebarSkeleton;
+34
View File
@@ -0,0 +1,34 @@
export const THEMES = [
"light",
"dark",
"cupcake",
"bumblebee",
"emerald",
"corporate",
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"garden",
"forest",
"aqua",
"lofi",
"pastel",
"fantasy",
"wireframe",
"black",
"luxury",
"dracula",
"cmyk",
"autumn",
"business",
"acid",
"lemonade",
"night",
"coffee",
"winter",
"dim",
"nord",
"sunset",
];
+8
View File
@@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin "daisyui" {
themes: all;
}
+6
View File
@@ -0,0 +1,6 @@
import axios from "axios";
export const axiosInstance = axios.create({
baseURL: import.meta.env.MODE === "development" ? "http://localhost:5001/api" : "/api",
withCredentials: true,
});
+7
View File
@@ -0,0 +1,7 @@
export function formatMessageTime(date) {
return new Date(date).toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
+14
View File
@@ -0,0 +1,14 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
+24
View File
@@ -0,0 +1,24 @@
import { useChatStore } from "../store/useChatStore";
import Sidebar from "../components/Sidebar";
import NoChatSelected from "../components/NoChatSelected";
import ChatContainer from "../components/ChatContainer";
const HomePage = () => {
const { selectedUser } = useChatStore();
return (
<div className="h-screen bg-base-200">
<div className="flex items-center justify-center pt-20 px-4">
<div className="bg-base-100 rounded-lg shadow-cl w-full max-w-6xl h-[calc(100vh-8rem)]">
<div className="flex h-full rounded-lg overflow-hidden">
<Sidebar />
{!selectedUser ? <NoChatSelected /> : <ChatContainer />}
</div>
</div>
</div>
</div>
);
};
export default HomePage;
+119
View File
@@ -0,0 +1,119 @@
import { useState } from "react";
import { useAuthStore } from "../store/useAuthStore";
import AuthImagePattern from "../components/AuthImagePattern";
import { Link } from "react-router-dom";
import { Eye, EyeOff, Loader2, Lock, Mail, MessageSquare } from "lucide-react";
const LoginPage = () => {
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: "",
password: "",
});
const { login, isLoggingIn } = useAuthStore();
const handleSubmit = async (e) => {
e.preventDefault();
login(formData);
};
return (
<div className="h-screen grid lg:grid-cols-2">
{/* Left Side - Form */}
<div className="flex flex-col justify-center items-center p-6 sm:p-12">
<div className="w-full max-w-md space-y-8">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex flex-col items-center gap-2 group">
<div
className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center group-hover:bg-primary/20
transition-colors"
>
<MessageSquare className="w-6 h-6 text-primary" />
</div>
<h1 className="text-2xl font-bold mt-2">Welcome Back</h1>
<p className="text-base-content/60">Sign in to your account</p>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Email</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-base-content/40" />
</div>
<input
type="email"
className={`input input-bordered w-full pl-10`}
placeholder="you@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Password</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-base-content/40" />
</div>
<input
type={showPassword ? "text" : "password"}
className={`input input-bordered w-full pl-10`}
placeholder="••••••••"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-base-content/40" />
) : (
<Eye className="h-5 w-5 text-base-content/40" />
)}
</button>
</div>
</div>
<button type="submit" className="btn btn-primary w-full" disabled={isLoggingIn}>
{isLoggingIn ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Loading...
</>
) : (
"Sign in"
)}
</button>
</form>
<div className="text-center">
<p className="text-base-content/60">
Don&apos;t have an account?{" "}
<Link to="/signup" className="link link-primary">
Create account
</Link>
</p>
</div>
</div>
</div>
{/* Right Side - Image/Pattern */}
<AuthImagePattern
title={"Welcome back!"}
subtitle={"Sign in to continue your conversations and catch up with your messages."}
/>
</div>
);
};
export default LoginPage;
+101
View File
@@ -0,0 +1,101 @@
import { useState } from "react";
import { useAuthStore } from "../store/useAuthStore";
import { Camera, Mail, User } from "lucide-react";
const ProfilePage = () => {
const { authUser, isUpdatingProfile, updateProfile } = useAuthStore();
const handleImageUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
updateProfile(file);
};
return (
<div className="h-screen pt-20">
<div className="max-w-2xl mx-auto p-4 py-8">
<div className="bg-base-300 rounded-xl p-6 space-y-8">
<div className="text-center">
<h1 className="text-2xl font-semibold ">Profile</h1>
<p className="mt-2">Your profile information</p>
</div>
{/* avatar upload section */}
<div className="flex flex-col items-center gap-4">
<div className="relative">
<img
src={authUser.profilePic || "/avatar.png"}
alt="Profile"
className="size-32 rounded-full object-cover border-4 "
/>
<label
htmlFor="avatar-upload"
className={`
absolute bottom-0 right-0
bg-base-content hover:scale-105
p-2 rounded-full cursor-pointer
transition-all duration-200
${isUpdatingProfile ? "animate-pulse pointer-events-none" : ""}
`}
>
<Camera className="w-5 h-5 text-base-200" />
<input
type="file"
id="avatar-upload"
className="hidden"
accept="image/*"
onChange={handleImageUpload}
disabled={isUpdatingProfile}
/>
</label>
</div>
<p className="text-sm text-zinc-400">
{isUpdatingProfile
? "Uploading..."
: "Click the camera icon to update your photo"}
</p>
</div>
<div className="space-y-6">
<div className="space-y-1.5">
<div className="text-sm text-zinc-400 flex items-center gap-2">
<User className="w-4 h-4" />
Full Name
</div>
<p className="px-4 py-2.5 bg-base-200 rounded-lg border">
{authUser?.fullName}
</p>
</div>
<div className="space-y-1.5">
<div className="text-sm text-zinc-400 flex items-center gap-2">
<Mail className="w-4 h-4" />
Email Address
</div>
<p className="px-4 py-2.5 bg-base-200 rounded-lg border">
{authUser?.email}
</p>
</div>
</div>
<div className="mt-6 bg-base-300 rounded-xl p-6">
<h2 className="text-lg font-medium mb-4">Account Information</h2>
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between py-2 border-b border-zinc-700">
<span>Member Since</span>
<span>{authUser.createdAt?.split("T")[0]}</span>
</div>
<div className="flex items-center justify-between py-2">
<span>Account Status</span>
<span className="text-green-500">Active</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ProfilePage;
+116
View File
@@ -0,0 +1,116 @@
import { THEMES } from "../constants";
import { useThemeStore } from "../store/useThemeStore";
import { Send } from "lucide-react";
const PREVIEW_MESSAGES = [
{ id: 1, content: "Hey! How's it going?", isSent: false },
{ id: 2, content: "I'm doing great! Just working on some new features.", isSent: true },
];
const SettingsPage = () => {
const { theme, setTheme } = useThemeStore();
return (
<div className="h-screen container mx-auto px-4 pt-20 max-w-5xl">
<div className="space-y-6">
<div className="flex flex-col gap-1">
<h2 className="text-lg font-semibold">Theme</h2>
<p className="text-sm text-base-content/70">Choose a theme for your chat interface</p>
</div>
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
{THEMES.map((t) => (
<button
key={t}
className={`
group flex flex-col items-center gap-1.5 p-2 rounded-lg transition-colors
${theme === t ? "bg-base-200" : "hover:bg-base-200/50"}
`}
onClick={() => setTheme(t)}
>
<div className="relative h-8 w-full rounded-md overflow-hidden" data-theme={t}>
<div className="absolute inset-0 grid grid-cols-4 gap-px p-1">
<div className="rounded bg-primary"></div>
<div className="rounded bg-secondary"></div>
<div className="rounded bg-accent"></div>
<div className="rounded bg-neutral"></div>
</div>
</div>
<span className="text-[11px] font-medium truncate w-full text-center">
{t.charAt(0).toUpperCase() + t.slice(1)}
</span>
</button>
))}
</div>
{/* Preview Section */}
<h3 className="text-lg font-semibold mb-3">Preview</h3>
<div className="rounded-xl border border-base-300 overflow-hidden bg-base-100 shadow-lg">
<div className="p-4 bg-base-200">
<div className="max-w-lg mx-auto">
{/* Mock Chat UI */}
<div className="bg-base-100 rounded-xl shadow-sm overflow-hidden">
{/* Chat Header */}
<div className="px-4 py-3 border-b border-base-300 bg-base-100">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-primary-content font-medium">
J
</div>
<div>
<h3 className="font-medium text-sm">John Doe</h3>
<p className="text-xs text-base-content/70">Online</p>
</div>
</div>
</div>
{/* Chat Messages */}
<div className="p-4 space-y-4 min-h-[200px] max-h-[200px] overflow-y-auto bg-base-100">
{PREVIEW_MESSAGES.map((message) => (
<div
key={message.id}
className={`flex ${message.isSent ? "justify-end" : "justify-start"}`}
>
<div
className={`
max-w-[80%] rounded-xl p-3 shadow-sm
${message.isSent ? "bg-primary text-primary-content" : "bg-base-200"}
`}
>
<p className="text-sm">{message.content}</p>
<p
className={`
text-[10px] mt-1.5
${message.isSent ? "text-primary-content/70" : "text-base-content/70"}
`}
>
12:00 PM
</p>
</div>
</div>
))}
</div>
{/* Chat Input */}
<div className="p-4 border-t border-base-300 bg-base-100">
<div className="flex gap-2">
<input
type="text"
className="input input-bordered flex-1 text-sm h-10"
placeholder="Type a message..."
value="This is a preview"
readOnly
/>
<button className="btn btn-primary h-10 min-h-0">
<Send size={18} />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default SettingsPage;
+154
View File
@@ -0,0 +1,154 @@
import { useState } from "react";
import { useAuthStore } from "../store/useAuthStore";
import { Eye, EyeOff, Loader2, Lock, Mail, MessageSquare, User } from "lucide-react";
import { Link } from "react-router-dom";
import AuthImagePattern from "../components/AuthImagePattern";
import toast from "react-hot-toast";
const SignUpPage = () => {
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
fullName: "",
email: "",
password: "",
});
const { signup, isSigningUp } = useAuthStore();
const validateForm = () => {
if (!formData.fullName.trim()) return toast.error("Full name is required");
if (!formData.email.trim()) return toast.error("Email is required");
if (!/\S+@\S+\.\S+/.test(formData.email)) return toast.error("Invalid email format");
if (!formData.password) return toast.error("Password is required");
if (formData.password.length < 6) return toast.error("Password must be at least 6 characters");
return true;
};
const handleSubmit = (e) => {
e.preventDefault();
const success = validateForm();
if (success === true) signup(formData);
};
return (
<div className="min-h-screen grid lg:grid-cols-2">
{/* left side */}
<div className="flex flex-col justify-center items-center p-6 sm:p-12">
<div className="w-full max-w-md space-y-8">
{/* LOGO */}
<div className="text-center mb-8">
<div className="flex flex-col items-center gap-2 group">
<div
className="size-12 rounded-xl bg-primary/10 flex items-center justify-center
group-hover:bg-primary/20 transition-colors"
>
<MessageSquare className="size-6 text-primary" />
</div>
<h1 className="text-2xl font-bold mt-2">Create Account</h1>
<p className="text-base-content/60">Get started with your free account</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Full Name</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="size-5 text-base-content/40" />
</div>
<input
type="text"
className={`input input-bordered w-full pl-10`}
placeholder="John Doe"
value={formData.fullName}
onChange={(e) => setFormData({ ...formData, fullName: e.target.value })}
/>
</div>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Email</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="size-5 text-base-content/40" />
</div>
<input
type="email"
className={`input input-bordered w-full pl-10`}
placeholder="you@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Password</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="size-5 text-base-content/40" />
</div>
<input
type={showPassword ? "text" : "password"}
className={`input input-bordered w-full pl-10`}
placeholder="••••••••"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="size-5 text-base-content/40" />
) : (
<Eye className="size-5 text-base-content/40" />
)}
</button>
</div>
</div>
<button type="submit" className="btn btn-primary w-full" disabled={isSigningUp}>
{isSigningUp ? (
<>
<Loader2 className="size-5 animate-spin" />
Loading...
</>
) : (
"Create Account"
)}
</button>
</form>
<div className="text-center">
<p className="text-base-content/60">
Already have an account?{" "}
<Link to="/login" className="link link-primary">
Sign in
</Link>
</p>
</div>
</div>
</div>
{/* right side */}
<AuthImagePattern
title="Join our community"
subtitle="Connect with friends, share moments, and stay in touch with your loved ones."
/>
</div>
);
};
export default SignUpPage;
+114
View File
@@ -0,0 +1,114 @@
import { create } from "zustand";
import { axiosInstance } from "../lib/axios.js";
import toast from "react-hot-toast";
import { io } from "socket.io-client";
const BASE_URL =
import.meta.env.MODE === "development" ? "http://localhost:5001" : "/";
export const useAuthStore = create((set, get) => ({
authUser: null,
isSigningUp: false,
isLoggingIn: false,
isUpdatingProfile: false,
isCheckingAuth: true,
onlineUsers: [],
socket: null,
checkAuth: async () => {
try {
const res = await axiosInstance.get("/auth/check");
set({ authUser: res.data });
get().connectSocket();
} catch (error) {
console.log("Error in checkAuth:", 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.response.data.message);
}
},
updateProfile: async (data) => {
set({ isUpdatingProfile: true });
// try {
const formData = new FormData();
formData.append("image", data);
const res = await axiosInstance.put("/auth/update-profile", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
console.log(res.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);
// } finally {
set({ isUpdatingProfile: false });
// }
},
connectSocket: () => {
const { authUser } = get();
if (!authUser || get().socket?.connected) return;
const socket = io(BASE_URL, {
query: {
userId: authUser._id,
},
});
socket.connect();
set({ socket: socket });
socket.on("getOnlineUsers", (userIds) => {
set({ onlineUsers: userIds });
});
},
disconnectSocket: () => {
if (get().socket?.connected) get().socket.disconnect();
},
}));
+68
View File
@@ -0,0 +1,68 @@
import { create } from "zustand";
import toast from "react-hot-toast";
import { axiosInstance } from "../lib/axios";
import { useAuthStore } from "./useAuthStore";
export const useChatStore = create((set, get) => ({
messages: [],
users: [],
selectedUser: null,
isUsersLoading: false,
isMessagesLoading: false,
getUsers: async () => {
set({ isUsersLoading: true });
try {
const res = await axiosInstance.get("/messages/users");
set({ users: res.data });
} catch (error) {
toast.error(error.response.data.message);
} finally {
set({ isUsersLoading: false });
}
},
getMessages: 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);
} finally {
set({ isMessagesLoading: false });
}
},
sendMessage: async (messageData) => {
const { selectedUser, messages } = get();
try {
const res = await axiosInstance.post(`/messages/send/${selectedUser._id}`, messageData);
set({ messages: [...messages, res.data] });
} catch (error) {
toast.error(error.response.data.message);
}
},
subscribeToMessages: () => {
const { selectedUser } = get();
if (!selectedUser) return;
const socket = useAuthStore.getState().socket;
socket.on("newMessage", (newMessage) => {
const isMessageSentFromSelectedUser = newMessage.senderId === selectedUser._id;
if (!isMessageSentFromSelectedUser) return;
set({
messages: [...get().messages, newMessage],
});
});
},
unsubscribeFromMessages: () => {
const socket = useAuthStore.getState().socket;
socket.off("newMessage");
},
setSelectedUser: (selectedUser) => set({ selectedUser }),
}));
+9
View File
@@ -0,0 +1,9 @@
import { create } from "zustand";
export const useThemeStore = create((set) => ({
theme: localStorage.getItem("chat-theme") || "coffee",
setTheme: (theme) => {
localStorage.setItem("chat-theme", theme);
set({ theme });
},
}));
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});
+13
View File
@@ -0,0 +1,13 @@
{
"name": "chat-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"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"
}