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}

} + +
+ + + setAssignmentData({ ...assignmentData, title: e.target.value }) + } + className="input input-bordered w-full" + /> + + + + setAssignmentData({ + ...assignmentData, + due_date: e.target.value, + }) + } + className="input input-bordered w-full" + /> + + + + + +
+ +
+ + {assignmentData.docs?.length > 0 && ( +
+ {assignmentData.docs.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+ )} + +
+ + {editingId && ( + + )} +
+
+
+
+ + {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) => ( + + + + ))} +
+
+ + +
+
+
+ ))} +
+
+ ); +} 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) => ( +
+
+
+ profile pic +
+
+
+ +
+
+

{message.text}

+
+
+ ))} +
+ + +
+ ); +} + +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 */} +
+
+ {currentUser.fullName} +
+
+ + {/* User info */} +
+

{currentUser.fullName}

+

+ {onlineUsers.includes(currentUser._id) ? "Online" : "Offline"} +

+
+
+ + {/* Close button */} + +
+
+ ); +}; +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 ( + + ); +}; + +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 ( +
+ +
+ ); +}; + +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 ( +
+
+
+ setMessageText(e.target.value)} + /> + {/* + */} +
+ +
+
+ ); +}; +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 + +
+ +
+
+
+ User Avatar +
+
+ +
+
+ ); +} + +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 */} + + + {/* Bottom navbar (mobile only) */} + +
+ ); +}; + +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 ( + + ); +} + +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}

} +
+ + + setTaskData({ ...taskData, title: e.target.value }) + } + className="input input-bordered w-full" + /> + + + setTaskData({ ...taskData, due_date: e.target.value }) + } + className="input input-bordered w-full" + /> + + +
+ + {editingId && ( + + )} +
+
+
+
+ + {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"} +

+
+ + +
+
+
+ )) + : !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 ( + <> + + + + + ); +}; + +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 ( +
+ +
+ ); +} 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 */} -
-
-
+ {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"}

-
@@ -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 }) + } + /> + +
+
); }; -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 ( + +
+ +
+ +
+
+
+ + Profile + + + +

+ Name:{" "} + + {authUser?.fullName || "Unknown"} + +

+

+ Email: {authUser?.email || "Not provided"} +

+

+ Username: {authUser?.username || "N/A"} +

+ +
+ +
+
+
+
+
+
+ ); +}; + +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)} - /> - -
+ + + +
+ + +
+ + + + + ); }; -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({