restored everything after nuking the repo
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
uploads/*
|
||||
@@ -0,0 +1,3 @@
|
||||
PORT=5001
|
||||
JWT_SECRET="shhhkeepitasecret"
|
||||
MONGODB_URI="mongodb://localhost:27017/chat-app"
|
||||
Generated
+1762
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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?
|
||||
@@ -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_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -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>
|
||||
Generated
+4019
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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 |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: all;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export function formatMessageTime(date) {
|
||||
return new Date(date).toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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'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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
},
|
||||
}));
|
||||
@@ -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 }),
|
||||
}));
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
@@ -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()],
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user