diff --git a/backend/package.json b/backend/package.json
index 607e9e8..2498fb4 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -19,7 +19,8 @@
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.9.5",
"mongoose-aggregate-paginate-v2": "^1.1.3",
- "multer": "^1.4.5-lts.1"
+ "multer": "^1.4.5-lts.1",
+ "socket.io": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.1.9",
diff --git a/backend/src/app.js b/backend/src/app.js
index 0d68ea0..04fadb4 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -1,11 +1,12 @@
import express from "express";
import cors from "cors";
+import cookieParser from "cookie-parser";
const app = express();
app.use(
cors({
- origin: process.env.CORS_ORIGIN,
+ origin: "http://localhost:5173",
credentials: true,
})
);
@@ -13,4 +14,24 @@ app.use(
app.use(express.json({ limit: "16kb" }));
app.use(express.urlencoded({ extended: true, limit: "16kb" }));
+// Configuring express to mark public as static storage folder
+app.use(express.static("public"));
+
+// Cookie Parser configuration for tokens
+app.use(cookieParser());
+
+// TODO: Routes will go here ✔
+import userRouter from "./routes/user.routes.js";
+import assignmentRouter from "./routes/assignment.routes.js";
+import taskRouter from "./routes/task.routes.js";
+import messageRouter from "./routes/message.routes.js";
+import scheduleRouter from "./routes/schedule.routes.js";
+
+// Routes Declaration
+app.use("/users", userRouter);
+app.use("/assignments", assignmentRouter);
+app.use("/tasks", taskRouter);
+app.use("/messages", messageRouter);
+app.use("/schedules", scheduleRouter);
+
export { app };
diff --git a/backend/src/controllers/assignment.controllers.js b/backend/src/controllers/assignment.controllers.js
new file mode 100644
index 0000000..1f6d6cd
--- /dev/null
+++ b/backend/src/controllers/assignment.controllers.js
@@ -0,0 +1,151 @@
+import { Assignment } from "../models/assignment.models.js";
+import { ApiError } from "../utils/ApiError.js";
+import { ApiResponse } from "../utils/ApiResponse.js";
+import { asyncHandler } from "../utils/asyncHandler.js";
+import { uploadToCloud, cloud } from "../utils/cloudinary.js";
+import mongoose, { isValidObjectId } from "mongoose";
+
+const createAssignment = asyncHandler(async (req, res) => {
+ const { title, description, due_date } = req.body;
+
+ if ([title, due_date].some((field) => field?.trim() === "")) {
+ throw new ApiError(400, "Some fields are required fields");
+ }
+
+ const docs = req.files
+ ? req.files.map(async (file) => {
+ const localPath = file.path;
+ console.log(localPath);
+ const doc = await uploadToCloud(localPath);
+
+ return doc?.url;
+ })
+ : [];
+
+ const uploadedDocs = await Promise.all(docs);
+
+ const assignment = await Assignment.create({
+ title,
+ description,
+ docs: uploadedDocs,
+ due_date,
+ user: req.user._id,
+ });
+
+ const assignmentFromDB = await Assignment.findById(assignment._id);
+
+ if (!assignmentFromDB) {
+ throw new ApiError(500, "Something went wrong while creating Assignment");
+ }
+
+ return res
+ .status(201)
+ .json(new ApiResponse(200, assignmentFromDB, "Assignment Ban Gaya 🤩!"));
+});
+
+const getUserAssignments = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+
+ const assignments = await Assignment.find({ user: userId }).sort({
+ createdAt: -1,
+ });
+
+ // if (!assignments.length) {
+ // throw new ApiError(404, "No assignments found for this user...");
+ // }
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, assignments, "Assignments Fetched"));
+});
+
+const updateAssignment = asyncHandler(async (req, res) => {
+ const { assignmentId } = req.params;
+ const { title, description, due_date } = req.body;
+
+ // Validate ObjectId
+ if (!isValidObjectId(assignmentId)) {
+ throw new ApiError(400, "Invalid Assignment ID");
+ }
+
+ // const assignment = await Assignment.findById(assignmentId);
+ // if (!assignment) {
+ // throw new ApiError(404, "Assignment not found");
+ // }
+ // if (assignment.user.toString() !== req.user._id.toString()) {
+ // throw new ApiError(403, "Unauthorized to update this assignment");
+ // }
+
+ // Better approach (apparently😭)
+ const assignment = await Assignment.findOne({
+ _id: assignmentId,
+ user: req.user._id,
+ });
+ // Validation Again 💀
+ if (!assignment) {
+ throw new ApiError(404, "Assignment not found or unauthorized access");
+ }
+
+ let uploadedDocs = assignment.docs;
+ if (req.files && req.files.length > 0) {
+ const docs = req.files.map(async (file) => {
+ const localPath = file.path;
+ return await uploadToCloud(localPath).url;
+ });
+ const newDocs = await Promise.all(docs);
+ uploadedDocs = [...uploadedDocs, ...newDocs]; // Can't afford to loose previously uploaded docs
+ }
+
+ assignment.docs = uploadedDocs;
+
+ assignment.title = title || assignment.title;
+ assignment.description = description || assignment.description;
+ assignment.due_date = due_date || assignment.due_date;
+
+ await assignment.save();
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, assignment, "Assignment Updates Successfully"));
+});
+
+const deleteAssignment = asyncHandler(async (req, res) => {
+ const { assignmentId } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(assignmentId)) {
+ throw new ApiError(400, "Invalid Assignment ID.");
+ }
+
+ const assignment = await Assignment.findOne({
+ _id: assignmentId,
+ user: req.user._id,
+ });
+ // Validation Again 💀
+ if (!assignment) {
+ throw new ApiError(404, "Assignment not found or unauthorized access");
+ }
+
+ // Remove files from Cloudinary
+ if (assignment.docs.length > 0) {
+ const deleteFilePromises = assignment.docs.map(async (fileUrl) => {
+ try {
+ const publicId = fileUrl.split("/").pop().split(".")[0]; // Extract public ID (got this from stackoverflow)
+ await cloud.uploader.destroy(publicId);
+ } catch (error) {
+ console.error(`Failed to delete file: ${fileUrl}`, error);
+ }
+ });
+ await Promise.all(deleteFilePromises);
+ }
+
+ await Assignment.findByIdAndDelete(assignmentId);
+
+ return res.status(200).json(new ApiResponse(200, {}, "Khatam tata bye bye"));
+});
+
+export {
+ createAssignment,
+ getUserAssignments,
+ updateAssignment,
+ deleteAssignment,
+};
diff --git a/backend/src/controllers/message.controller.js b/backend/src/controllers/message.controller.js
new file mode 100644
index 0000000..19a6f0e
--- /dev/null
+++ b/backend/src/controllers/message.controller.js
@@ -0,0 +1,99 @@
+import { User } from "../models/user.models.js";
+import Message from "../models/message.models.js";
+import { ApiError } from "../utils/ApiError.js";
+import { ApiResponse } from "../utils/ApiResponse.js";
+import { asyncHandler } from "../utils/asyncHandler.js";
+import { uploadToCloud } from "../utils/cloudinary.js";
+import { getReceiverSocketId, io } from "../utils/socket.js";
+
+const getUsers = asyncHandler(async (req, res) => {
+ try {
+ const loggedUserId = req.user._id;
+
+ // Get all users except the logged in user
+ const filteredUsers = await User.find({
+ _id: { $ne: loggedUserId },
+ }).select("-password");
+
+ if (!filteredUsers) {
+ return res.status(404).json(new ApiResponse(404, null, "No users found"));
+ }
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, filteredUsers, "Users fetched"));
+ } catch (error) {
+ console.error("Error in getUsersForSidebar: ", error.message);
+ throw new ApiError(500, "Something went wrong while fetching users");
+ }
+});
+
+const getMessages = asyncHandler(async (req, res) => {
+ try {
+ const loggedUserId = req.user._id;
+ const receiverId = req.params.id;
+
+ // Fetching messages
+ const messages = await Message.find({
+ $or: [
+ { senderId: loggedUserId, receiverId: receiverId },
+ { senderId: receiverId, receiverId: loggedUserId },
+ ],
+ }).sort({ createdAt: 1 });
+
+ if (!messages) {
+ return res
+ .status(404)
+ .json(new ApiResponse(404, null, "No messages found"));
+ }
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, messages, "Messages fetched"));
+ } catch (error) {
+ console.error("Error in getMessages: ", error.message);
+ throw new ApiError(500, "Something went wrong while fetching messages");
+ }
+});
+
+const sendMessage = asyncHandler(async (req, res) => {
+ try {
+ const loggedUserId = req.user._id;
+ const receiverId = req.params.id;
+ const { text } = req.body;
+
+ let doc = null;
+ const file = req.file;
+ if (file) {
+ const localPath = file.path;
+ doc = await uploadToCloud(localPath);
+
+ if (!doc) {
+ throw new ApiError(400, "File couldn't be saved");
+ }
+ }
+
+ const message = await Message.create({
+ senderId: loggedUserId,
+ receiverId: receiverId,
+ text,
+ doc: doc ? doc.url : null,
+ });
+
+ if (!message) {
+ throw new ApiError(400, "Message couldn't be saved");
+ }
+
+ const receiverSocketId = getReceiverSocketId(receiverId);
+ if (receiverSocketId) {
+ io.to(receiverSocketId).emit("newMessage", message);
+ }
+
+ return res.status(201).json(new ApiResponse(201, message, "Message sent"));
+ } catch (error) {
+ console.error("Error in sendMessage: ", error.message);
+ throw new ApiError(500, "Something went wrong while sending message");
+ }
+});
+
+export { getUsers, getMessages, sendMessage };
diff --git a/backend/src/controllers/schedule.controllers.js b/backend/src/controllers/schedule.controllers.js
new file mode 100644
index 0000000..d8bf0e5
--- /dev/null
+++ b/backend/src/controllers/schedule.controllers.js
@@ -0,0 +1,74 @@
+import { Schedule } from "../models/schedules.models.js";
+import { ApiError } from "../utils/ApiError.js";
+import { ApiResponse } from "../utils/ApiResponse.js";
+import { asyncHandler } from "../utils/asyncHandler.js";
+import mongoose from "mongoose";
+
+// const createSchedule = asyncHandler(async (req, res) => {
+// const { reminderDate, assignmentId, taskId, message } = req.body;
+
+// if (!reminderDate) {
+// throw new ApiError(400, "date is required");
+// }
+
+// if (assignmentId && taskId) {
+// throw new ApiError(
+// 400,
+// "Schedule can be linked to either an assignment or a task, not both."
+// );
+// }
+
+// const schedule = await Schedule.create({
+// user: req.user._id,
+// reminderDate,
+// assignmentId: assignmentId || null,
+// taskId: taskId || null,
+// message: message || "Karle Bhai Complete",
+// });
+
+// return res
+// .status(201)
+// .json(new ApiResponse(201, schedule, "Schedule Bangaya 🤩!"));
+// });
+
+const getUserSchedules = asyncHandler(async (req, res) => {
+ const userId = req.user._id;
+
+ const schedules = await Schedule.find({ user: userId })
+ .populate("assignmentId", "title due_date")
+ .populate("taskId", "title deadline")
+ .sort({ reminderDate: 1 });
+
+ if (!schedules.length) {
+ throw new ApiError(404, "No Schedule");
+ }
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, schedules, "Schedules Fetched Successfully"));
+});
+
+const deleteSchedule = asyncHandler(async (req, res) => {
+ const { scheduleId } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(scheduleId)) {
+ throw new ApiError(400, "Invalid ScheduleId.");
+ }
+
+ const schedule = await Schedule.findOne({
+ _id: scheduleId,
+ user: req.user._id,
+ });
+
+ if (!schedule) {
+ throw new ApiError(404, "Schedule not found or unauthorized access");
+ }
+
+ await Schedule.findByIdAndDelete(scheduleId);
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, {}, "Schedule Deleted Successfully"));
+});
+
+export { getUserSchedules, deleteSchedule };
diff --git a/backend/src/controllers/task.controller.js b/backend/src/controllers/task.controller.js
new file mode 100644
index 0000000..4ff17d2
--- /dev/null
+++ b/backend/src/controllers/task.controller.js
@@ -0,0 +1,99 @@
+import { Task } from "../models/task.models.js";
+import { ApiError } from "../utils/ApiError.js";
+import { ApiResponse } from "../utils/ApiResponse.js";
+import { asyncHandler } from "../utils/asyncHandler.js";
+import mongoose from "mongoose";
+
+const createTask = asyncHandler(async (req, res) => {
+ const { title, description, due_date } = req.body;
+
+ if ([title, due_date].some((field) => field?.trim() === "")) {
+ throw new ApiError(400, "Some fields are required fields");
+ }
+
+ if (!title || !description) throw new ApiError(400, "Not present");
+
+ const task = await Task.create({
+ title,
+ description,
+ due_date,
+ user: req.user._id,
+ });
+
+ const taskFromDB = await Task.findById(task._id);
+
+ if (!taskFromDB) {
+ throw new ApiError(500, "Something went wrong while creating Task");
+ }
+
+ return res
+ .status(201)
+ .json(new ApiResponse(200, taskFromDB, "Task Ban Gaya 🤡!"));
+});
+
+const getUserTasks = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+
+ const tasks = await Task.find({ user: userId }).sort({
+ createdAt: -1,
+ });
+
+ // if (!tasks.length) {
+ // throw new ApiError(404, "No tasks found for this user...");
+ // }
+
+ return res.status(200).json(new ApiResponse(200, tasks, "Tasks Fetched"));
+});
+
+const updateTask = asyncHandler(async (req, res) => {
+ const { taskId } = req.params;
+ const { title, description, due_date } = req.body;
+
+ // Validate ObjectId
+ if (!mongoose.isValidObjectId(taskId)) {
+ throw new ApiError(400, "Invalid Task ID");
+ }
+
+ // Better approach (apparently😭)
+ const task = await Task.findOne({
+ _id: taskId,
+ user: req.user._id,
+ });
+ // Validation Again 💀
+ if (!task) {
+ throw new ApiError(404, "Task not found or unauthorized access");
+ }
+
+ task.title = title || task.title;
+ task.description = description || task.description;
+ task.due_date = due_date || task.due_date;
+
+ await task.save();
+
+ return res
+ .status(200)
+ .json(new ApiResponse(200, task, "Task Updated Successfully"));
+});
+
+const deleteTask = asyncHandler(async (req, res) => {
+ const { taskId } = req.params;
+
+ if (!mongoose.Types.ObjectId.isValid(taskId)) {
+ throw new ApiError(400, "Invalid Task ID.");
+ }
+
+ const task = await Task.findOne({
+ _id: taskId,
+ user: req.user._id,
+ });
+ // Validation Again 💀
+ if (!task) {
+ throw new ApiError(404, "Task not found or unauthorized access");
+ }
+
+ await Task.findByIdAndDelete(taskId);
+
+ return res.status(200).json(new ApiResponse(200, {}, "Khatam tata bye bye"));
+});
+
+export { createTask, getUserTasks, updateTask, deleteTask };
diff --git a/backend/src/controllers/user.controllers.js b/backend/src/controllers/user.controllers.js
new file mode 100644
index 0000000..00503ce
--- /dev/null
+++ b/backend/src/controllers/user.controllers.js
@@ -0,0 +1,226 @@
+import { asyncHandler } from "../utils/asyncHandler.js";
+import { ApiError } from "../utils/ApiError.js";
+import { ApiResponse } from "../utils/ApiResponse.js";
+import { User } from "../models/user.models.js";
+import { uploadToCloud } from "../utils/cloudinary.js";
+
+const generateAccessRefreshTokens = async (userId) => {
+ try {
+ // Generate Access and Refresh Tokens
+ const user = await User.findById(userId);
+ const accessToken = user.generateAccessToken();
+ const refreshToken = user.generateRefreshToken();
+
+ // Save Refresh Token in Database
+ user.refreshToken = refreshToken;
+ // Save User
+ await user.save({ validateBeforeSave: false });
+ return { accessToken, refreshToken };
+ } catch (err) {
+ throw new ApiError(
+ 500,
+ "Something went wrong while generating refresh and access token"
+ );
+ }
+};
+
+const register = asyncHandler(async (req, res) => {
+ // Get Data from req.body
+ const { fullName, username, email, password } = req.body;
+ if(!fullName) console.log("Nahi mila");
+
+ if ([fullName, email, username, password].some((field) => !field?.trim())) {
+ throw new ApiError(400, "Fill all fields");
+ }
+
+ // Check if all fields are present
+ const existedUser = await User.findOne({
+ $or: [{ username }, { email }],
+ });
+
+ // Return if user is already present
+ if (existedUser) {
+ throw new ApiError(409, "User Credentials Already Exists...");
+ }
+
+ const localPath = req.file?.path;
+ if (!localPath) {
+ throw new ApiError(400, "Avatar File is Required...");
+ }
+
+ console.log(localPath);
+ // Otherwise register the user and save details in database
+ const avatar = await uploadToCloud(localPath);
+ if (!avatar) {
+ throw new ApiError(400, "Avatar Couldn't be Saved...");
+ }
+
+ const user = await User.create({
+ fullName,
+ avatar: avatar.url,
+ email,
+ password,
+ username: username.toLowerCase(),
+ });
+
+ const userFromDB = await User.findById(user._id).select(
+ "-password -refreshToken"
+ );
+
+ const { accessToken, refreshToken } = await generateAccessRefreshTokens(
+ user._id
+ );
+
+ const cookieOptions = {
+ httpOnly: true,
+ secure: true,
+ };
+
+ // Returned Created user and message
+ return res
+ .status(201)
+ .cookie("accessToken", accessToken, cookieOptions)
+ .cookie("refreshToken", refreshToken, cookieOptions)
+ .json(new ApiResponse(200, userFromDB, "User Ban Gaya 🤩!"));
+});
+
+const login = asyncHandler(async (req, res) => {
+ console.log(req.body);
+ const { email, username, password } = req.body;
+
+ // Validation
+ if (!username && !email) {
+ throw new ApiError(400, "username or email is required");
+ }
+
+ // find user in database
+ const user = await User.findOne({
+ $or: [
+ {
+ username,
+ },
+ {
+ email,
+ },
+ ],
+ });
+
+ if (!user) {
+ throw new ApiError(404, "requested User doesn't even exist");
+ }
+
+ // Check if password is correct
+ const valid = await user.isPasswordCorrect(password);
+ if (!valid) {
+ throw new ApiError(401, "Invalid user credentials");
+ }
+
+ // Generate Access and Refresh Tokens
+ const { accessToken, refreshToken } = await generateAccessRefreshTokens(
+ user._id
+ );
+
+ // Send Access and Refresh Tokens as Cookies
+ const loggedUserFromDB = await User.findById(user._id).select(
+ "-password -refreshToken"
+ );
+
+ const cookieOptions = {
+ httpOnly: true,
+ secure: true,
+ };
+
+ // Return Logged In User and Tokens
+ return res
+ .status(200)
+ .cookie("accessToken", accessToken, cookieOptions)
+ .cookie("refreshToken", refreshToken, cookieOptions)
+ .json(
+ new ApiResponse(
+ 200,
+ {
+ user: loggedUserFromDB,
+ accessToken,
+ refreshToken,
+ },
+ "User logged In Successfully"
+ )
+ );
+});
+
+// Logout Functionality
+const logout = asyncHandler(async (req, res) => {
+ await User.findByIdAndUpdate(
+ req.user._id,
+ {
+ $unset: {
+ refreshToken: 1,
+ },
+ },
+ {
+ new: true,
+ }
+ );
+
+ const cookieOptions = {
+ httpOnly: true,
+ secure: true,
+ };
+
+ return res
+ .status(200)
+ .clearCookie("accessToken", cookieOptions)
+ .clearCookie("refreshToken", cookieOptions)
+ .json(new ApiResponse(200, {}, "User logged Out successfuly"));
+});
+
+// Endpoint that has to be hit to regenerate token
+const refreshAccessToken = asyncHandler(async (req, res) => {
+ const incomingToken = req.cookies.refreshToken || req.body.refreshToken;
+
+ if (!incomingToken) {
+ throw new ApiError(401, "Unauthorised Request");
+ }
+
+ const decodedToken = jwt.verify(
+ incomingToken,
+ process.env.REFRESH_TOKEN_SECRET
+ );
+ const user = await User.findById(decodedToken._id);
+ if (!user) {
+ throw new ApiError(401, "Invalid Token");
+ }
+
+ if (incomingToken !== user?.refreshToken) {
+ throw new ApiError(401, "Refresh Token is expired or used");
+ }
+ const cookieOptions = {
+ httpOnly: true,
+ secure: true,
+ };
+
+ const { accessToken, newRefreshToken } = await generateAccessRefreshTokens(
+ user._id
+ );
+
+ return res
+ .status(200)
+ .cookie("accessToken", accessToken, cookieOptions)
+ .cookie("refreshToken", newRefreshToken, cookieOptions)
+ .json(
+ new ApiResponse(
+ 200,
+ { accessToken, refreshToken: newRefreshToken },
+ "Token Refreshed"
+ )
+ );
+});
+
+// Get Current user data lol
+const getCurrentUser = asyncHandler(async (req, res) => {
+ return res
+ .status(200)
+ .json(new ApiResponse(200, req.user, "Current User Data"));
+});
+
+export { register, login, logout, refreshAccessToken, getCurrentUser };
diff --git a/backend/src/index.js b/backend/src/index.js
index 32aa522..936aaca 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -1,17 +1,63 @@
+import express from "express";
// dotenv for env variable access throughout the project
import "dotenv/config";
+import cookieParser from "cookie-parser";
+import cors from "cors";
// this file will handle core routing logic and route controllers
-import { app } from "./app.js";
+import { app, server } from "./utils/socket.js";
// mongodb connection
import connectDB from "./db/db.js";
const PORT = process.env.PORT || 8000;
+app.use(express.json({ limit: "16kb" }));
+app.use(express.urlencoded({ extended: true, limit: "16kb" }));
+
+// Configuring express to mark public as static storage folder
+app.use(express.static("public"));
+
+// Cookie Parser configuration for tokens
+app.use(cookieParser());
+
+app.use(
+ cors({
+ origin: "http://localhost:5173",
+ credentials: true,
+ })
+);
+
+import userRouter from "./routes/user.routes.js";
+import assignmentRouter from "./routes/assignment.routes.js";
+import taskRouter from "./routes/task.routes.js";
+import messageRouter from "./routes/message.routes.js";
+import scheduleRouter from "./routes/schedule.routes.js";
+
+// Routes Declaration
+app.use("/users", userRouter);
+app.use("/assignments", assignmentRouter);
+app.use("/tasks", taskRouter);
+app.use("/messages", messageRouter);
+app.use("/schedules", scheduleRouter);
+
+app.get("/quote", async (req, res) => {
+ try {
+ const response = await fetch("https://zenquotes.io/api/random");
+ if (!response.ok) throw new Error("Failed to fetch quote");
+
+ const data = await response.json();
+ res.json(data);
+ console.log("Quote fetched from ZenQuotes API");
+ } catch (error) {
+ console.error("Error fetching quote from ZenQuotes API:", error);
+ res.status(500).json({ error: "Failed to fetch quote from ZenQuotes" });
+ }
+});
+
connectDB()
.then(() => {
- app.listen(PORT, () => {
+ server.listen(PORT, () => {
console.log(`🤖 Server running on port ${PORT}`);
});
})
diff --git a/backend/src/middlewares/auth.middleware.js b/backend/src/middlewares/auth.middleware.js
new file mode 100644
index 0000000..a0b3365
--- /dev/null
+++ b/backend/src/middlewares/auth.middleware.js
@@ -0,0 +1,27 @@
+import { ApiError } from "../utils/ApiError.js";
+import { asyncHandler } from "../utils/asyncHandler.js";
+import jwt from "jsonwebtoken";
+import { User } from "../models/user.models.js";
+
+export const verifyJWT = asyncHandler(async (req, _, next) => {
+ try {
+ // Get the token from the cookies
+ const token = req.cookies.accessToken;
+ if (!token) throw new ApiError(401, "Kuch to kaam nahi kar rha hai");
+
+ // Verify the token
+ const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
+ // Find the user with the decoded id
+ const user = await User.findById(decoded?._id).select(
+ "-password -refreshToken"
+ );
+ if (!user) {
+ throw new ApiError(401, "Invalid Access");
+ }
+ // Set the user in the request object
+ req.user = user;
+ next();
+ } catch (err) {
+ throw new ApiError(401, err);
+ }
+});
diff --git a/backend/src/middlewares/multer.middleware.js b/backend/src/middlewares/multer.middleware.js
new file mode 100644
index 0000000..120cdbd
--- /dev/null
+++ b/backend/src/middlewares/multer.middleware.js
@@ -0,0 +1,29 @@
+import multer from "multer";
+
+// We wouldn't want filenames to be same in the backend
+// This will generate random filename for a file based on the current timestamp
+// function getRandomFileName() {
+// var timestamp = new Date().toISOString().replace(/[-:.]/g, "");
+// var random = ("" + Math.random()).substring(2, 8);
+// var random_number = timestamp + random;
+// return random_number;
+// }
+
+// Basic multer storage configuration
+const storage = multer.diskStorage({
+ destination: function (req, file, cb) {
+ console.log(req.body);
+ // All local cached files will be in temp folder in public directory
+ cb(null, "./public/temp");
+ },
+ filename: function (req, file, cb) {
+ console.log("MIME TYPE", file.mimetype);
+ // FIXME: This is a bug, the file name should be random
+ // const randomName = getRandomFileName();
+ // const ext = path.extname(file.originalname);
+ // cb(null, `${randomName}${ext}`);
+ cb(null, file.originalname);
+ },
+});
+
+export const upload = multer({ storage });
diff --git a/backend/src/models/assignment.models.js b/backend/src/models/assignment.models.js
new file mode 100644
index 0000000..2b4a5bc
--- /dev/null
+++ b/backend/src/models/assignment.models.js
@@ -0,0 +1,59 @@
+import mongoose, { Schema } from "mongoose";
+import { Schedule } from "./schedules.models.js";
+
+const assignmentSchema = new Schema(
+ {
+ title: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ },
+ docs: [
+ {
+ type: String,
+ required: true,
+ },
+ ],
+ due_date: {
+ type: Date,
+ required: true,
+ },
+ isComplete: {
+ type: Boolean,
+ default: false,
+ },
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+assignmentSchema.post("save", async function (doc, next) {
+ try {
+ await Schedule.create({
+ user: doc.user,
+ reminderDate: doc.due_date,
+ assignmentId: doc._id,
+ message: `Assignment: ${doc.title}"`,
+ });
+ console.log("Schedule created for Assignment:", doc.title);
+ } catch (error) {
+ console.error("Error creating schedule for assignment:", error);
+ }
+ next();
+});
+
+assignmentSchema.post("findOneAndDelete", async function (doc) {
+ if (doc) {
+ await Schedule.deleteMany({ assignmentId: doc._id });
+ console.log("Deleted schedule for assignment:", doc.title);
+ }
+});
+
+export const Assignment = mongoose.model("Assignment", assignmentSchema);
diff --git a/backend/src/models/diagram-export-1-29-2025-6_49_52-PM.png b/backend/src/models/diagram-export-1-29-2025-6_49_52-PM.png
new file mode 100644
index 0000000..f8f0673
Binary files /dev/null and b/backend/src/models/diagram-export-1-29-2025-6_49_52-PM.png differ
diff --git a/backend/src/models/message.models.js b/backend/src/models/message.models.js
new file mode 100644
index 0000000..2ea009f
--- /dev/null
+++ b/backend/src/models/message.models.js
@@ -0,0 +1,27 @@
+import mongoose, { Schema } from "mongoose";
+
+const messageSchema = new 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, //URL 😏
+ },
+ },
+ { timestamps: true }
+);
+
+const Message = mongoose.model("Message", messageSchema);
+
+export default Message;
diff --git a/backend/src/models/schedules.models.js b/backend/src/models/schedules.models.js
new file mode 100644
index 0000000..bd512fd
--- /dev/null
+++ b/backend/src/models/schedules.models.js
@@ -0,0 +1,37 @@
+import mongoose, { Schema } from "mongoose";
+
+const scheduleSchema = new Schema(
+ {
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ },
+ reminderDate: {
+ type: Date,
+ required: true,
+ },
+ assignmentId: {
+ type: Schema.Types.ObjectId,
+ ref: "Assignment",
+ default: null,
+ },
+ taskId: {
+ type: Schema.Types.ObjectId,
+ ref: "Task",
+ default: null,
+ },
+ message: {
+ type: String,
+ default: "Complete Your Assignment",
+ },
+ status: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+export const Schedule = mongoose.model("Schedule", scheduleSchema);
diff --git a/backend/src/models/task.models.js b/backend/src/models/task.models.js
new file mode 100644
index 0000000..6db832e
--- /dev/null
+++ b/backend/src/models/task.models.js
@@ -0,0 +1,60 @@
+import mongoose, { Schema } from "mongoose";
+import { Schedule } from "./schedules.models.js";
+
+const taskSchema = new Schema(
+ {
+ title: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ },
+ due_date: {
+ type: Date,
+ required: true,
+ },
+ isComplete: {
+ type: Boolean,
+ default: false,
+ },
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+taskSchema.pre("save", function (next) {
+ if (typeof this.due_date === "string") {
+ this.due_date = new Date(this.due_date);
+ }
+ next();
+});
+
+taskSchema.post("save", async function (doc, next) {
+ try {
+ await Schedule.create({
+ user: doc.user,
+ reminderDate: doc.due_date,
+ taskId: doc._id,
+ message: `Task: "${doc.title}"`,
+ });
+ console.log("Schedule created for Task:", doc.title);
+ } catch (error) {
+ console.error("Error creating schedule for task:", error);
+ }
+ next();
+});
+
+taskSchema.post("findOneAndDelete", async function (doc) {
+ if (doc) {
+ await Schedule.deleteMany({ taskId: doc._id });
+ console.log("Deleted schedule for task:", doc.title);
+ }
+});
+
+export const Task = mongoose.model("Task", taskSchema);
diff --git a/backend/src/models/user.models.js b/backend/src/models/user.models.js
new file mode 100644
index 0000000..2610b8e
--- /dev/null
+++ b/backend/src/models/user.models.js
@@ -0,0 +1,96 @@
+import mongoose, { Schema } from "mongoose";
+import jwt from "jsonwebtoken";
+import bcrypt from "bcrypt";
+
+const userSchema = new Schema(
+ {
+ fullName: {
+ type: String,
+ required: true,
+ index: true,
+ trim: true,
+ },
+ username: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ trim: true,
+ index: true,
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ trim: true,
+ },
+ avatar: {
+ type: String, // Cloudinary URL
+ required: true,
+ },
+ password: {
+ type: String,
+ required: [true, "Password is required"],
+ },
+ priorityOrder: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: "Assignment",
+ },
+ ],
+ refreshToken: {
+ type: String,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+// Hashing the password before saving them into database for security
+userSchema.pre("save", async function (next) {
+ if (this.isModified("password")) {
+ this.password = await bcrypt.hash(this.password, 10);
+ }
+ return next();
+});
+
+// Custom Methods
+
+// While we are at it maybe i can also add a custom method to check if the password matches hashed password from our backend
+// This just returns a boolean after comparing the two
+userSchema.methods.isPasswordCorrect = async function (password) {
+ return await bcrypt.compare(password, this.password);
+};
+
+// Access Token
+userSchema.methods.generateAccessToken = function () {
+ return jwt.sign(
+ {
+ _id: this._id,
+ email: this.email,
+ username: this.username,
+ fullName: this.fullName,
+ },
+ process.env.ACCESS_TOKEN_SECRET,
+ {
+ expiresIn: process.env.ACCESS_TOKEN_EXPIRY,
+ }
+ );
+};
+
+// Refresh Token
+userSchema.methods.generateRefreshToken = function () {
+ return jwt.sign(
+ {
+ _id: this._id,
+ },
+ process.env.REFRESH_TOKEN_SECRET,
+ {
+ expiresIn: process.env.REFRESH_TOKEN_EXPIRY,
+ }
+ );
+};
+
+export const User = mongoose.model("User", userSchema);
diff --git a/backend/src/routes/assignment.routes.js b/backend/src/routes/assignment.routes.js
new file mode 100644
index 0000000..966c862
--- /dev/null
+++ b/backend/src/routes/assignment.routes.js
@@ -0,0 +1,22 @@
+import { Router } from "express";
+import { verifyJWT } from "../middlewares/auth.middleware.js";
+import { upload } from "../middlewares/multer.middleware.js";
+import {
+ createAssignment,
+ getUserAssignments,
+ updateAssignment,
+ deleteAssignment,
+} from "../controllers/assignment.controllers.js";
+const router = Router();
+
+router.use(verifyJWT);
+
+router.route("/").post(upload.array("docs", 5), createAssignment);
+
+// Fetch all assignments of current user
+router.route("/getAssignments").get(getUserAssignments);
+
+// Assignment update and delete routes
+router.route("/:assignmentId").patch(updateAssignment).delete(deleteAssignment);
+
+export default router;
diff --git a/backend/src/routes/message.routes.js b/backend/src/routes/message.routes.js
new file mode 100644
index 0000000..2867a50
--- /dev/null
+++ b/backend/src/routes/message.routes.js
@@ -0,0 +1,17 @@
+import { Router } from "express";
+import { verifyJWT } from "../middlewares/auth.middleware.js";
+import {
+ getUsers,
+ getMessages,
+ sendMessage,
+} from "../controllers/message.controller.js";
+import { upload } from "../middlewares/multer.middleware.js";
+
+const router = Router();
+
+router.get("/users", verifyJWT, getUsers);
+router.get("/:id", verifyJWT, getMessages);
+
+router.post("/send/:id", verifyJWT, upload.single("img"), sendMessage);
+
+export default router;
diff --git a/backend/src/routes/schedule.routes.js b/backend/src/routes/schedule.routes.js
new file mode 100644
index 0000000..f10ac88
--- /dev/null
+++ b/backend/src/routes/schedule.routes.js
@@ -0,0 +1,21 @@
+import { Router } from "express";
+import { verifyJWT } from "../middlewares/auth.middleware.js";
+import {
+ getUserSchedules,
+ deleteSchedule,
+} from "../controllers/schedule.controllers.js";
+
+const router = Router();
+
+router.use(verifyJWT);
+
+// Create a new schedule
+// router.route("/").post(createSchedule);
+
+// Fetch all schedules of the current user
+router.route("/getSchedules").get(getUserSchedules);
+
+// Delete a schedule
+router.route("/:scheduleId").delete(deleteSchedule);
+
+export default router;
diff --git a/backend/src/routes/task.routes.js b/backend/src/routes/task.routes.js
new file mode 100644
index 0000000..708432c
--- /dev/null
+++ b/backend/src/routes/task.routes.js
@@ -0,0 +1,22 @@
+import { Router } from "express";
+import { verifyJWT } from "../middlewares/auth.middleware.js";
+import { upload } from "../middlewares/multer.middleware.js";
+import {
+ createTask,
+ getUserTasks,
+ updateTask,
+ deleteTask,
+} from "../controllers/task.controller.js";
+const router = Router();
+
+router.use(verifyJWT);
+
+router.route("/").post(upload.none(), createTask);
+
+// Fetch all tasks of current user
+router.route("/getTasks").get(getUserTasks);
+
+// Task update and delete routes
+router.route("/:taskId").patch(updateTask).delete(deleteTask);
+
+export default router;
diff --git a/backend/src/routes/user.routes.js b/backend/src/routes/user.routes.js
new file mode 100644
index 0000000..6f6cf10
--- /dev/null
+++ b/backend/src/routes/user.routes.js
@@ -0,0 +1,24 @@
+import { Router } from "express";
+import {
+ register,
+ logout,
+ login,
+ refreshAccessToken,
+ getCurrentUser,
+} from "../controllers/user.controllers.js";
+import { upload } from "../middlewares/multer.middleware.js";
+import { verifyJWT } from "../middlewares/auth.middleware.js";
+
+const router = Router();
+
+router.route("/register").post(upload.single("avatar"), register);
+router.route("/login").post(login);
+
+// Secured routes with Authentication
+router.route("/logout").post(verifyJWT, logout);
+router.route("/refresh-token").post(refreshAccessToken);
+
+// Current User Data
+router.route("/current-user").get(verifyJWT, getCurrentUser);
+
+export default router;
diff --git a/backend/src/utils/ApiError.js b/backend/src/utils/ApiError.js
new file mode 100644
index 0000000..4667a7e
--- /dev/null
+++ b/backend/src/utils/ApiError.js
@@ -0,0 +1,30 @@
+// Custom Error messages to use throughout the app for consistent logs
+
+class ApiError extends Error {
+ // Default Response
+ constructor(
+ statusCode,
+ message = "Something went wrong",
+ errors = [],
+ stack = ""
+ ) {
+ super(message);
+ this.statusCode = statusCode;
+ // No need for error to return data
+ this.data = null;
+ this.message = message;
+ // ofcourse it's not success
+ this.success = false;
+ this.errors = errors;
+
+
+ // Good practice to also return stack trace for debugging
+ if (stack) {
+ this.stack = stack;
+ } else {
+ Error.captureStackTrace(this, this.constructor);
+ }
+ }
+}
+
+export { ApiError };
diff --git a/backend/src/utils/ApiResponse.js b/backend/src/utils/ApiResponse.js
new file mode 100644
index 0000000..d9ab421
--- /dev/null
+++ b/backend/src/utils/ApiResponse.js
@@ -0,0 +1,12 @@
+// Custom API response for consistent data returns at frontend
+
+class ApiResponse {
+ constructor(statusCode, data, message = "Success") {
+ (this.statusCode = statusCode),
+ (this.data = data),
+ (this.message = message),
+ (this.success = statusCode < 400);
+ }
+}
+
+export { ApiResponse };
diff --git a/backend/src/utils/asyncHandler.js b/backend/src/utils/asyncHandler.js
new file mode 100644
index 0000000..d69892d
--- /dev/null
+++ b/backend/src/utils/asyncHandler.js
@@ -0,0 +1,8 @@
+// Higher Order function to handle asynchronous calls and errors
+// This will be used to call functions that require time and need to be called asynchronously
+
+export const asyncHandler = (requestHandler) => {
+ return (req, res, next) => {
+ Promise.resolve(requestHandler(req, res, next)).catch((err) => next(err));
+ };
+};
diff --git a/backend/src/utils/cloudinary.js b/backend/src/utils/cloudinary.js
new file mode 100644
index 0000000..cbcb287
--- /dev/null
+++ b/backend/src/utils/cloudinary.js
@@ -0,0 +1,43 @@
+// Had to import since we are using type: module
+import { v2 as cloud } from "cloudinary";
+
+// Filesystem for filehandling (deletion capabilities)
+import fs from "fs";
+
+cloud.config({
+ cloud_name: "the-secretary",
+ api_key: process.env.CLOUDINARY_KEY,
+ api_secret: process.env.CLOUDINARY_SECRET,
+});
+
+// we can't store everything locally
+const deleteLocalFile = (path) => {
+ try {
+ fs.unlinkSync(path);
+ console.log(`Successfully deleted ${path}`);
+ } catch (err) {
+ console.error(`Error deleting file ${path}`, err);
+ }
+};
+
+// Helper function to upload file to cloudinary from local path
+const uploadToCloud = async (localPath) => {
+ try {
+ if (!localPath) return null;
+ const res = await cloud.uploader.upload(localPath, {
+ use_filename: true,
+ unique_filename: false,
+ overwrite: true,
+ resource_type: "auto",
+ });
+ console.log("File Uploaded Successfully", res.url);
+ deleteLocalFile(localPath);
+ return res;
+ } catch (err) {
+ console.log(err);
+ deleteLocalFile(localPath);
+ return null;
+ }
+};
+
+export { uploadToCloud, cloud };
diff --git a/backend/src/utils/socket.js b/backend/src/utils/socket.js
new file mode 100644
index 0000000..01810cf
--- /dev/null
+++ b/backend/src/utils/socket.js
@@ -0,0 +1,36 @@
+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];
+}
+
+const userSocketMap = {};
+
+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 };
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 0dc290b..247ff6b 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -24,6 +24,25 @@
dependencies:
sparse-bitfield "^3.0.3"
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
+ integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
+
+"@types/cors@^2.8.12":
+ version "2.8.17"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
+ integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*", "@types/node@>=10.0.0":
+ version "22.13.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
+ integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
+ dependencies:
+ undici-types "~6.20.0"
+
"@types/webidl-conversions@*":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz#1306dbfa53768bcbcfc95a1c8cde367975581859"
@@ -41,7 +60,7 @@ abbrev@1:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-accepts@~1.3.8:
+accepts@~1.3.4, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
@@ -97,6 +116,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+base64id@2.0.0, base64id@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+ integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
bcrypt@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2"
@@ -269,7 +293,7 @@ cookie@0.7.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
-cookie@0.7.2:
+cookie@0.7.2, cookie@~0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
@@ -279,7 +303,7 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-cors@^2.8.5:
+cors@^2.8.5, cors@~2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
@@ -301,6 +325,13 @@ debug@4, debug@4.x, debug@^4:
dependencies:
ms "^2.1.3"
+debug@~4.3.1, debug@~4.3.2, debug@~4.3.4:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
+ integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+ dependencies:
+ ms "^2.1.3"
+
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -362,6 +393,26 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+engine.io-parser@~5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
+ integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
+
+engine.io@~6.6.0:
+ version "6.6.4"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee"
+ integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==
+ dependencies:
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
+ accepts "~1.3.4"
+ base64id "2.0.0"
+ cookie "~0.7.2"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.2.1"
+ ws "~8.17.1"
+
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
@@ -1198,6 +1249,35 @@ simple-update-notifier@^2.0.0:
dependencies:
semver "^7.5.3"
+socket.io-adapter@~2.5.2:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082"
+ integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==
+ dependencies:
+ debug "~4.3.4"
+ ws "~8.17.1"
+
+socket.io-parser@~4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+ integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@^4.8.1:
+ version "4.8.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a"
+ integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ cors "~2.8.5"
+ debug "~4.3.2"
+ engine.io "~6.6.0"
+ socket.io-adapter "~2.5.2"
+ socket.io-parser "~4.2.4"
+
sparse-bitfield@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11"
@@ -1311,6 +1391,11 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
+undici-types@~6.20.0:
+ version "6.20.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
+ integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -1369,6 +1454,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+ws@~8.17.1:
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
+
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
diff --git a/frontend/components/AssignmentsManager.jsx b/frontend/components/AssignmentsManager.jsx
new file mode 100644
index 0000000..cfa505f
--- /dev/null
+++ b/frontend/components/AssignmentsManager.jsx
@@ -0,0 +1,219 @@
+import { useState, useEffect } from "react";
+import { useAssignmentStore } from "../src/store/useAssignmentStore";
+import { useTheme } from "../context/ThemeContext";
+import { LuUpload } from "react-icons/lu";
+import { FaFileAlt } from "react-icons/fa";
+
+export default function AssignmentsManager() {
+ const theme = useTheme();
+ const {
+ assignments,
+ fetchAssignments,
+ createAssignment,
+ updateAssignment,
+ deleteAssignment,
+ isLoading,
+ error,
+ } = useAssignmentStore();
+
+ const [assignmentData, setAssignmentData] = useState({
+ title: "",
+ description: "",
+ due_date: "",
+ docs: [],
+ });
+ const [editingId, setEditingId] = useState(null);
+
+ useEffect(() => {
+ fetchAssignments();
+ }, [fetchAssignments]);
+
+ const handleFileChange = (e) => {
+ const files = Array.from(e.target.files);
+ setAssignmentData((prev) => ({ ...prev, docs: [...prev.docs, ...files] }));
+ };
+
+ const removeFile = (index) => {
+ setAssignmentData((prev) => ({
+ ...prev,
+ docs: prev.docs.filter((_, i) => i !== index),
+ }));
+ };
+
+ const handleEdit = (assignment) => {
+ setEditingId(assignment._id);
+ setAssignmentData({
+ title: assignment.title,
+ description: assignment.description,
+ due_date: assignment.due_date || "",
+ docs: [],
+ });
+ };
+
+ const handleCancelEdit = () => {
+ setEditingId(null);
+ setAssignmentData({ title: "", description: "", due_date: "", docs: [] });
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const formData = new FormData();
+ formData.append("title", assignmentData.title);
+ formData.append("description", assignmentData.description);
+ formData.append("due_date", assignmentData.due_date);
+ assignmentData.docs.forEach((file) => formData.append("docs", file));
+ if (editingId) {
+ await updateAssignment(editingId, formData);
+ } else {
+ await createAssignment(formData);
+ }
+ handleCancelEdit();
+ fetchAssignments();
+ };
+
+ return (
+
+
+
+
+ {editingId ? "Edit Assignment" : "Assignments"}
+
+
+ {error &&
{error}
}
+
+
+
+
+
+ {isLoading && (
+
Loading assignments...
+ )}
+
+
+ {assignments.map((assignment) => (
+
+
+
{assignment.title}
+
+ {assignment.description}
+
+
+ Due: {assignment.due_date || "No date set"}
+
+
+ {assignment.docs &&
+ assignment.docs.length > 0 &&
+ assignment.docs.map((doc, index) => (
+
+
+
+ ))}
+
+
+ handleEdit(assignment)}>
+ Edit
+
+ deleteAssignment(assignment._id)}>
+ Delete
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/components/ChatContainer.jsx b/frontend/components/ChatContainer.jsx
new file mode 100644
index 0000000..67be2cd
--- /dev/null
+++ b/frontend/components/ChatContainer.jsx
@@ -0,0 +1,89 @@
+import { useChatStore } from "../src/store/useChatStore.js";
+import { useAuthStore } from "../src/store/useAuthStore.js";
+import { useEffect, useRef } from "react";
+import ChatHeader from "./ChatHeader";
+import MessageInput from "./MessageInput";
+
+function ChatContainer() {
+ // StackOverflow + AI
+ function formatMessageTime(date) {
+ return new Date(date).toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ });
+ }
+ const {
+ messages,
+ getMessages,
+ isMessagesLoading,
+ currentUser,
+ updateOnRealtime,
+ removeUpdateOnRealtime,
+ } = useChatStore();
+ const { authUser } = useAuthStore();
+ const messageEndRef = useRef(null);
+
+ useEffect(() => {
+ getMessages(currentUser._id);
+ updateOnRealtime();
+
+ return () => removeUpdateOnRealtime();
+ }, [currentUser._id, getMessages, updateOnRealtime, removeUpdateOnRealtime]);
+
+ useEffect(() => {
+ const scrollToBottom = () => {
+ messageEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ scrollToBottom();
+ }, [messages]);
+
+ if (isMessagesLoading) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+
+ {messages.map((message) => (
+
+
+
+
+
+
+
+
+ {formatMessageTime(message.createdAt)}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+export default ChatContainer;
diff --git a/frontend/components/ChatHeader.jsx b/frontend/components/ChatHeader.jsx
new file mode 100644
index 0000000..afe4251
--- /dev/null
+++ b/frontend/components/ChatHeader.jsx
@@ -0,0 +1,37 @@
+import { RxCross1 } from "react-icons/rx";
+import { useAuthStore } from "../src/store/useAuthStore";
+import { useChatStore } from "../src/store/useChatStore";
+
+const ChatHeader = () => {
+ const { currentUser, setCurrentUser } = useChatStore();
+ const { onlineUsers } = useAuthStore();
+
+ return (
+
+
+
+ {/* Avatar */}
+
+
+
+
+
+
+ {/* User info */}
+
+
{currentUser.fullName}
+
+ {onlineUsers.includes(currentUser._id) ? "Online" : "Offline"}
+
+
+
+
+ {/* Close button */}
+
setCurrentUser(null)}>
+
+
+
+
+ );
+};
+export default ChatHeader;
\ No newline at end of file
diff --git a/frontend/components/DashHeader.jsx b/frontend/components/DashHeader.jsx
new file mode 100644
index 0000000..c6e14f8
--- /dev/null
+++ b/frontend/components/DashHeader.jsx
@@ -0,0 +1,57 @@
+import { HiOutlineMenuAlt2 } from "react-icons/hi";
+import { HiOutlineBadgeCheck } from "react-icons/hi";
+import ThemeSwitcher from "./ThemeSwitcher";
+import { useTheme } from "../context/ThemeContext"; // Import theme context
+import { Link } from "react-router-dom";
+
+const DashHeader = () => {
+ const { theme } = useTheme(); // Get the current theme
+
+ return (
+
+
+
+ {/* Left Section: Logo & Menu Button */}
+
+
+
+
+
+
+
+
+ Assignify
+
+
+
+
+ {/* Right Section: Theme Toggle Button */}
+
+
+
+
+
+
+ );
+};
+
+export default DashHeader;
diff --git a/frontend/components/DashboardSidebar.jsx b/frontend/components/DashboardSidebar.jsx
new file mode 100644
index 0000000..6786282
--- /dev/null
+++ b/frontend/components/DashboardSidebar.jsx
@@ -0,0 +1,52 @@
+import {
+ HiOutlineViewGrid,
+ HiOutlineClipboardList,
+ HiOutlineChartBar,
+ HiOutlineChatAlt,
+ HiOutlineLogout,
+ HiUser,
+} from "react-icons/hi";
+import { Link } from "react-router-dom";
+import { useAuthStore } from "../src/store/useAuthStore";
+
+const DashboardSidebar = () => {
+ const { logout } = useAuthStore();
+ return (
+
+
+
+
+ Dashboard
+
+
+
+
+ Assignments
+
+
+
+
+ Heatmap
+
+
+
+
+ Chats
+
+
+
+
+ Profile
+
+
+
+
+ Logout
+
+
+
+
+ );
+};
+
+export default DashboardSidebar;
diff --git a/frontend/components/MessageInput.jsx b/frontend/components/MessageInput.jsx
new file mode 100644
index 0000000..7ca52bd
--- /dev/null
+++ b/frontend/components/MessageInput.jsx
@@ -0,0 +1,59 @@
+import { useState } from "react";
+import { useChatStore } from "../src/store/useChatStore";
+import { LuSend } from "react-icons/lu";
+// import toast from "react-hot-toast";
+
+const MessageInput = () => {
+ const [messageText, setMessageText] = useState("");
+ const { sendMessage } = useChatStore();
+
+ const handleSendMessage = async (e) => {
+ e.preventDefault();
+ if (messageText.trim() === "") return;
+
+ try {
+ await sendMessage({
+ text: messageText.trim(),
+ });
+ // Clear form
+ setMessageText("");
+ } catch (error) {
+ console.error("Failed to send message:", error);
+ }
+ };
+
+ return (
+
+ );
+};
+export default MessageInput;
diff --git a/frontend/components/Modal.jsx b/frontend/components/Modal.jsx
index bd8c7da..5044de9 100644
--- a/frontend/components/Modal.jsx
+++ b/frontend/components/Modal.jsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useModal } from '../context/ModalContext';
diff --git a/frontend/components/Navbar.jsx b/frontend/components/Navbar.jsx
new file mode 100644
index 0000000..d71e3a9
--- /dev/null
+++ b/frontend/components/Navbar.jsx
@@ -0,0 +1,51 @@
+import { NavLink, useNavigate } from "react-router-dom";
+import { TbLogout } from "react-icons/tb";
+import { useAuthStore } from "../src/store/useAuthStore";
+
+function Navbar() {
+ const navigate = useNavigate();
+ const { authUser, logout } = useAuthStore();
+ console.log("Auth User:", authUser);
+ console.log(authUser.username);
+
+ return (
+
+
+
+ Your Chats
+
+
+
navigate(-1)} className="btn btn-ghost btn-circle">
+
+
+
+
+
+
+
+
+
+
+ Profile
+
+
+ Logout
+
+
+
+
+ );
+}
+
+export default Navbar;
diff --git a/frontend/components/NavbarHome.jsx b/frontend/components/NavbarHome.jsx
new file mode 100644
index 0000000..b3d1fcc
--- /dev/null
+++ b/frontend/components/NavbarHome.jsx
@@ -0,0 +1,75 @@
+import { useState } from "react";
+import { FaSearch, FaBars, FaArrowRight, FaRightToBracket } from "react-icons/fa";
+
+const Navbar = () => {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ return (
+
+ {/* Top navbar */}
+
+ {/* Logo */}
+
+
+ Company
+
+
+ {/* Search (Desktop) */}
+
+
+ Good potions
+ Bad potions
+ Illegal potions
+
+
+
+
+
+
+
+ {/* Menu (Desktop) */}
+
+
+ {/* Menu (Mobile) */}
+
+
setIsDropdownOpen(!isDropdownOpen)}>
+
+
+
+ {isDropdownOpen && (
+
+ )}
+
+
+
+ {/* Bottom navbar (mobile only) */}
+
+
+
+ Good potions
+ Bad potions
+ Illegal potions
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Navbar;
diff --git a/frontend/components/NoChatSelected.jsx b/frontend/components/NoChatSelected.jsx
new file mode 100644
index 0000000..9f6735d
--- /dev/null
+++ b/frontend/components/NoChatSelected.jsx
@@ -0,0 +1,27 @@
+// import { FaRegMessage } from "react-icons/fa6";
+import { TbMessage } from "react-icons/tb";
+
+
+function NoChatSelected() {
+ return (
+
+
+
+
Chat Area
+
+ Select a user from sidebar to start chatting
+
+
+
+ );
+}
+
+export default NoChatSelected;
diff --git a/frontend/components/QuoteDisplay.jsx b/frontend/components/QuoteDisplay.jsx
new file mode 100644
index 0000000..7d908b3
--- /dev/null
+++ b/frontend/components/QuoteDisplay.jsx
@@ -0,0 +1,54 @@
+import { useState, useEffect } from "react";
+import axios from "axios";
+import { useTheme } from "../context/ThemeContext";
+import { axiosInstance } from "../src/lib/axios";
+const QuoteDisplay = () => {
+ const { theme } = useTheme(); // Get theme state
+ const [quote, setQuote] = useState("");
+ const [author, setAuthor] = useState("");
+ const [loading, setLoading] = useState(true);
+
+ // Fetch a random quote from ZenQuote API
+ const fetchQuote = async () => {
+ try {
+ const response = await axiosInstance.get('/quote')
+ setQuote(response.data[0].q);
+ setAuthor(response.data[0].a);
+ setLoading(false);
+ } catch (error) {
+ console.error("Error fetching quote:", error);
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchQuote(); // Fetch the quote when the component mounts
+ }, []);
+
+ return (
+
+ {loading ? (
+
Loading quote...
+ ) : (
+
+
+ {quote}
+
+
+ - {author}
+
+
+ )}
+
+ );
+};
+
+export default QuoteDisplay;
diff --git a/frontend/components/RollingQuotes.jsx b/frontend/components/RollingQuotes.jsx
new file mode 100644
index 0000000..225dd28
--- /dev/null
+++ b/frontend/components/RollingQuotes.jsx
@@ -0,0 +1,39 @@
+import { useState, useEffect } from "react";
+import { motion } from "framer-motion";
+
+export default function RollingQuotes() {
+ const quotes = [
+ "The best way to predict the future is to create it.",
+ "Success is not final, failure is not fatal: It is the courage to continue that counts.",
+ "Believe you can and you're halfway there.",
+ "The only limit to our realization of tomorrow is our doubts of today.",
+ "The future belongs to those who learn more skills and combine them in creative ways. – Robert Greene",
+ ];
+
+ const [currentQuote, setCurrentQuote] = useState(0);
+
+ // Automatically switch to the next quote every 3 seconds
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentQuote((prevQuote) => (prevQuote + 1) % quotes.length);
+ ease: "easeInOut";
+ }, 2000); // Change quote every 3 seconds
+
+ return () => clearInterval(interval); // Clean up interval on component unmount
+ }, []);
+
+ return (
+
+
+ {quotes[currentQuote]}
+
+
+ );
+}
diff --git a/frontend/components/Sidebar.jsx b/frontend/components/Sidebar.jsx
new file mode 100644
index 0000000..8841b06
--- /dev/null
+++ b/frontend/components/Sidebar.jsx
@@ -0,0 +1,89 @@
+import { useEffect, useState } from "react";
+import { useChatStore } from "../src/store/useChatStore";
+// import { useAuthStore } from "../src/store/useAuthStore";
+import { LuUsers } from "react-icons/lu";
+import { useAuthStore } from "../src/store/useAuthStore";
+
+function Sidebar() {
+ const { getUsers, users, currentUser, setCurrentUser, 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 (Loading...
);
+ return (
+
+
+
+
+ Users
+
+
+
+ setShowOnlineOnly(e.target.checked)}
+ className="checkbox checkbox-sm"
+ />
+ Show Currently Online
+
+
+ ({onlineUsers.length - 1} online)
+
+
+
+
+
+ {isUsersLoading ? (
+
Loading...
+ ) : (
+ filteredUsers.map((user) => (
+
setCurrentUser(user)}
+ className={`w-full p-3 flex items-center gap-3 hover:bg-base-300 transition-colors ${
+ currentUser?._id === user._id
+ ? "bg-base-300 ring-1 ring-base-300"
+ : ""
+ }`}>
+
+
+ {onlineUsers.includes(user._id) && (
+
+ )}
+
+
+ {/* User info - only visible on larger screens */}
+
+
{user.fullName}
+
+ {onlineUsers.includes(user._id) ? "Online" : "Offline"}
+
+
+
+ ))
+ )}
+
+ {filteredUsers?.length === 0 && !isUsersLoading && (
+
No User Online
+ )}
+
+
+ );
+}
+
+export default Sidebar;
diff --git a/frontend/components/TaskManager.jsx b/frontend/components/TaskManager.jsx
new file mode 100644
index 0000000..7b7d0e0
--- /dev/null
+++ b/frontend/components/TaskManager.jsx
@@ -0,0 +1,164 @@
+import { useState, useEffect } from "react";
+import useTaskStore from "../src/store/useTaskStore.js";
+import { useTheme } from "../context/ThemeContext";
+
+export default function TaskManager() {
+ const { theme } = useTheme();
+ const {
+ tasks,
+ fetchTasks,
+ handleCreate,
+ handleUpdate,
+ handleDelete,
+ isLoading,
+ error,
+ } = useTaskStore();
+
+ const [taskData, setTaskData] = useState({
+ title: "",
+ description: "",
+ due_date: "",
+ });
+ const [editingId, setEditingId] = useState(null);
+
+ useEffect(() => {
+ fetchTasks();
+ }, [fetchTasks]);
+
+ const handleEdit = (task) => {
+ setEditingId(task._id);
+ setTaskData({
+ title: task.title,
+ description: task.description,
+ due_date: task.due_date ? task.due_date.split("T")[0] : "", // Extract "YYYY-MM-DD"
+ });
+ };
+
+ const handleCancelEdit = () => {
+ setEditingId(null);
+ setTaskData({ title: "", description: "", due_date: "" });
+ };
+
+ const handleSubmit = async () => {
+ if (editingId) {
+ await handleUpdate(editingId, taskData);
+ } else {
+ await handleCreate(taskData);
+ }
+ handleCancelEdit();
+ };
+
+ return (
+
+
+
+
+ {editingId ? "Edit Task" : "Task Manager"}
+
+ {error &&
{error}
}
+
+
Title
+
+ setTaskData({ ...taskData, title: e.target.value })
+ }
+ className="input input-bordered w-full"
+ />
+
Due Date
+
+ setTaskData({ ...taskData, due_date: e.target.value })
+ }
+ className="input input-bordered w-full"
+ />
+
Description
+
+ setTaskData({ ...taskData, description: e.target.value })
+ }
+ className="textarea textarea-bordered w-full">
+
+
+ {editingId ? "Update Task" : "Add Task"}
+
+ {editingId && (
+
+ Cancel
+
+ )}
+
+
+
+
+
+ {isLoading && (
+
Loading tasks...
+ )}
+
+
+ {tasks.length > 0
+ ? tasks.map((task) => (
+
+
+
{task.title}
+
+ {task.description}
+
+
+ Due:{" "}
+ {task.due_date
+ ? task.due_date.split("T")[0]
+ : "No date set"}
+
+
+ handleEdit(task)}>
+ Edit
+
+ handleDelete(task._id)}>
+ Delete
+
+
+
+
+ ))
+ : !isLoading && (
+
+ No tasks found. Add a new task to get started!
+
+ )}
+
+
+ );
+}
diff --git a/frontend/components/TextAnimate.jsx b/frontend/components/TextAnimate.jsx
new file mode 100644
index 0000000..4b84b1e
--- /dev/null
+++ b/frontend/components/TextAnimate.jsx
@@ -0,0 +1,53 @@
+import { useRef } from "react";
+import PropTypes from "prop-types";
+import { motion, useInView } from "framer-motion";
+
+const animationVariants = {
+ fadeIn: {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { staggerChildren: 0.05 } },
+ },
+ fadeInUp: {
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
+ },
+ popIn: {
+ hidden: { scale: 0 },
+ visible: {
+ scale: 1,
+ transition: { type: "spring", damping: 15, stiffness: 400 },
+ },
+ },
+};
+
+const TextAnimate = ({ text, type = "fadeInUp", ...props }) => {
+ const ref = useRef(null);
+ const isInView = useInView(ref, { once: true });
+ const { hidden, visible } =
+ animationVariants[type] || animationVariants.fadeIn;
+
+ return (
+
+ {text.split(" ").map((word, index) => (
+
+ {word}
+
+ ))}
+
+ );
+};
+TextAnimate.propTypes = {
+ text: PropTypes.string.isRequired,
+ type: PropTypes.oneOf(["fadeIn", "fadeInUp", "popIn"]),
+};
+
+export { TextAnimate };
diff --git a/frontend/components/ThemeSwitcher.jsx b/frontend/components/ThemeSwitcher.jsx
new file mode 100644
index 0000000..42978c0
--- /dev/null
+++ b/frontend/components/ThemeSwitcher.jsx
@@ -0,0 +1,74 @@
+import { useTheme } from "../context/ThemeContext"; // Import the custom hook
+import { FaMoon, FaSun } from "react-icons/fa";
+
+const ThemeSwitcher = () => {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+ <>
+
+
+
+
+ {theme === "light" ? : }
+
+
+ >
+ );
+};
+
+export default ThemeSwitcher;
\ No newline at end of file
diff --git a/frontend/context/ModalContext.jsx b/frontend/context/ModalContext.jsx
index 404d969..1b95c40 100644
--- a/frontend/context/ModalContext.jsx
+++ b/frontend/context/ModalContext.jsx
@@ -1,4 +1,4 @@
-import React, { createContext, useState, useContext } from 'react';
+import { createContext, useState, useContext } from 'react';
const ModalContext = createContext();
diff --git a/frontend/context/TaskContext.jsx b/frontend/context/TaskContext.jsx
new file mode 100644
index 0000000..acc0e87
--- /dev/null
+++ b/frontend/context/TaskContext.jsx
@@ -0,0 +1,98 @@
+import { createContext, useState, useContext, useEffect } from "react";
+import { createTask, getUserTasks, updateTask, deleteTask } from "./apitask"; // Import API functions
+
+// Create Context
+const TaskContext = createContext();
+
+// Custom hook to use TaskContext
+export const useTasks = () => {
+ return useContext(TaskContext);
+};
+
+// Function to validate MongoDB ObjectId
+const isValidObjectId = (id) => /^[a-f\d]{24}$/i.test(id);
+
+
+// TaskProvider Component
+export const TaskProvider = ({ children }) => {
+ const [tasks, setTasks] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Fetch tasks from API
+ const fetchTasks = async () => {
+ setIsLoading(true);
+ try {
+ const response = await getUserTasks();
+ setTasks(response.data.data); // Assuming response.data.data holds the tasks array
+ } catch (err) {
+ setError(err.response?.data?.message || "Error fetching tasks");
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Add a new task
+ const handleCreate = async (taskData) => {
+ setIsLoading(true);
+ try {
+ await createTask(taskData);
+ fetchTasks(); // Refresh task list
+ } catch (err) {
+ setError(err.response?.data?.message || "Error creating task");
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Update an existing task
+ const handleUpdate = async (taskId, taskData) => {
+ if (!isValidObjectId(taskId)) {
+ setError("Invalid Task ID");
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await updateTask(taskId, taskData);
+ fetchTasks(); // Refresh task list
+ } catch (err) {
+ setError(err.response?.data?.message || "Error updating task");
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Delete a task
+ const handleDelete = async (taskId) => {
+ if (!isValidObjectId(taskId)) {
+ setError("Invalid Task ID");
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await deleteTask(taskId);
+ fetchTasks(); // Refresh task list
+ } catch (err) {
+ setError(err.response?.data?.message || "Error deleting task");
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Load tasks when component mounts
+ useEffect(() => {
+ fetchTasks();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/context/ThemeContext.jsx b/frontend/context/ThemeContext.jsx
new file mode 100644
index 0000000..74a891e
--- /dev/null
+++ b/frontend/context/ThemeContext.jsx
@@ -0,0 +1,47 @@
+import { createContext, useContext, useEffect, useState } from "react";
+import PropTypes from "prop-types";
+
+// Create a context
+const ThemeContext = createContext();
+
+// Custom hook to use the theme context
+export const useTheme = () => useContext(ThemeContext);
+
+// Theme provider component
+export function ThemeProvider({ children }) {
+ const storedTheme = localStorage.getItem("theme") || "light";
+ const [theme, setTheme] = useState(storedTheme);
+
+ useEffect(() => {
+ document.documentElement.setAttribute("data-theme", theme);
+ localStorage.setItem("theme", theme);
+ }, [theme]);
+
+ // Toggle theme function
+ const toggleTheme = () => {
+ setTheme(theme === "light" ? "dark" : "light");
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+ThemeProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+// Dark Mode Page Component
+export default function DarkModePage() {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+
+ Switch to {theme === "light" ? "Dark" : "Light"} Mode
+
+
+ );
+}
diff --git a/frontend/context/apitask.jsx b/frontend/context/apitask.jsx
new file mode 100644
index 0000000..c41080f
--- /dev/null
+++ b/frontend/context/apitask.jsx
@@ -0,0 +1,33 @@
+import axios from "axios";
+
+const API = axios.create({
+ baseURL: "http://localhost:8000/tasks",
+ withCredentials: true, // Required for cookies (refresh tokens)
+});
+
+API.interceptors.request.use((config) => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ console.log("Adding Authorization header:", token); // Log token for debugging
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ });
+
+
+
+
+// Create a new task
+export const createTask = (taskData) => API.post("/", taskData);
+
+// Get all tasks for the current user
+export const getUserTasks = () => API.get("/getTasks");
+
+// Get a specific task by ID
+export const getTaskById = (taskId) => API.get(`/${taskId}`);
+
+// Update a task
+export const updateTask = (taskId, taskData) => API.patch(`/${taskId}`, taskData);
+
+// Delete a task
+export const deleteTask = (taskId) => API.delete(`/${taskId}`);
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7e2d2d1..c79e271 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,26 +13,32 @@
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
- "@tailwindcss/vite": "^4.0.1",
+ "@fullcalendar/core": "^6.1.15",
+ "@fullcalendar/daygrid": "^6.1.15",
+ "@fullcalendar/react": "^6.1.15",
"axios": "^1.7.9",
"motion": "^12.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hot-toast": "^2.5.1",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.4",
- "tailwindcss": "^4.0.1"
+ "socket.io-client": "^4.8.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+ "@tailwindcss/vite": "^4.0.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
- "daisyui": "^4.12.23",
+ "daisyui": "^5.0.0-beta.6",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
+ "tailwindcss": "^4.0.3",
"vite": "^6.0.5"
}
},
@@ -339,6 +345,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -355,6 +362,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -371,6 +379,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -387,6 +396,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -403,6 +413,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -419,6 +430,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -435,6 +447,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -451,6 +464,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -467,6 +481,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -483,6 +498,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -499,6 +515,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -515,6 +532,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -531,6 +549,7 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -547,6 +566,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -563,6 +583,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -579,6 +600,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -595,6 +617,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -611,6 +634,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -627,6 +651,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -643,6 +668,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -659,6 +685,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -675,6 +702,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -691,6 +719,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -707,6 +736,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -723,6 +753,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -940,6 +971,35 @@
"react": ">=16.3"
}
},
+ "node_modules/@fullcalendar/core": {
+ "version": "6.1.15",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
+ "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "preact": "~10.12.1"
+ }
+ },
+ "node_modules/@fullcalendar/daygrid": {
+ "version": "6.1.15",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
+ "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@fullcalendar/core": "~6.1.15"
+ }
+ },
+ "node_modules/@fullcalendar/react": {
+ "version": "6.1.15",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.15.tgz",
+ "integrity": "sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@fullcalendar/core": "~6.1.15",
+ "react": "^16.7.0 || ^17 || ^18 || ^19",
+ "react-dom": "^16.7.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1066,6 +1126,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1079,6 +1140,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1092,6 +1154,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1105,6 +1168,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1118,6 +1182,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1131,6 +1196,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1144,6 +1210,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1157,6 +1224,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1170,6 +1238,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1183,6 +1252,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1196,6 +1266,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1209,6 +1280,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1222,6 +1294,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1235,6 +1308,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1248,6 +1322,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1261,6 +1336,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1274,6 +1350,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1287,6 +1364,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1300,52 +1378,62 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.1.tgz",
- "integrity": "sha512-lc+ly6PKHqgCVl7eO8D2JlV96Lks5bmL6pdtM6UasyUHLU2zmrOqU6jfgln120IVnCh3VC8GG/ca24xVTtSokw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.3.tgz",
+ "integrity": "sha512-QsVJokOl0pJ4AbJV33D2npvLcHGPWi5MOSZtrtE0GT3tSx+3D0JE2lokLA8yHS1x3oCY/3IyRyy7XX6tmzid7A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.0",
"jiti": "^2.4.2",
- "tailwindcss": "4.0.1"
+ "tailwindcss": "4.0.3"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.1.tgz",
- "integrity": "sha512-3z1SpWoDeaA6K6jd92CRrGyDghOcRILEgyWVHRhaUm/tcpiazwJpU9BSG0xB7GGGnl9capojaC+zme/nKsZd/w==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.3.tgz",
+ "integrity": "sha512-FFcp3VNvRjjmFA39ORM27g2mbflMQljhvM7gxBAujHxUy4LXlKa6yMF9wbHdTbPqTONiCyyOYxccvJyVyI/XBg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.0.1",
- "@tailwindcss/oxide-darwin-arm64": "4.0.1",
- "@tailwindcss/oxide-darwin-x64": "4.0.1",
- "@tailwindcss/oxide-freebsd-x64": "4.0.1",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.1",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.0.1",
- "@tailwindcss/oxide-linux-arm64-musl": "4.0.1",
- "@tailwindcss/oxide-linux-x64-gnu": "4.0.1",
- "@tailwindcss/oxide-linux-x64-musl": "4.0.1",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.0.1",
- "@tailwindcss/oxide-win32-x64-msvc": "4.0.1"
+ "@tailwindcss/oxide-android-arm64": "4.0.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.0.3",
+ "@tailwindcss/oxide-darwin-x64": "4.0.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.0.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.0.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.0.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.0.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.0.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.0.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.0.3"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.1.tgz",
- "integrity": "sha512-eP/rI9WaAElpeiiHDqGtDqga9iDsOClXxIqdHayHsw93F24F03b60CwgGhrGF9Io/EuWIpz3TMRhPVOLhoXivw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.3.tgz",
+ "integrity": "sha512-S8XOTQuMnpijZRlPm5HBzPJjZ28quB+40LSRHjRnQF6rRYKsvpr1qkY7dfwsetNdd+kMLOMDsvmuT8WnqqETvg==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1356,12 +1444,13 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.1.tgz",
- "integrity": "sha512-jZVUo0kNd1IjxdCYwg4dwegDNsq7PoUx4LM814RmgY3gfJ63Y6GlpJXHOpd5FLv1igpeZox5LzRk2oz8MQoJwQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.3.tgz",
+ "integrity": "sha512-smrY2DpzhXvgDhZtQlYAl8+vxJ04lv2/64C1eiRxvsRT2nkw/q+zA1/eAYKvUHat6cIuwqDku3QucmrUT6pCeg==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1372,12 +1461,13 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.1.tgz",
- "integrity": "sha512-E31wHiIf4LB0aKRohrS4U6XfFSACCL9ifUFfPQ16FhcBIL4wU5rcBidvWvT9TQFGPkpE69n5dyXUcqiMrnF/Ig==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.3.tgz",
+ "integrity": "sha512-NTz8x/LcGUjpZAWUxz0ZuzHao90Wj9spoQgomwB+/hgceh5gcJDfvaBYqxLFpKzVglpnbDSq1Fg0p0zI4oa5Pg==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1388,12 +1478,13 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.1.tgz",
- "integrity": "sha512-8/3ZKLMYqgAsBzTeczOKWtT4geF02g9S7cntY5gvqQZ4E0ImX724cHcZJi9k6fkE6aLbvwxxHxaShFvRxblwKQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.3.tgz",
+ "integrity": "sha512-yQc9Q0JCOp3kkAV8gKgDctXO60IkQhHpqGB+KgOccDtD5UmN6Q5+gd+lcsDyQ7N8dRuK1fAud51xQpZJgKfm7g==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1404,12 +1495,13 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.1.tgz",
- "integrity": "sha512-EYjbh225klQfWzy6LeIAfdjHCK+p71yLV/GjdPNW47Bfkkq05fTzIhHhCgshUvNp78EIA33iQU+ktWpW06NgHw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.3.tgz",
+ "integrity": "sha512-e1ivVMLSnxTOU1O3npnxN16FEyWM/g3SuH2pP6udxXwa0/SnSAijRwcAYRpqIlhVKujr158S8UeHxQjC4fGl4w==",
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1420,12 +1512,13 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.1.tgz",
- "integrity": "sha512-PrX2SwIqWNP5cYeSyQfrhbk4ffOM338T6CrEwIAGvLPoUZiklt19yknlsBme6bReSw7TSAMy+8KFdLLi5fcWNQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.3.tgz",
+ "integrity": "sha512-PLrToqQqX6sdJ9DmMi8IxZWWrfjc9pdi9AEEPTrtMts3Jm9HBi1WqEeF1VwZZ2aW9TXloE5OwA35zuuq1Bhb/Q==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1436,12 +1529,13 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.1.tgz",
- "integrity": "sha512-iuoFGhKDojtfloi5uj6MIk4kxEOGcsAk/kPbZItF9Dp7TnzVhxo2U/718tXhxGrg6jSL3ST3cQHIjA6yw3OeXw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.3.tgz",
+ "integrity": "sha512-YlzRxx7N1ampfgSKzEDw0iwDkJXUInR4cgNEqmR4TzHkU2Vhg59CGPJrTI7dxOBofD8+O35R13Nk9Ytyv0JUFg==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1452,12 +1546,13 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.1.tgz",
- "integrity": "sha512-pNUrGQYyE8RK+N9yvkPmHnlKDfFbni9A3lsi37u4RoA/6Yn+zWVoegvAQMZu3w+jqnpb2A/bYJ+LumcclUZ3yg==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.3.tgz",
+ "integrity": "sha512-Xfc3z/li6XkuD7Hs+Uk6pjyCXnfnd9zuQTKOyDTZJ544xc2yoMKUkuDw6Et9wb31MzU2/c0CIUpTDa71lL9KHw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1468,12 +1563,13 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.1.tgz",
- "integrity": "sha512-xSGWaDcT6SJ75su9zWXj8GYb2jM/przXwZGH96RTS7HGDIoI1tvgpls88YajG5Sx7hXaqAWCufjw5L/dlu+lzg==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.3.tgz",
+ "integrity": "sha512-ugKVqKzwa/cjmqSQG17aS9DYrEcQ/a5NITcgmOr3JLW4Iz64C37eoDlkC8tIepD3S/Td/ywKAolTQ8fKbjEL4g==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1484,12 +1580,13 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.1.tgz",
- "integrity": "sha512-BUNL2isUZ2yWnbplPddggJpZxsqGHPZ1RJAYpu63W4znUnKCzI4m/jiy0WpyYqqOKL9jDM5q0QdsQ9mc3aw5YQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.3.tgz",
+ "integrity": "sha512-qHPDMl+UUwsk1RMJMgAXvhraWqUUT+LR/tkXix5RA39UGxtTrHwsLIN1AhNxI5i2RFXAXfmFXDqZCdyQ4dWmAQ==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1500,12 +1597,13 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.1.tgz",
- "integrity": "sha512-ZtcVu+XXOddGsPlvO5nh2fnbKmwly2C07ZB1lcYCf/b8qIWF04QY9o6vy6/+6ioLRfbp3E7H/ipFio38DZX4oQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.3.tgz",
+ "integrity": "sha512-+ujwN4phBGyOsPyLgGgeCyUm4Mul+gqWVCIGuSXWgrx9xVUnf6LVXrw0BDBc9Aq1S2qMyOTX4OkCGbZeoIo8Qw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1516,15 +1614,16 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.1.tgz",
- "integrity": "sha512-ZkwMBA7uR+nyrafIZI8ce3PduE0dDVFVmxmInCUPTN17Jgy6RfEPXzqtL5fz658eDDxKa5xZ+gmiTt+5AMD0pw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.3.tgz",
+ "integrity": "sha512-Qj6rSO+EvXnNDymloKZ11D54JJTnDrkRWJBzNHENDxjt0HtrCZJbSLIrcJ/WdaoU4othrel/oFqHpO/doxIS/Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "^4.0.1",
- "@tailwindcss/oxide": "^4.0.1",
+ "@tailwindcss/node": "^4.0.3",
+ "@tailwindcss/oxide": "^4.0.3",
"lightningcss": "^1.29.1",
- "tailwindcss": "4.0.1"
+ "tailwindcss": "4.0.3"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
@@ -1585,6 +1684,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1598,14 +1698,14 @@
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -1995,16 +2095,6 @@
"node": ">=6"
}
},
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
@@ -2113,65 +2203,20 @@
"node": ">= 8"
}
},
- "node_modules/css-selector-tokenizer": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
- "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "fastparse": "^1.1.2"
- }
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
"license": "MIT"
},
- "node_modules/culori": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
- "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- }
- },
"node_modules/daisyui": {
- "version": "4.12.23",
- "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.23.tgz",
- "integrity": "sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ==",
+ "version": "5.0.0-beta.6",
+ "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.0-beta.6.tgz",
+ "integrity": "sha512-gwXHv6MApRBrvUayzg83vS6bfZ+y7/1VGLu0a8/cEAMviS4rXLCd4AndEdlVxhq+25wkAp0CZRkNQ7O4wIoFnQ==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "css-selector-tokenizer": "^0.8",
- "culori": "^3",
- "picocolors": "^1",
- "postcss-js": "^4"
- },
- "engines": {
- "node": ">=16.9.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/daisyui"
+ "url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/data-view-buffer": {
@@ -2302,6 +2347,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
@@ -2345,10 +2391,50 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/engine.io-client": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -2533,6 +2619,7 @@
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -2823,13 +2910,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fastparse": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
- "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -2962,6 +3042,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -3123,6 +3204,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3140,6 +3230,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/has-bigints": {
@@ -3692,6 +3783,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+ "dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -3807,6 +3899,7 @@
"version": "1.29.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
"integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==",
+ "dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^1.0.3"
@@ -3838,6 +3931,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3858,6 +3952,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3878,6 +3973,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3898,6 +3994,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3918,6 +4015,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3938,6 +4036,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3958,6 +4057,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3978,6 +4078,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3998,6 +4099,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4018,6 +4120,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -4165,13 +4268,13 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -4418,6 +4521,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
"license": "ISC"
},
"node_modules/possible-typed-array-names": {
@@ -4434,6 +4538,7 @@
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -4458,24 +4563,14 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/postcss-js": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
- "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
+ "node_modules/preact": {
+ "version": "10.12.1",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
+ "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
- "dependencies": {
- "camelcase-css": "^2.0.1"
- },
- "engines": {
- "node": "^12 || ^14 || >= 16"
- },
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- "peerDependencies": {
- "postcss": "^8.4.21"
+ "url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
@@ -4540,6 +4635,23 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz",
+ "integrity": "sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
@@ -4681,6 +4793,7 @@
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@@ -4943,10 +5056,73 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -5090,15 +5266,17 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.1.tgz",
- "integrity": "sha512-UK5Biiit/e+r3i0O223bisoS5+y7ZT1PM8Ojn0MxRHzXN1VPZ2KY6Lo6fhu1dOfCfyUAlK7Lt6wSxowRabATBw==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.3.tgz",
+ "integrity": "sha512-ImmZF0Lon5RrQpsEAKGxRvHwCvMgSC4XVlFRqmbzTEDb/3wvin9zfEZrMwgsa3yqBbPqahYcVI6lulM2S7IZAA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -5271,6 +5449,7 @@
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.24.2",
@@ -5452,6 +5631,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -5471,6 +5679,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+ "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index ab91bf0..88cc3ee 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,26 +15,32 @@
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
- "@tailwindcss/vite": "^4.0.1",
+ "@fullcalendar/core": "^6.1.15",
+ "@fullcalendar/daygrid": "^6.1.15",
+ "@fullcalendar/react": "^6.1.15",
"axios": "^1.7.9",
"motion": "^12.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hot-toast": "^2.5.1",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.4",
- "tailwindcss": "^4.0.1"
+ "socket.io-client": "^4.8.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+ "@tailwindcss/vite": "^4.0.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
- "daisyui": "^4.12.23",
+ "daisyui": "^5.0.0-beta.6",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
+ "tailwindcss": "^4.0.3",
"vite": "^6.0.5"
}
}
diff --git a/frontend/pages/Assignments.jsx b/frontend/pages/Assignments.jsx
new file mode 100644
index 0000000..d173efd
--- /dev/null
+++ b/frontend/pages/Assignments.jsx
@@ -0,0 +1,24 @@
+import { ThemeProvider } from "../context/ThemeContext";
+import DashHeader from "../components/DashHeader";
+import DashboardSidebar from "../components/DashboardSidebar";
+// import QuoteDisplay from "../components/QuoteDisplay";
+import AssignmentsManager from "../components/AssignmentsManager";
+// import { TaskProvider } from "../context/TaskContext";
+const Dashboard = () => {
+ return (
+
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/frontend/pages/Calendar.css b/frontend/pages/Calendar.css
new file mode 100644
index 0000000..1bba31b
--- /dev/null
+++ b/frontend/pages/Calendar.css
@@ -0,0 +1,50 @@
+.Calendar {
+ margin: 20px;
+ margin-left: 260px;
+}
+
+.Calendar-header {
+ font-size: 50px;
+ text-align: center;
+ margin: 0 20px 0 20px;
+}
+
+.event-tooltip {
+ padding: 10px;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ max-height: 150px;
+ word-wrap: break-word;
+}
+
+.event-time {
+ font-size: 12px;
+ color: #555;
+}
+
+.event-title {
+ font-weight: bold;
+ margin: 5px 0;
+}
+
+.event-description {
+ font-size: 14px;
+ color: #333;
+}
+
+.fc-daygrid-event {
+ font-size: 12px;
+ white-space: wrap;
+ /* overflow: hidden; */
+}
+
+.fc-daygrid-dot-event .fc-event-title {
+ font-weight: bold;
+ font-size: 12px;
+}
+
+.fc-daygrid-dot-event {
+ padding: 2px 5px;
+}
diff --git a/frontend/pages/Calendar.jsx b/frontend/pages/Calendar.jsx
new file mode 100644
index 0000000..cbfb1aa
--- /dev/null
+++ b/frontend/pages/Calendar.jsx
@@ -0,0 +1,98 @@
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import DashHeader from "../components/DashHeader";
+import DashboardSidebar from "../components/DashboardSidebar";
+import { ThemeProvider } from "../context/ThemeContext";
+import "./Calendar.css";
+import { axiosInstance } from "../src/lib/axios";
+import { useEffect, useState } from "react";
+// import toast from "react-hot-toast";
+// function renderEventContent(eventInfo) {
+// const eventDetails = eventInfo.event?._def;
+// return (
+//
+//
+//
{eventInfo.timeText}
+//
{eventDetails?.title}
+//
{eventDetails?.extendedProps.description}
+//
+//
+// );
+// }
+function Calendar() {
+ const [schedules, setSchedules] = useState([]);
+
+ useEffect(() => {
+ const fetchSchedules = async () => {
+ try {
+ const response = await axiosInstance.get("/schedules/getSchedules");
+
+ const fetchedSchedules = response.data.data.map((schedule) => ({
+ id: schedule._id,
+ title: schedule.message,
+ start: schedule.reminderDate,
+ description: schedule.assignmentId
+ ? schedule.assignmentId.title
+ : schedule.taskId
+ ? schedule.taskId.title
+ : "General Reminder",
+ allDay: true,
+ }));
+
+ setSchedules(fetchedSchedules);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ fetchSchedules();
+ }, []);
+
+ const handleEventClick = async (clickInfo) => {
+ const confirmDelete = window.confirm(
+ `Delete schedule: ${clickInfo.event.title}?`
+ );
+
+ if (confirmDelete) {
+ try {
+ await axiosInstance.delete(`/schedules/${clickInfo.event.id}`);
+ setSchedules((prevSchedules) =>
+ prevSchedules.filter((event) => event.id !== clickInfo.event.id)
+ );
+ } catch (error) {
+ console.error("Error deleting schedule:", error);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ {
+ info.el.style.whiteSpace = "nowrap";
+ info.el.style.overflow = "scroll";
+ info.el.style.textOverflow = "ellipsis";
+ info.el.style.fontSize = "12px";
+ }}
+ />
+
+
+
+ );
+}
+export default Calendar;
diff --git a/frontend/pages/Chat.jsx b/frontend/pages/Chat.jsx
new file mode 100644
index 0000000..6922ca8
--- /dev/null
+++ b/frontend/pages/Chat.jsx
@@ -0,0 +1,28 @@
+import { useChatStore } from "../src/store/useChatStore";
+import Sidebar from "../components/Sidebar";
+import NoChatSelected from "../components/NoChatSelected";
+import ChatContainer from "../components/ChatContainer";
+import Navbar from "../components/Navbar";
+
+const Chat = () => {
+ const { currentUser } = useChatStore();
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {!currentUser ? : }
+
+
+
+
+ >
+ );
+};
+
+export default Chat;
diff --git a/frontend/pages/ContactUs.jsx b/frontend/pages/ContactUs.jsx
index 8aaafa6..fc66684 100644
--- a/frontend/pages/ContactUs.jsx
+++ b/frontend/pages/ContactUs.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInstagram, faTwitter, faGithub } from "@fortawesome/free-brands-svg-icons";
diff --git a/frontend/pages/Dashboard.jsx b/frontend/pages/Dashboard.jsx
new file mode 100644
index 0000000..ee802b7
--- /dev/null
+++ b/frontend/pages/Dashboard.jsx
@@ -0,0 +1,27 @@
+// import { IoIosLogOut } from "react-icons/io";
+// import { FaChartBar, FaCalendarAlt, FaFacebookMessenger, FaUsersCog } from "react-icons/fa";
+// import ThemeSwitcher from '../components/ThemeSwitcher';
+import { ThemeProvider } from "../context/ThemeContext";
+import DashHeader from "../components/DashHeader";
+import DashboardSidebar from "../components/DashboardSidebar";
+// import QuoteDisplay from "../components/QuoteDisplay";
+import TaskManager from "../components/TaskManager";
+// import { TaskProvider } from "../context/TaskContext";
+const Dashboard = () => {
+ return (
+
+
+
+
+
+
+
+ {/* */}
+
+
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/frontend/pages/Home.jsx b/frontend/pages/Home.jsx
index f866ef7..168d0b2 100644
--- a/frontend/pages/Home.jsx
+++ b/frontend/pages/Home.jsx
@@ -1,110 +1,111 @@
-import React from 'react'
-import { useState } from 'react'
-import {motion} from "framer-motion"
-import { Link } from 'react-router-dom'
-import { useContext } from 'react'
-import './styles.css';
-import useScrollTo from '../hooks/useScrollTo'
-import Login from './Login'
-import Signup from './Signup'
-import useActiveForm from '../hooks/useActiveForm'
-import Modal from '../components/Modal'
-import { useModal } from '../context/ModalContext'
-import ContactUs from './ContactUs'
-import Photos from './Photos'
-import Layout1 from './Layout1'
-const Home = () => {
- const [isActive1, setIsActive1] = useState(false);
+import { useState } from "react";
+import { motion } from "framer-motion";
+import Login from "./Login";
+import Signup from "./Signup";
+import useActiveForm from "../hooks/useActiveForm";
+import Modal from "../components/Modal";
+import { useModal } from "../context/ModalContext";
+import ContactUs from "./ContactUs";
+import Photos from "./Photos";
+import Layout1 from "./Layout1";
+import { FaBars, FaArrowRight } from "react-icons/fa";
+import { FaRightToBracket } from "react-icons/fa6";
+import { HiOutlineBadgeCheck } from "react-icons/hi";
+import RollingQuotes from "../components/RollingQuotes";
- const scrollTo = useScrollTo();
+const Home = () => {
const { isModalOpen, toggleModal } = useModal();
const [isSignUp, toggleForm] = useActiveForm();
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
- const toggleActiveClass = () => {
- setIsActive1(!isActive1);
- };
-
- const removeActive = () => {
- setIsActive1(false);
+ const openGetStartedModal = () => {
+ toggleForm(true);
+ toggleModal();
};
- const openGetStartedModal = () => {
+ const openLoginModal = () => {
+ toggleForm(false);
toggleModal();
};
return (
-
- {/* Starry Background */}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Navbar */}
-
-
-
-
-
+
+ {/* Navbar */}
+
+ {/* Top navbar */}
+
+ {/* Logo */}
+
+
+
+ {/* Menu (Desktop) */}
+
- {/* Navigation Menu */}
-
- scrollTo('top')}>
-
- Home
-
-
-
-
- Get Started
-
-
- scrollTo('contact-us')}>
-
- Contact Us
-
-
-
+ {/* Menu (Mobile) */}
+
+
setIsDropdownOpen(!isDropdownOpen)}>
+
+
- {/* Hamburger Icon for smaller screens */}
-
-
-
-
-
-
-
+ {isDropdownOpen && (
+
+ )}
+
+
-
-
-
{/* Modal Component */}
{isModalOpen && (
{/* Sign Up Form */}
-
- {isSignUp &&
}
+
+ {isSignUp && }
{/* Sign In Form */}
-
- {!isSignUp &&
}
+
+ {!isSignUp && }
{/* Toggle Panel */}
-
{isSignUp ? "Hello, User!" : "Welcome User!"}
-
{isSignUp ? "If you already have an account" : "If you don't have an account"}
-
+
+ {isSignUp ? "Hello, User!" : "Welcome User!"}
+
+
+ {isSignUp
+ ? "If you already have an account"
+ : "If you don't have an account"}
+
+
{isSignUp ? "Sign In" : "Sign Up"}
@@ -112,10 +113,10 @@ const Home = () => {
)}
- {/* Main Container */}
-
+ {/* Main Container */}
+
{
whileHover={{
scale: 1.1,
color: "#c3bef0",
- boxShadow: "0 0 1rem #ffffff, inset 0 0 1rem rgb(255, 255, 255), 0 0 2rem #ffffff, inset 0 0 2rem rgb(255, 255, 255)",
- }}
- >
+ boxShadow:
+ "0 0 1rem #ffffff, inset 0 0 1rem rgb(255, 255, 255), 0 0 2rem #ffffff, inset 0 0 2rem rgb(255, 255, 255)",
+ }}>
ASSIGNIFY
-
- "The future belongs to those who learn more skills and combine them in creative ways." – Robert Greene
-
+
+
+ Get started
+
+
-
-
-
-
-
-
-
-
- {/* Contact Us Section */}
-
-
+
+
{
whileHover={{
scale: 1.1,
color: "#c3bef0",
- boxShadow: "0 0 1rem #ffffff, inset 0 0 1rem rgb(255, 255, 255), 0 0 2rem #ffffff, inset 0 0 2rem rgb(255, 255, 255)",
- }}
- >
+ boxShadow:
+ "0 0 1rem #ffffff, inset 0 0 1rem rgb(255, 255, 255), 0 0 2rem #ffffff, inset 0 0 2rem rgb(255, 255, 255)",
+ }}>
CONTACT US
-
-
-
- {/* Left Side: Contact Us */}
-
- {/* Right Side: Photo Content */}
@@ -184,6 +172,6 @@ const Home = () => {
);
-}
+};
-export default Home
+export default Home;
diff --git a/frontend/pages/Layout1.jsx b/frontend/pages/Layout1.jsx
index 9ebdd5b..327e3cd 100644
--- a/frontend/pages/Layout1.jsx
+++ b/frontend/pages/Layout1.jsx
@@ -67,15 +67,15 @@ export default function Gestures() {
* ============== Styles ================
*/
const box = {
- width: "100%", // Full width on small screens
- height: "300px", // Adjust height for small screens, but ensure it's enough to fill the screen
- maxWidth: "100%", // Ensure the width is responsive
+ width: "100%",
+ height: "300px",
+ maxWidth: "100%",
paddingLeft: "30px",
paddingRight: "30px",
backgroundColor: "#c3bef0",
borderRadius: 10,
border: "2px solid #ffffff",
boxShadow: "0 0 2rem #ffffff, inset 0 0 2rem rgb(255, 255, 255)",
- backgroundSize: "cover", // Ensures the background image covers the div properly
- backgroundPosition: "center", // Centers the background image
+ backgroundSize: "cover",
+ backgroundPosition: "center",
};
diff --git a/frontend/pages/Login.jsx b/frontend/pages/Login.jsx
index 21d995c..8190b9d 100644
--- a/frontend/pages/Login.jsx
+++ b/frontend/pages/Login.jsx
@@ -1,101 +1,63 @@
-import React, { useState, useContext } from 'react';
-import { useNavigate } from 'react-router-dom';
-import UserContext from '../context/UserContext';
-import axios from 'axios'
+import { useState } from "react";
+import { useAuthStore } from "../src/store/useAuthStore";
+import { LuEye, LuEyeOff } from "react-icons/lu";
const Login = () => {
// State variables for email and password
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
-
- // Hook for navigation
- const navigate = useNavigate();
-
- // Accessing setUser function from UserContext
- const { setUser } = useContext(UserContext);
+ const [showPassword, setShowPassword] = useState(false);
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+ const { login, isLoggingIn } = useAuthStore();
- // Handler for form submission
- const submitHandler = async (e) => {
- e.preventDefault(); // Prevent default form submission behavior
-
- // User data to be sent to the server
- const userData = {
- email: email,
- password: password,
- };
-
- try {
- // Sending a POST request to the login endpoint
- const response = await axios.post(`${import.meta.env.VITE_BASE_URL}/login`, userData);
-
- // If login is successful
- if (response.status === 200) {
- const data = response.data;
-
- // Setting the user data in context
- setUser(data.user);
-
- // Storing the token and user data in local storage
- localStorage.setItem('token', data.token);
- localStorage.setItem('user', JSON.stringify(data.user));
-
- // Navigating to the main page
- navigate('/main-page');
- }
- } catch (error) {
- // Logging the error to the console
- console.error('Error during login:', error);
-
- // Handling different types of errors
- if (error.response) {
- console.error('Error response:', error.response);
- const errorData = error.response.data;
- if (errorData) {
- const message = errorData.message || 'Something went wrong on the server.';
- alert(`Error: ${message}`);
- } else {
- alert('Something went wrong on the server.');
- }
- } else if (error.request) {
- alert('Network error. Please check your internet connection.');
- } else if (error.message) {
- alert(error.message);
- } else {
- alert('An error occurred while setting up the request.');
- }
- }
-
- // Clearing the form fields
- setEmail('');
- setPassword('');
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ await login(formData);
};
return (
-
- Sign In
- or use your email & password
+
+ Sign In
+ or use your email & password
+
setEmail(e.target.value)}
- />
- setPassword(e.target.value)}
+ className="input input-bordered w-72"
+ value={formData.email}
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
+
+ {/* Password Input with Toggle */}
+
+
+ setFormData({ ...formData, password: e.target.value })
+ }
+ />
+ setShowPassword(!showPassword)}>
+ {showPassword ? : }
+
+
+
- Sign In
+ className="btn btn-primary w-24"
+ disabled={isLoggingIn}>
+ {isLoggingIn ? "Signing In..." : "Sign In"}
);
};
-export default Login;
\ No newline at end of file
+export default Login;
diff --git a/frontend/pages/Profile.jsx b/frontend/pages/Profile.jsx
new file mode 100644
index 0000000..bdcf98e
--- /dev/null
+++ b/frontend/pages/Profile.jsx
@@ -0,0 +1,88 @@
+import { useAuthStore } from "../src/store/useAuthStore";
+import { ThemeProvider } from "../context/ThemeContext";
+import DashHeader from "../components/DashHeader";
+import DashboardSidebar from "../components/DashboardSidebar";
+import { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+import { motion } from "framer-motion";
+
+const Profile = () => {
+ const { authUser, logout, checkAuth, isCheckingAuth } = useAuthStore();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ checkAuth();
+ if (!authUser && !isCheckingAuth) {
+ toast.error("You need to be logged in to access this page");
+ navigate("/");
+ }
+ }, [authUser, checkAuth, isCheckingAuth, navigate]);
+
+ if (isCheckingAuth) {
+ return (
+
+ Checking authentication...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name:{" "}
+
+ {authUser?.fullName || "Unknown"}
+
+
+
+ Email: {authUser?.email || "Not provided"}
+
+
+ Username: {authUser?.username || "N/A"}
+
+
+
+
+ Logout
+
+
+
+
+
+
+
+ );
+};
+
+export default Profile;
diff --git a/frontend/pages/Signup.jsx b/frontend/pages/Signup.jsx
index a71cb07..d3f21a1 100644
--- a/frontend/pages/Signup.jsx
+++ b/frontend/pages/Signup.jsx
@@ -1,102 +1,116 @@
-import React, { useContext, useState } from 'react';
-import { Link,useNavigate } from 'react-router-dom';
-import axios from 'axios';
-import UserContext from '../context/UserContext';
-
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useAuthStore } from "../src/store/useAuthStore"; // Import Zustand store
+import { LuUpload, LuEyeOff, LuEye, LuCheck } from "react-icons/lu";
+import toast from "react-hot-toast";
const Signup = () => {
- const[email,setEmail]=useState('');
- const [password,setPassword]=useState('');
- const[name,setName]=useState('');
- const[username,setUsername]=useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [formData, setFormData] = useState({
+ fullName: "",
+ username: "",
+ email: "",
+ password: "",
+ avatar: null,
+ });
+ const navigate = useNavigate();
+ const { signup, isSigningUp } = useAuthStore();
- const navigate=useNavigate();
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
- const{user,setUser}=useContext(UserContext);
+ const handleAvatarChange = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ setFormData((prev) => ({ ...prev, avatar: file }));
+ }
+ };
const submitHandler = async (e) => {
e.preventDefault();
- const newUser = {
- name: name,
- username:username,
- email:email,
- password:password,
- };
+ if (!formData.avatar) return toast("Please upload an avatar.");
- try{
- const response=await axios.post(`${import.meta.env.VITE_BASE_URL}/signup`)
- if (response.status === 201) {
- const data = response.data;
- setUser(data.user);
- localStorage.setItem('token',data.token)
- navigate('/main-page');
- }
- }catch(error){
- // Enhanced error handling
- console.error('Error during registration:', error); // Log entire error object
-
- if (error.response) {
- // If error.response exists, we can get details from the server's response
- console.error('Error response:', error.response); // Log the entire error response object
+ const form = new FormData();
+ Object.entries(formData).forEach(([key, value]) => {
+ form.append(key, value);
+ });
- const errorData = error.response.data;
- if (errorData) {
- const message = errorData.message || 'Something went wrong on the server.';
- alert(`Error: ${message}`);
- } else {
- alert('Something went wrong on the server.');
- }
- } else if (error.request) {
- alert('Network error. Please check your internet connection.');
- } else if (error.message) {
- alert(error.message);
- } else {
- alert('An error occurred while setting up the request.');
+ try {
+ await signup(form);
+ navigate("/dashboard");
+ } catch (error) {
+ console.error("Signup error:", error);
}
+ };
- setEmail('');
- setName('');
- setPassword('');
- setUsername('');
-
- }
-};
return (
-
- {/* Sign Up Form */}
- Create Account
- setEmail(e.target.value)}
- />
- setUsername(e.target.value)}
- />
- setEmail(e.target.value)}
+
+ Create Account
+
- setPassword(e.target.value)}
- />
-
- Sign Up
-
-
+
+
+
+
+
+ setShowPassword(!showPassword)}>
+ {showPassword ? : }
+
+
+
+
+ {formData.avatar ? (
+
+ ) : (
+
+ )}
+ {formData.avatar ? formData.avatar.name : "Upload Avatar"}
+
+
+
+
+ {isSigningUp ? "Signing Up..." : "Sign Up"}
+
+
);
};
-export default Signup;
\ No newline at end of file
+export default Signup;
diff --git a/frontend/pages/styles.css b/frontend/pages/styles.css
index 96e03ed..e69de29 100644
--- a/frontend/pages/styles.css
+++ b/frontend/pages/styles.css
@@ -1,153 +0,0 @@
-
-
-*{
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body{
- overflow: hidden;
- background-color: black;
- }
- star{
- z-index: 0;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100vh;
- background-position-x: center;
- background-size: cover;
- animation: animateBg 50s linear infinite;
- }
- @keyframes animateBg {
- 0%,100%
- {
- transform:scale(1);
- }
- 50%
- {
- transform:scale(1.2);
- }
- }
- .star span{
- position: absolute;
-
- width: 4px;
- height: 4px;
- background: #fff;
- border-radius: 50%;
- box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.1), 0 0 0 8px rgba(255, 255, 255, 0.1), 0 0 20px rgba(255, 255, 255, 1) ;
- animation: animate 3s linear infinite;
- }
- .star span::before{
- content: '';
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- width: 300px;
- height: 1px;
- background: linear-gradient(90deg, #fff, transparent);
- }
- @keyframes animate {
- 0%
- {
- transform: rotate(270deg) translateX(0);
- opacity: 1;
- }
- 70%
- {
- opacity: 1;
-
- }
- 100%
- {
- transform: rotate(270deg) translateX(-1500px);
- opacity: 0;
-
- }
-
- }
-.star span:nth-child(1){
- top: 0;
- right: 0;
- left:initial;
- animation-delay:0 ;
- animation-duration: 1s;
- }
-
- .star span:nth-child(2){
- top: 0;
- left:0;
- left:initial;
- animation-delay:2.0s;
- animation-duration: 5s;
- }
-
- .star span:nth-child(3){
- top: 80px;
- right: 0px;
- left:initial;
- animation-delay:2.0s ;
- animation-duration: 5ss;
- }
-
-.star span:nth-child(4){
- top: 0;
- right: 180px;
- left:initial;
- animation-delay:2s;
- animation-duration: 5s;
- }
-
- .star span:nth-child(5){
- top: 0;
- right: 400px;
- left:initial;
- animation-delay:1.4s;
- animation-duration: 5s;
- }
-
- .star span:nth-child(6){
- top: 0;
- right: 600px;
- left:initial;
- animation-delay:1.6s ;
- animation-duration: 5s;
- }
- .star span:nth-child(7 ){
- top: 300px;
- right: 0px;
- left:initial;
- animation-delay:2.4s ;
- animation-duration: 5s;
- }
-
- .star span:nth-child(8){
- top: 0px;
- right: 700px;
- left:initial;
- animation-delay:1.4s ;
- animation-duration: 1.25s;
- }
-
- .star span:nth-child(9){
- top: 0px;
- right: 1000px;
- left:initial;
- animation-delay:0.75s ;
- animation-duration: 2.25s;
- }
-
- span:nth-child(10){
- top: 0px;
- right: 1000px;
- left:initial;
- animation-delay:2.75s ;
- animation-duration: 2.25s;
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/pages/theme.css b/frontend/pages/theme.css
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/App.css b/frontend/src/App.css
index b9d355d..e69de29 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e3e6a60..87a44c9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,21 +1,75 @@
-import React from 'react'
-import { BrowserRouter as Router,Route,Routes } from 'react-router-dom'
-import { useState } from 'react'
-import './App.css'
-import Home from '../pages/Home'
-import Login from '../pages/Login'
-import Signup from '../pages/Signup'
-function App() {
+import { useEffect } from "react";
+import { Route, Routes, Navigate } from "react-router-dom";
+import "./App.css";
+import Home from "../pages/Home";
+import Login from "../pages/Login";
+import Signup from "../pages/Signup";
+import Chat from "../pages/Chat";
+import Dashboard from "../pages/Dashboard";
+import Assignments from "../pages/Assignments";
+import Calendar from "../pages/Calendar";
+import { useAuthStore } from "./store/useAuthStore";
+import { Toaster } from "react-hot-toast";
+import Profile from "../pages/Profile";
- return (
+const App = () => {
+ const { authUser, checkAuth, isCheckingAuth } = useAuthStore();
+
+ useEffect(() => {
+ checkAuth();
+ }, [checkAuth]);
+
+ if (isCheckingAuth && !authUser) {
+ return (
+
+
+
Loading...
+
+ );
+ }
+ return (
+ <>
- }/>
- } />
- } />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ : }
+ />
+ : }
+ />
-
- )
-}
-export default App
+
+ >
+ );
+};
+
+export default App;
diff --git a/frontend/src/index.css b/frontend/src/index.css
index f1d8c73..7c18ad7 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1 +1,3 @@
+@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap');
@import "tailwindcss";
+@plugin "daisyui";
diff --git a/frontend/src/lib/axios.js b/frontend/src/lib/axios.js
new file mode 100644
index 0000000..5ac4df7
--- /dev/null
+++ b/frontend/src/lib/axios.js
@@ -0,0 +1,6 @@
+import axios from "axios";
+
+export const axiosInstance = axios.create({
+ baseURL: "http://localhost:8000",
+ withCredentials: true,
+});
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index c4cf566..15c723e 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,19 +1,15 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
-import ModalProvider from '../context/ModalContext.jsx'
-import { UserProvider } from '../context/UserContext.jsx'
-import { BrowserRouter } from 'react-router-dom'
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.jsx";
+import ModalProvider from "../context/ModalContext.jsx";
+import { BrowserRouter } from "react-router-dom";
-createRoot(document.getElementById('root')).render(
-
+createRoot(document.getElementById("root")).render(
-
-
-
-
-
+ {/* */}
+
+
+
+ {/* */}
- ,
-)
+);
diff --git a/frontend/src/store/useAssignmentStore.js b/frontend/src/store/useAssignmentStore.js
new file mode 100644
index 0000000..176addc
--- /dev/null
+++ b/frontend/src/store/useAssignmentStore.js
@@ -0,0 +1,76 @@
+import { create } from "zustand";
+import { axiosInstance } from "../lib/axios";
+import toast from "react-hot-toast";
+
+const isValidObjectId = (id) => /^[a-f\d]{24}$/i.test(id);
+
+export const useAssignmentStore = create((set) => ({
+ assignments: [],
+ isLoading: false,
+ error: null,
+
+ fetchAssignments: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await axiosInstance.get("/assignments/getAssignments");
+ set({ assignments: response.data.data, isLoading: false });
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error fetching assignments";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ createAssignment: async (formData) => {
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.post("/assignments", formData);
+ toast.success("Assignment created successfully");
+ await useAssignmentStore.getState().fetchAssignments();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error creating assignment";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ updateAssignment: async (assignmentId, formData) => {
+ if (!isValidObjectId(assignmentId)) {
+ const errorMessage = "Invalid Assignment ID";
+ set({ error: errorMessage });
+ toast.error(errorMessage);
+ return;
+ }
+
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.patch(`/assignments/${assignmentId}`, formData);
+ toast.success("Assignment updated successfully");
+ await useAssignmentStore.getState().fetchAssignments();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error updating assignment";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ deleteAssignment: async (assignmentId) => {
+ if (!isValidObjectId(assignmentId)) {
+ const errorMessage = "Invalid Assignment ID";
+ set({ error: errorMessage });
+ toast.error(errorMessage);
+ return;
+ }
+
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.delete(`/assignments/${assignmentId}`);
+ toast.success("Assignment deleted successfully");
+ await useAssignmentStore.getState().fetchAssignments();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error deleting assignment";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+}));
diff --git a/frontend/src/store/useAuthStore.js b/frontend/src/store/useAuthStore.js
new file mode 100644
index 0000000..31b2e0c
--- /dev/null
+++ b/frontend/src/store/useAuthStore.js
@@ -0,0 +1,89 @@
+import { create } from "zustand";
+import { io } from "socket.io-client";
+import toast from "react-hot-toast";
+import { axiosInstance } from "../lib/axios.js";
+
+// const BASE_URL =
+// import.meta.env.MODE === "development" ? "http://localhost:8000" : "/";
+
+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(
+ "http://localhost:8000/users/current-user"
+ );
+
+ set({ authUser: res.data.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("/users/register", data);
+ set({ authUser: res.data });
+ toast.success("Account created...You're now a User");
+ 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("/users/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("/users/logout");
+ set({ authUser: null });
+ toast.success("Logged out successfully");
+ get().disconnectSocket();
+ } catch (error) {
+ toast.error(error.response.data.message);
+ }
+ },
+
+ connectSocket: () => {
+ const { authUser, socket } = get();
+ if (!authUser || socket?.connected) return;
+
+ const newSocket = io("http://localhost:8000", {
+ query: { userId: authUser._id },
+ }).connect();
+
+ set({ socket: newSocket });
+
+ newSocket.on("getOnlineUsers", (ids) => set({ onlineUsers: ids }));
+ },
+ disconnectSocket: () => {
+ if (get().socket?.connected) get().socket.disconnect();
+ },
+}));
diff --git a/frontend/src/store/useChatStore.js b/frontend/src/store/useChatStore.js
new file mode 100644
index 0000000..2424250
--- /dev/null
+++ b/frontend/src/store/useChatStore.js
@@ -0,0 +1,75 @@
+import { create } from "zustand";
+import toast from "react-hot-toast";
+import { useAuthStore } from "./useAuthStore";
+import { axiosInstance } from "../lib/axios";
+
+// Created a basic zustand store for chats
+export const useChatStore = create((set, get) => ({
+ messages: [],
+ users: [],
+ currentUser: null, // Initially
+ isUsersLoading: false,
+ isMessagesLoading: false,
+
+ getUsers: async () => {
+ set({ isUsersLoading: true });
+ try {
+ const usersFromBackend = await axiosInstance.get("/messages/users");
+ set({ users: usersFromBackend.data.data });
+ } catch (error) {
+ toast.error(error.response.data.message);
+ } finally {
+ set({ isUsersLoading: false });
+ }
+ },
+
+ getMessages: async (userId) => {
+ set({ isMessagesLoading: true });
+ try {
+ const messagesFromBackend = await axiosInstance.get(
+ `/messages/${userId}`
+ );
+ // console.log("Here are messages: ", messagesFromBackend.data.data);
+ set({ messages: messagesFromBackend.data.data });
+ } catch (error) {
+ toast.error(error.response.data.message);
+ } finally {
+ set({ isMessagesLoading: false });
+ }
+ },
+
+ sendMessage: async (messageData) => {
+ const { currentUser, messages } = get();
+ // console.log("yoyoyoy", currentUser);
+ try {
+ const res = await axiosInstance.post(
+ `/messages/send/${currentUser._id}`,
+ messageData
+ );
+ set({ messages: [...messages, res.data.data] });
+ } catch (error) {
+ toast.error(error.response.data.message);
+ }
+ },
+
+ updateOnRealtime: () => {
+ const { currentUser } = get();
+ if (!currentUser) return;
+
+ const socket = useAuthStore.getState().socket;
+
+ socket.on("newMessage", (newMessage) => {
+ if(newMessage.senderId !== currentUser._id) return;
+ set((state) => ({
+ messages: [...state.messages, newMessage],
+ }));
+ });
+ },
+
+ removeUpdateOnRealtime: () => {
+ const socket = useAuthStore.getState().socket;
+ socket.off("newMessage");
+ },
+
+ setCurrentUser: (currentUser) => set({ currentUser }),
+}));
diff --git a/frontend/src/store/useTaskStore.js b/frontend/src/store/useTaskStore.js
new file mode 100644
index 0000000..aca4c8a
--- /dev/null
+++ b/frontend/src/store/useTaskStore.js
@@ -0,0 +1,75 @@
+import { create } from "zustand";
+import { axiosInstance } from "../lib/axios";
+import toast from "react-hot-toast";
+
+const useTaskStore = create((set) => ({
+ tasks: [],
+ isLoading: false,
+ error: null,
+
+ fetchTasks: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await axiosInstance.get("/tasks/getTasks");
+ set({ tasks: response.data.data, isLoading: false });
+ } catch (err) {
+ const errorMessage =
+ err.response?.data?.message || "Error fetching tasks";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ handleCreate: async (taskData) => {
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.post("/tasks", taskData);
+ toast.success("Task created successfully");
+ await useTaskStore.getState().fetchTasks();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error creating task";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ handleUpdate: async (taskId, taskData) => {
+ if (!/^[a-f\d]{24}$/i.test(taskId)) {
+ const errorMessage = "Invalid Task ID";
+ set({ error: errorMessage });
+ toast.error(errorMessage);
+ return;
+ }
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.patch(`/tasks/${taskId}`, taskData);
+ toast.success("Task updated successfully");
+ await useTaskStore.getState().fetchTasks();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error updating task";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+
+ handleDelete: async (taskId) => {
+ if (!/^[a-f\d]{24}$/i.test(taskId)) {
+ const errorMessage = "Invalid Task ID";
+ set({ error: errorMessage });
+ toast.error(errorMessage);
+ return;
+ }
+ set({ isLoading: true, error: null });
+ try {
+ await axiosInstance.delete(`/tasks/${taskId}`);
+ toast.success("Task deleted successfully");
+ await useTaskStore.getState().fetchTasks();
+ } catch (err) {
+ const errorMessage = err.response?.data?.message || "Error deleting task";
+ set({ error: errorMessage, isLoading: false });
+ toast.error(errorMessage);
+ }
+ },
+}));
+
+export default useTaskStore;
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 1e13fd8..41f3a96 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -1,13 +1,17 @@
-
-import daisyui from 'daisyui';
-
+import daisyui from "daisyui";
export default {
- content: [
- "./src/**/*.{html,js,ts,jsx,tsx}",
- ],
+ content: ["./src/**/*.{html,js,ts,jsx,tsx}"],
theme: {
- extend: {},
+ extend: {
+ fontFamily: {
+ quicksand: ["Quicksand", "serif"],
+ },
+ },
+ },
+ plugins: [daisyui],
+ daisyui: {
+ themes: true,
+ darkTheme: "night",
},
- plugins: [daisyui],
};
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 3e96bf6..c72c0ae 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -1,7 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
-import daisyui from 'daisyui'
// https://vite.dev/config/
export default defineConfig({