diff --git a/backend/.babelrc b/.babelrc similarity index 100% rename from backend/.babelrc rename to .babelrc diff --git a/.gitignore b/.gitignore index 3d70248ba2..0936ed3415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,35 @@ -node_modules -.DS_Store +# Node modules +node_modules/ +backend/node_modules/ +frontend/node_modules/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables .env .env.local .env.development.local .env.test.local .env.production.local +backend/.env +frontend/.env -build +# Build outputs +frontend/dist/ +frontend/build/ +build/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# OS files +.DS_Store +Thumbs.db + +# IDE configs +.vscode/ +.idea/ -package-lock.json \ No newline at end of file +# Package lock +package-lock.json diff --git a/README.md b/README.md index 31466b54c2..8e6cefa137 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,138 @@ -# Final Project +Task and Collaboration Management System +Overview +Live Demo -Replace this readme with your own information about your project. +Backend: https://project-final-darius-1.onrender.com -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +Frontend: https://project-final-darius.netlify.app/ -## The problem +This project is a full-stack task and project management application designed to help users organize their work while supporting collaboration within groups. It combines task tracking, project organization, and shared group functionality into a single platform. -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +The system addresses a common problem: managing tasks individually is already challenging, but coordinating work across multiple users introduces additional complexity. This application provides a structured solution for both personal productivity and team-based workflows. -## View it live +Features +Task and Project Management +Create, update, and delete tasks +Mark tasks as completed +Organize tasks into projects +Automatic project progress calculation based on task completion +Filtering, search, and pagination for scalability -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. \ No newline at end of file +Group Collaboration + +Create, join, and leave groups +Assign projects to groups +View group members +Shared workflows for collaborative productivity +Basic access control (only the creator can delete a group) + +File and Folder Upload Workflow + +Upload multiple files and entire folders when creating tasks +Files are sent using multipart FormData and processed with Multer +Supports scalable file storage (including GridFS integration) +Basic file type detection (images, PDFs, documents, text) +Preserves folder structure information for better organization + +User Experience and Accessibility +Responsive design using Material-UI +Dark and light mode support +Accessible UI components (ARIA roles, dialogs, alerts) +Real-time UI updates through state-driven rendering +Integrated task creation flow combining projects, files, and deadlines + +Tech Stack +Frontend +React (Vite) +Zustand (modular state management with persistence) +Material-UI +Axios +Backend +Node.js with Express +MongoDB +GridFS +Multer +JWT authentication +Deployment +Frontend hosted on Netlify +Backend hosted on Render +Architecture +Backend + +The backend is built using Express and follows a modular and scalable structure with clear separation of routes, middleware, and models. + +Backend Implementation Details +Environment configuration using dotenv for secure variable management +MongoDB connection initialized at server startup via a dedicated function +Custom CORS configuration restricting access to approved frontend origins +Middleware layer handling: +JSON and URL parsing +Authentication +Debug logging +Route structure: +/auth – authentication +/tasks – task management (protected) +/groups – collaboration system +Authentication and Authorization + +A centralized authentication middleware verifies JWT tokens on protected routes. Once validated, user data is attached to the request object, enabling secure and consistent access control across the application. + +File Handling + +File uploads are processed using Multer, supporting multipart requests and integration with scalable storage solutions such as GridFS. Uploaded files can be classified by type (image, PDF, document, text), allowing for future extensibility. + +Frontend +Modular State Management with Zustand + +The application uses Zustand to manage global state in a modular way, separating logic across: + +user authentication +group collaboration +task and project management + +This structure keeps business logic independent from UI components and improves maintainability and scalability. + +Persistent Authentication Flow + +User authentication is persisted using JWT tokens stored in localStorage. On application load, the system restores the session and automatically attaches the token to API requests. + +API-backed and Local State Synchronization +Backend-managed data (users, groups, files) is handled through API calls +Task and project data are persisted locally for fast access and improved UX +Axios is used for API communication with automatic authentication headers + +Security + +JWT-based authentication +Protected API routes using centralized middleware +Token verification with user context attached to requests +Authorization checks (e.g., only group creators can delete groups) +Input validation and structured error handling + +Challenges + +Designing consistent collaboration logic for group membership, ownership, and shared projects +Synchronizing frontend state with backend data while maintaining performance +Implementing scalable file and folder upload workflows +Structuring frontend logic using modular Zustand stores without overcomplication + +Key Highlights + +Full task CRUD with completion tracking +Project management with automatic progress calculation +Group-based collaboration system +File and folder upload workflow integrated into task creation +Centralized and modular state management using Zustand +Persistent authentication and session handling +Responsive and accessible UI design + +What This Project Demonstrates + +Ability to design and build a complete full-stack application +Strong understanding of modular frontend state management +Experience implementing persistent authentication flows using JWT +Knowledge of secure backend design using centralized middleware +Ability to handle multipart file uploads and classify uploaded content +Understanding of API-driven and client-side state synchronization +Focus on accessibility, usability, and responsive design +Experience building collaborative systems with shared user state diff --git a/backend/db/db.js b/backend/db/db.js new file mode 100644 index 0000000000..fb86065218 --- /dev/null +++ b/backend/db/db.js @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; + +export const connectDB = async () => { + const mongoUrl = process.env.MONGO_URI || "mongodb://localhost:27017/task-manager"; + + try { + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + + }); + console.log("✅ MongoDB connected:", mongoUrl); + } catch (err) { + console.error("❌ MongoDB connection error:", err); + process.exit(1); // exit process if DB connection fails + } + + mongoose.connection.on("disconnected", () => { + console.warn("⚠️ MongoDB disconnected!"); + }); +}; diff --git a/backend/middleware/authmiddleware.js b/backend/middleware/authmiddleware.js new file mode 100644 index 0000000000..4887d73b02 --- /dev/null +++ b/backend/middleware/authmiddleware.js @@ -0,0 +1,36 @@ +import jwt from "jsonwebtoken"; + +export const authMiddleware = (req, res, next) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader) { + console.warn("[Auth] No Authorization header"); + return res.status(401).json({ error: "No token provided" }); + } + + const token = authHeader.split(" ")[1]; + if (!token) { + console.warn("[Auth] No token after Bearer"); + return res.status(401).json({ error: "No token provided" }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET || "supersecret123"); + + if (!decoded) { + console.warn("[Auth] Token could not be decoded"); + return res.status(401).json({ error: "Invalid token" }); + } + + // Ensure req.user has id and groupId + req.user = { + id: decoded.id, + groupId: decoded.groupId, + }; + + + next(); + } catch (err) { + console.error("[Auth] Error verifying token:", err.message); + res.status(401).json({ error: "Unauthorized" }); + } +}; diff --git a/backend/middleware/upload-task.js b/backend/middleware/upload-task.js new file mode 100644 index 0000000000..5ea9ffd8eb --- /dev/null +++ b/backend/middleware/upload-task.js @@ -0,0 +1,26 @@ +import multer from "multer"; +import path from "path"; + +// Store uploads in /uploads folder +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, "uploads/"); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + path.extname(file.originalname)); // unique name + }, +}); + +export const upload = multer({ storage }); + +export const detectFileType = (mimetype) => { + if (mimetype.startsWith("image/")) return "image"; + if (mimetype === "application/pdf") return "pdf"; + if ( + mimetype === "application/msword" || + mimetype.includes("officedocument") + ) + return "doc"; + if (mimetype.startsWith("text/")) return "text"; + return "other"; +}; \ No newline at end of file diff --git a/backend/model/files.js b/backend/model/files.js new file mode 100644 index 0000000000..bbb7a9d7cc --- /dev/null +++ b/backend/model/files.js @@ -0,0 +1,17 @@ +import mongoose from "mongoose"; +const fileSchema = new mongoose.Schema({ + name: String, // original file name + filename: String, // GridFS stored filename (with timestamp prefix) + url: String, // `/tasks/files/:filename` for frontend + contentType: String, // MIME type (e.g., text/plain, application/pdf) + size: Number, // file size in bytes + type: { + type: String, + enum: ["image", "pdf", "doc", "text", "other"], + default: "other" + }, + folder: { type: String, default: "root" }, +}); + + +export default mongoose.model("File", fileSchema); diff --git a/backend/model/groups.js b/backend/model/groups.js new file mode 100644 index 0000000000..6962f1d29f --- /dev/null +++ b/backend/model/groups.js @@ -0,0 +1,10 @@ +import mongoose from "mongoose"; + +const groupSchema = new mongoose.Schema({ + name: { type: String, required: true }, + members: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], + currentProject: { type: String, default: null }, + createdAt: { type: Date, default: Date.now }, +}); + +export default mongoose.model("Group", groupSchema); diff --git a/backend/model/tasks.js b/backend/model/tasks.js new file mode 100644 index 0000000000..74e8b1b4e5 --- /dev/null +++ b/backend/model/tasks.js @@ -0,0 +1,31 @@ +import mongoose from "mongoose"; + +const fileSchema = new mongoose.Schema({ + name: String, // original file name + filename: String, // GridFS stored filename (with timestamp prefix) + url: String, // `/tasks/files/:filename` for frontend + contentType: String, // MIME type (e.g., text/plain, application/pdf) + size: Number, // file size in bytes + type: { + type: String, + enum: ["image", "pdf", "doc", "text", "other"], + default: "other" + }, + folder: { type: String, default: "root" }, +}); + + +const taskSchema = new mongoose.Schema({ + title: { type: String, required: true, trim: true }, + description: { type: String }, +category: { type: String, enum: ["Work","Home","Health","Errands","Leisure","Other",""], default: "Other" } , +priority: { type: String, enum: ["low", "medium", "high"], default: "medium" }, + dueDate: { type: Date }, + completed: { type: Boolean, default: false }, + files: [fileSchema], // structured files + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + group: { type: mongoose.Schema.Types.ObjectId, ref: "Group", required: true }, + createdAt: { type: Date, default: Date.now }, +}); + +export const Task = mongoose.model("Task", taskSchema); diff --git a/backend/model/users.js b/backend/model/users.js new file mode 100644 index 0000000000..d31051ce57 --- /dev/null +++ b/backend/model/users.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true, trim: true }, +email: { type: String, lowercase: true } ,// remove required + passwordHash: { type: String, required: true }, + group: { type: mongoose.Schema.Types.ObjectId, ref: "Group" }, + createdAt: { type: Date, default: Date.now } +}); + +export default mongoose.model("User", userSchema); diff --git a/backend/package.json b/backend/package.json index 08f29f2448..2e22da1ad8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,10 +1,11 @@ { "name": "project-final-backend", "version": "1.0.0", + "type": "module", "description": "Server part of final project", "scripts": { - "start": "babel-node server.js", - "dev": "nodemon server.js --exec babel-node" + "start": "babel-node backend/server.js", + "dev": "nodemon backend/server.js --exec babel-node" }, "author": "", "license": "ISC", @@ -12,9 +13,13 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "backend": "file:..", + "bcrypt": "^6.0.0", "cors": "^2.8.5", "express": "^4.17.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", + "multer": "^2.0.2", "nodemon": "^3.0.1" } -} \ No newline at end of file +} diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000000..0950184dba --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,112 @@ +import express from "express"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import User from "../model/users.js"; +import Group from "../model/groups.js"; +import { authMiddleware } from "../middleware/authmiddleware.js"; + +const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || "supersecret123"; + +// --- Register --- +router.post("/register", async (req, res) => { + console.log("REQ.BODY:", req.body); + + try { + const { username = "", password } = req.body; + + // Validate input + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required" }); + } + + // Check if username already exists + const existingUser = await User.findOne({ username }); + if (existingUser) { + return res.status(409).json({ error: "Username already taken" }); + } + + // Hash password + const salt = await bcrypt.genSalt(10); + const passwordHash = await bcrypt.hash(password, salt); + + // Find or create default group + let defaultGroup = await Group.findOne({ name: "Default Group" }); + if (!defaultGroup) { + defaultGroup = await Group.create({ name: "Default Group" }); + } + + // Create user + const user = await User.create({ + username, + passwordHash, + group: defaultGroup._id, + + }); + + res.status(201).json({ message: "User registered successfully", userId: user._id }); + } catch (err) { + console.error("Register error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// --- Login --- +router.post("/login", async (req, res) => { + try { + const { username, password } = req.body; + + if (!password || (!username)) { + return res.status(400).json({ error: "Provide username and password" }); + } + + // Find user by username o + const user = await User.findOne({ username }).populate("group"); + + if (!user) return res.status(401).json({ error: "User not found" }); + + // Compare password + const isMatch = await bcrypt.compare(password, user.passwordHash); + + + if (!isMatch) return res.status(401).json({ error: "Invalid password" }); + + + // Generate JWT + const token = jwt.sign( + { id: user._id, + groupId: user.group._id + }, + JWT_SECRET, + { expiresIn: "7d" } + ); + + res.json({ token, user: { id: user._id, username: user.username, group: user.group } }); + } catch (err) { + console.error("Login error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); +router.get("/me", authMiddleware, async (req, res) => { + try { + // Fetch user data from database using the ID from the JWT + const user = await User.findById(req.user.id).populate("group").select("-passwordHash"); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + // Return user data in the expected format + res.json({ + user: { + id: user._id, + username: user.username, + group: user.group ? { id: user.group._id, name: user.group.name } : null, + }, + }); + } catch (err) { + console.error("Get user error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default router; diff --git a/backend/routes/group-routes.js b/backend/routes/group-routes.js new file mode 100644 index 0000000000..81b7d55993 --- /dev/null +++ b/backend/routes/group-routes.js @@ -0,0 +1,156 @@ +import express from "express"; +import Group from "../model/groups.js"; +import { authMiddleware } from "../middleware/authmiddleware.js"; + +const router = express.Router(); + +/** + * CREATE a new group + * POST /groups + * Body: { name, currentProject (optional) } + */ +router.post("/", authMiddleware, async (req, res) => { + try { + const { name, currentProject } = req.body; + if (!name) return res.status(400).json({ error: "Group name is required" }); + + const existing = await Group.findOne({ name }); + if (existing) return res.status(409).json({ error: "Group name already exists" }); + + const group = await Group.create({ + name, + currentProject: currentProject || null, + members: [req.user.id], // creator automatically joins + }); + + res.status(201).json(group); + } catch (err) { + console.error("Create group error:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// DELETE /groups/:id +router.delete("/:id", authMiddleware, async (req, res) => { + try { + const group = await Group.findById(req.params.id); + if (!group) return res.status(404).json({ error: "Group not found" }); + + // Optional: Check if user is group creator + if (group.members[0].toString() !== req.user.id) { + return res.status(403).json({ error: "Only group creator can delete" }); + } + + await group.deleteOne(); + res.json({ message: "Group deleted successfully" }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +/** + * GET all groups + * GET /groups + */ +router.get("/", authMiddleware, async (req, res) => { + + try { + const groups = await Group.find().populate("members", "username"); + + console.log("Fetched groups:", JSON.stringify(groups, null, 2)); + + res.json(groups); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +/** + * JOIN a group + * PUT /groups/:id/join + */ +router.put("/:id/join", authMiddleware, async (req, res) => { + try { + const group = await Group.findById(req.params.id); + if (!group) return res.status(404).json({ error: "Group not found" }); + + if (!group.members.includes(req.user.id)) { + group.members.push(req.user.id); + await group.save(); + } + + res.json(group); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +/** + * LEAVE a group + * PUT /groups/:id/leave + */ +router.put("/:id/leave", authMiddleware, async (req, res) => { + try { + const group = await Group.findById(req.params.id); + if (!group) return res.status(404).json({ error: "Group not found" }); + + group.members = group.members.filter((id) => id.toString() !== req.user.id); + await group.save(); + + res.json({ message: "Left group successfully", group }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +/** + * SET current project for group + * PUT /groups/:id/project + * Body: { projectName } + */ +router.put("/:id/project", authMiddleware, async (req, res) => { + try { + const { projectName } = req.body; + if (!projectName) return res.status(400).json({ error: "Project name required" }); + + const group = await Group.findById(req.params.id); + if (!group) return res.status(404).json({ error: "Group not found" }); + + group.currentProject = projectName; + await group.save(); + + res.json(group); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +/** + * GET single group by ID + * GET /groups/:id + */ +router.get("/:id", authMiddleware, async (req, res) => { + try { + const group = await Group.findById(req.params.id).populate("members", "username"); + if (!group) return res.status(404).json({ error: "Group not found" }); + res.json(group); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +router.delete("/:id/project", authMiddleware, async (req, res) => { + try { + const group = await Group.findById(req.params.id); + if (!group) return res.status(404).json({ error: "Group not found" }); + + group.currentProject = null; + await group.save(); + + res.json({ message: "Project removed", group }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/backend/routes/taskroutes.js b/backend/routes/taskroutes.js new file mode 100644 index 0000000000..d5390bcbeb --- /dev/null +++ b/backend/routes/taskroutes.js @@ -0,0 +1,196 @@ +import express from "express"; +import mongoose from "mongoose"; +import multer from "multer"; +import { Task } from "../model/tasks.js"; +import Group from "../model/groups.js"; +import { authMiddleware } from "../middleware/authmiddleware.js"; + +const router = express.Router(); + +// ------------------ MONGODB GRIDFS SETUP ------------------ +const mongoURI = process.env.MONGO_URI; +const conn = mongoose.createConnection(mongoURI, {}); + +let gfs; +conn.once("open", () => { + gfs = new mongoose.mongo.GridFSBucket(conn.db, { bucketName: "uploads" }); + console.log("✅ GridFS initialized"); +}); + +// ------------------ MULTER MEMORY STORAGE ------------------ +const upload = multer({ storage: multer.memoryStorage() }); + +// ------------------ HELPERS ------------------ +const waitForGFS = () => + new Promise((resolve) => { + if (gfs) return resolve(); + conn.once("open", () => { + gfs = new mongoose.mongo.GridFSBucket(conn.db, { bucketName: "uploads" }); + console.log("✅ GridFS initialized"); + resolve(); + }); + }); + +const saveFileToGridFS = async (file) => { + await waitForGFS(); + return new Promise((resolve, reject) => { + try { + const filename = `${Date.now()}-${file.originalname}`; + const uploadStream = gfs.openUploadStream(filename, { contentType: file.mimetype }); + uploadStream.end(file.buffer); + + uploadStream.on("finish", () => + resolve({ + name: file.originalname, + filename, + url: `/tasks/files/${filename}`, + contentType: file.mimetype, + size: file.size, + folder: "root", + }) + ); + + uploadStream.on("error", reject); + } catch (err) { + reject(err); + } + }); +}; + +// ------------------ ROUTES ------------------ + +// --- Serve files from GridFS --- +router.get("/files/:filename", async (req, res) => { + try { + await waitForGFS(); + const { filename } = req.params; + + const files = await gfs.find({ filename }).toArray(); + if (!files.length) return res.status(404).json({ error: "File not found" }); + + const file = files[0]; + res.set({ + "Content-Type": file.contentType || "application/octet-stream", + "Content-Disposition": `inline; filename="${file.filename}"`, + }); + + gfs.openDownloadStreamByName(filename).pipe(res).on("error", (err) => { + console.error("❌ Error streaming file:", err); + if (!res.headersSent) res.status(500).json({ error: "Error streaming file" }); + else res.destroy(err); + }); + } catch (err) { + + res.status(500).json({ error: err.message }); + } +}); + +// --- Create task --- +router.post("/", authMiddleware, upload.array("files", 5), async (req, res) => { + try { + const { task: title, category, projectId, dueDate, description } = req.body; + + const files = await Promise.all((req.files || []).map(saveFileToGridFS)); + + const safeCategory = category?.trim() || "Other"; + const groupId = req.user.groupId || (await Group.findOne({ name: "Default Group" }))._id; + + const task = await Task.create({ + title, + description, + category: safeCategory, + dueDate, + files, + createdBy: req.user.id, + group: groupId, + }); + + res.status(201).json(task); + } catch (err) { + + res.status(400).json({ error: err.message }); + } +}); + + + +// --- Upload file to existing task --- +router.post("/:taskId/files", authMiddleware, upload.single("file"), async (req, res) => { + try { + + const task = await Task.findById(req.params.taskId); + if (!task) return res.status(404).json({ error: "Task not found" }); + + const fileMeta = await saveFileToGridFS(req.file); + task.files.push(fileMeta); + await task.save(); + + res.json({ message: "File uploaded", task }); + } catch (err) { + + res.status(500).json({ error: err.message }); + } +}); + +// --- Get all tasks --- +router.get("/", authMiddleware, async (req, res) => { + try { + const tasks = await Task.find({ group: req.user.groupId }) + .populate("createdBy", "username") + .sort({ createdAt: -1 }); + + res.json(tasks); + } catch (err) { + + res.status(500).json({ error: err.message }); + } +}); + +// --- Get single task --- +router.get("/:id", authMiddleware, async (req, res) => { + try { + const task = await Task.findById(req.params.id); + if (!task) return res.status(404).json({ error: "Task not found" }); + + res.json(task); + } catch (err) { + + res.status(500).json({ error: err.message }); + } +}); + +// --- Update task --- +router.put("/:id", authMiddleware, upload.array("files", 5), async (req, res) => { + try { + const { title, description, priority, dueDate, completed, folder } = req.body; + const newFiles = await Promise.all((req.files || []).map(saveFileToGridFS)); + + const task = await Task.findByIdAndUpdate( + req.params.id, + { $set: { title, description, priority, dueDate, completed }, $push: { files: { $each: newFiles } } }, + { new: true } + ); + + if (!task) return res.status(404).json({ error: "Task not found" }); + + res.json(task); + } catch (err) { + + res.status(400).json({ error: err.message }); + } +}); + +// --- Delete task --- +router.delete("/:id", authMiddleware, async (req, res) => { + try { + const task = await Task.findByIdAndDelete(req.params.id); + if (!task) return res.status(404).json({ error: "Task not found" }); + + res.json({ message: "Task deleted" }); + } catch (err) { + + res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 070c875189..7ed87a9f82 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,22 +1,75 @@ import express from "express"; import cors from "cors"; -import mongoose from "mongoose"; +import dotenv from "dotenv"; +import multer from "multer"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; +import { connectDB } from "./db/db.js"; +import authRoutes from "./routes/auth.js"; +import taskRoutes from "./routes/taskroutes.js"; +import groupRoutes from "./routes/group-routes.js"; +import { authMiddleware } from "./middleware/authmiddleware.js"; + +dotenv.config(); -const port = process.env.PORT || 8080; const app = express(); +const port = process.env.PORT || 8080; + +// --- Database --- +connectDB(); -app.use(cors()); +// --- CORS Setup --- +const allowedOrigins = [ + "https://project-final-darius.netlify.app", + "https://project-final-darius-1.onrender.com" +]; + +app.use(cors({ + origin: (origin, callback) => { + console.log("[CORS] Request origin:", origin); + if (!origin || allowedOrigins.includes(origin)) { + return callback(null, true); + } + callback(new Error("CORS not allowed")); + }, + credentials: true, +})); + +// --- Middleware --- app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// --- Request Logger --- -app.get("/", (req, res) => { + +// --- Auth Logger --- +app.use("/tasks", authMiddleware, (req, res, next) => { + console.log("[Auth] req.user set:", req.user); + next(); +}); + +// --- Routes --- +app.use("/auth", authRoutes); +app.use("/tasks", taskRoutes); +app.use("/groups", groupRoutes); + +// --- Root Route --- +app.get("/", (_, res) => { + console.log("[Root] Accessed /"); res.send("Hello Technigo!"); }); -// Start the server +// --- Start Server --- app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); + console.log(`Server running at http://localhost:${port}`); + console.log("NODE_ENV:", process.env.NODE_ENV); + console.log("MONGO_URI:", process.env.MONGO_URI); +}); + +// --- Multer Setup Example for Logging --- +const upload = multer({ storage: multer.memoryStorage() }); + +app.post("/debug-upload", upload.single("file"), (req, res) => { + console.log("[Upload] req.file:", req.file); + console.log("[Upload] req.body:", req.body); + res.json({ message: "Upload debug complete" }); }); diff --git a/backend/wake.js b/backend/wake.js new file mode 100644 index 0000000000..847caa83ea --- /dev/null +++ b/backend/wake.js @@ -0,0 +1,8 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); + +mongoose.connect(process.env.MONGO_URI) + .then(() => console.log("✅ Cluster awake!")) + .catch(err => console.error(err)); diff --git a/frontend/index.html b/frontend/index.html index 664410b5b9..705d22b70d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,12 +2,19 @@ - + + - Technigo React Vite Boiler Plate + Todo
- + diff --git a/frontend/package.json b/frontend/package.json index 7b2747e949..8f53fc5f7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.1", + "@mui/material": "^7.3.1", + "@mui/x-date-pickers": "^8.10.2", + "axios": "^1.11.0", + "date-fns": "^4.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^7.8.2", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/public/_redirects b/frontend/public/_redirects new file mode 100644 index 0000000000..ad37e2c2c9 --- /dev/null +++ b/frontend/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/frontend/public/photos/7247856.webp b/frontend/public/photos/7247856.webp new file mode 100644 index 0000000000..0f41ea17b7 Binary files /dev/null and b/frontend/public/photos/7247856.webp differ diff --git a/frontend/public/photos/stars-night-galaxy-4k-3840x2160.webp b/frontend/public/photos/stars-night-galaxy-4k-3840x2160.webp new file mode 100644 index 0000000000..6c1b3f8adb Binary files /dev/null and b/frontend/public/photos/stars-night-galaxy-4k-3840x2160.webp differ diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000000..542c6194a7 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://project-final-darius.netlify.app/sitemap.xml diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a24275e6e..685d21a1a6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,275 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { + BrowserRouter, + Routes, + Route, + Navigate +} from "react-router-dom"; +import { useTaskStore } from './store/useTaskStore'; +import { Header } from './Header'; +import { SubmitTask } from './SubmitTask'; +import { DisplayTasks } from './DisplayTasks'; +import { Footer } from './Footer'; +import RegisterForm from './registration.jsx'; +import LoginForm from './login.jsx'; +import GroupsManagement from './groups-mngnt.jsx'; +import { + Box, + Button, + Typography, + Paper, + Container, + CssBaseline, + createTheme, + ThemeProvider, +} from '@mui/material'; + +/* ------------------ LOGIN PAGE (externalized later) ------------------ */ +const LoginPage = ({ theme, onLogin }) => { + const [showRegisterModal, setShowRegisterModal] = useState(false); + + return ( + + + + + Welcome to To-Do App + + + + + + + {showRegisterModal && ( + setShowRegisterModal(false)} + > + e.stopPropagation()} + > + + + + + )} + + + ); +}; + +/* ------------------ APP (with inline MainApp) ------------------ */ export const App = () => { + const [darkMode, setDarkMode] = useState(false); + const tasks = useTaskStore((s) => s.tasks); + + const [user, setUser] = useState(() => { + const token = localStorage.getItem('token'); + return token ? { token } : null; + }); + + const [showGroups, setShowGroups] = useState(false); + + const handleLogin = (token) => { + localStorage.setItem('token', token); + setUser({ ...user, token }); + }; + + const handleLogout = () => { + localStorage.removeItem('token'); + setUser(null); + }; + + const toggleDarkMode = useCallback(() => setDarkMode((prev) => !prev), []); + + const theme = useMemo( + () => + createTheme({ + palette: { + mode: darkMode ? 'dark' : 'light', + primary: { + main: darkMode ? '#9c27b0' : '#1976d2', + contrastText: '#fff', + }, + background: { + default: darkMode ? '#000' : '#fff', + paper: darkMode ? '#1a1a1a' : '#f5f5f5', + }, + }, + }), + [darkMode] + ); + + /* ------------------ MAIN APP ------------------ */ + const MainApp = () => ( + + + +
setShowGroups(true)} + /> + + + + + + + + + + To Do List + + Total Tasks: {tasks.length} + + Uncompleted Tasks: {tasks.filter((t) => !t.completed).length} + + + + + + + + + + + + {showGroups && ( + setShowGroups(false)} /> + )} + +