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..32f7a38cd8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,58 @@ -# Final Project +Final Project -Replace this readme with your own information about your project. +#This is a full-stack Task Management App where users can create, update, and manage tasks, projects, and groups with file uploads, authentication, and a responsive UI. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +# The problem -## The problem +#A lot of people including myself have task managing problems this app solves it with a vast array of methods. It also supports collaberation. -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 assignment was to build a complete full-stack application. I chose to create a task and project management system that supports collaboration, authentication, and file handling. -## View it live +#I planned the project around scalability and user experience, using: -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 +#Frontend: React (Vite) with Zustand for state management and Material-UI for accessible, responsive components. + +Backend: Node.js + Express with MongoDB, GridFS for file storage, Multer for uploads, and JWT for authentication. + +Deployment: Netlify (frontend) and Render (backend). + +If I had more time, I’d add real-time updates (WebSockets), richer collaboration tools, and more advanced analytics dashboards. + +## View live +https://project-final-darius-1.onrender.com + +Frontend: https://project-final-darius.netlify.app/ + +Key Highlights: + +Frontend: React (Vite), Zustand for state management with persistence, Material-UI for responsive and accessible UI, Axios for API communication. + +Backend: Node.js + Express with a RESTful API, MongoDB for storage, GridFS for file handling, Multer for uploads, and JWT for authentication. + +Core Features: + +Full task CRUD (create, update, delete, completion tracking). + +Project management with automatic completion calculation. + +Group management (create, join, leave, assign projects). + +File uploads stored in MongoDB GridFS. + +Filtering, search, and pagination support. + +UX & Accessibility: Dark/light mode toggle, responsive layout, ARIA roles, modals, snackbars, and real-time state-driven updates. + +Deployment: Frontend on Netlify, backend on Render. + +What it demonstrates: + +Proficiency in advanced state management (Zustand + persistence). + +Secure authentication with JWT and protected API routes. + +Scalable backend architecture with modular routes and file storage. + +Strong focus on UX and accessibility through Material-UI and responsive design. + +Readiness for future enhancements like real-time collaboration. 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)} /> + )} + +