commit 36f9a5e69b70086ac7c60939a101ffee605126ec Author: sbinsalman Date: Tue Nov 25 11:11:42 2025 -0700 upload files diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cee347 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# SleekBoard + +## Overview +This project consists of a **Node.js backend** and a **React frontend**, both located in their respective subdirectories. + +## Prerequisites +Make sure you have the following installed: +- [Node.js](https://nodejs.org/) (22.21.0 or higher) +- [npm](https://www.npmjs.com/) (10.9.4 or higher) + +## Project Structure +``` +project-root/ +│-- backend/ # Node.js backend +│-- frontend/ # React frontend +│-- README.md # This file +``` + +--- + +## Backend Setup + +### Navigate to the backend directory +```sh +cd backend +``` + +### Install dependencies +```sh +npm install +``` + +### Configure Environment Variables +Create a `.env` file in the `backend/` directory and add necessary environment variables. +``` +PORT=5050 +MONGO_URI=mongodb+srv://sleekBoard_team:cpZaj3w0lgma80BY@cluster0.egyquin.mongodb.net/?appName=Cluster0 +``` + +### Run the backend server +```sh +npm run start +``` + +### Running in Development Mode +```sh +npm run dev +``` + +By default, the backend runs on `http://localhost:5050/` (or the port specified in `.env`). + +--- + +## Frontend Setup + +### Navigate to the frontend directory +```sh +cd ../frontend +``` + +### Install dependencies +```sh +npm install +``` + +### Configure Environment Variables +Create a `.env` file in the `frontend/` directory and add necessary environment variables. +``` +VITE_BACKEND_URL = http://localhost:5050 +``` + +### Build the frontend server +```sh +npm run build +``` + +### Run the frontend server in Development mode +```sh +npm run dev +``` + +By default, the frontend runs on `http://localhost:5173/`. + +--- + + +## Additional Notes +- Ensure the backend is running before accessing the frontend. +- Update `.env` files with appropriate values for production environments. +--- diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 0000000..c40a144 Binary files /dev/null and b/backend/.DS_Store differ diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..46041ee --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,139 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite logs files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* \ No newline at end of file diff --git a/backend/controllers/comments.controller.js b/backend/controllers/comments.controller.js new file mode 100644 index 0000000..78cb1c3 --- /dev/null +++ b/backend/controllers/comments.controller.js @@ -0,0 +1,78 @@ +import Comment from "../models/comments.model.js"; +import { StatusCodes } from "http-status-codes"; + +// POST /api/comments +export const createComment = async (req, res) => + { + try + { + const { userStoryId, commentText, commentedBy } = req.body; + + if (!userStoryId || !commentText) + { + return res.status(StatusCodes.BAD_REQUEST).json({ message: "userStoryId and text are required" }); + } + + const comment = new Comment({userStoryId,commentText,commentedBy}); + + const saved = await comment.save(); + return res.status(StatusCodes.CREATED).json(saved); + } + catch (err) + { + console.error("Error creating comment", err); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: "Error creating comment" }); + } + }; + +// PUT /api/comments/:id +export const updateComment = async (req, res) => + { + try + { + const { id } = req.params; + const { commentText } = req.body; + + if (!commentText) + { + return res.status(StatusCodes.BAD_REQUEST).json({ message: "text is required" }); + } + + const updated = await Comment.findByIdAndUpdate(id,{ commentText },{ new: true }); + + if (!updated) + { + return res.status(StatusCodes.NOT_FOUND).json({ message: "Comment not found" }); + } + + return res.status(StatusCodes.OK).json(updated); + } + catch (err) + { + console.error("Error updating comment", err); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: "Error updating comment" }); + } + }; + +// DELETE /api/comments/:id +export const deleteComment = async (req, res) => + { + try + { + const { id } = req.params; + + const deleted = await Comment.findByIdAndDelete(id); + + if (!deleted) + { + return res.status(StatusCodes.NOT_FOUND).json({ message: "Comment not found" }); + } + + return res.status(StatusCodes.OK).json({ message: "Comment deleted successfully" }); + } + catch (err) + { + console.error("Error deleting comment", err); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: "Error deleting comment" }); + } + }; \ No newline at end of file diff --git a/backend/controllers/userStories.controller.js b/backend/controllers/userStories.controller.js new file mode 100644 index 0000000..883cfbd --- /dev/null +++ b/backend/controllers/userStories.controller.js @@ -0,0 +1,64 @@ +import UserStory from "../models/userStories.model.js"; +import { StatusCodes } from "http-status-codes"; +import Comment from "../models/comments.model.js"; + + +export async function createUserStories(req, res) { + try { + const userStory = new UserStory(req.body); + const savedStory = await userStory.save(); + + res.status(StatusCodes.CREATED).json(savedStory); + } catch (e) { + res.status(StatusCodes.NOT_FOUND).json({ error: e.message }); + } +} + +export async function getUserStory(req, res) { + try { + const userStory = await UserStory.findById(req.params.id).populate({ + path: "comments", + options: { sort: { createdAt: -1 } }, + }); + + if (!userStory) { + return res.status(StatusCodes.NOT_FOUND).json({ message: "User story not found" }); + } + + return res.status(StatusCodes.OK).json(userStory); + } catch (error) { + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: error.message }); + } +} + +export async function updateUserStory(req, res) { + try { + const updatedStory = await UserStory.findByIdAndUpdate(req.params.id, req.body, { new: true }); + if (!updatedStory) { + return res.status(StatusCodes.NOT_FOUND).json({ message: "User story not found" }); + } + res.status(StatusCodes.OK).json(updatedStory); + } catch (error) { + res.status(StatusCodes.BAD_REQUEST).json({ message: error.message }); + } +} + +export async function deleteUserStory(req, res) { + try { + const out = await UserStory.findByIdAndDelete(req.params.id); + if (!out) return res.status(StatusCodes.NOT_FOUND).json({ error: "User story not found" }); + res.status(StatusCodes.OK).json({ message: "User story deleted successfully" }); + } catch (e) { + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: e.message }); + } +} + +export async function listUserStories(req, res) { + try { + const stories = await UserStory.find(); + res.status(StatusCodes.OK).json(stories); + + } catch (e) { + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: e.message }); + } +} \ No newline at end of file diff --git a/backend/models/comments.model.js b/backend/models/comments.model.js new file mode 100644 index 0000000..015c9b0 --- /dev/null +++ b/backend/models/comments.model.js @@ -0,0 +1,25 @@ +import mongoose, { model } from "mongoose"; +const { Schema } = mongoose; + +const CommentUserStorySchema = new Schema( + { + userStoryId: { type: Schema.Types.ObjectId, ref: "UserStory", required: true }, + commentText: { type: String, required: true }, + commentedBy: { type: String, required: true }, + }, + { timestamps: true } +); + +CommentUserStorySchema.post("save", async function (doc, next) { + try { + await mongoose.model("UserStory").findByIdAndUpdate( + doc.userStoryId, + { $addToSet: { comments: doc._id } } // $addToSet avoids duplicates + ); + next(); + } catch (err) { + next(err); + } +}); + +export default model("Comment", CommentUserStorySchema); diff --git a/backend/models/userStories.model.js b/backend/models/userStories.model.js new file mode 100644 index 0000000..996aa1f --- /dev/null +++ b/backend/models/userStories.model.js @@ -0,0 +1,20 @@ +import mongoose, { model } from "mongoose"; +const { Schema } = mongoose; + +const UserStorySchema = new Schema( + { + title: { type: String, required: true }, + description: { type: String, required: true }, + status: { type: String, enum: ["Todo", "In-Review", "Sprint-Ready"], default: "Todo" }, + businessValue: { type: Number, required: true, min: 1, max: 100 }, + storyPoint: { type: Number, required: true, enum: [1, 2, 3, 5, 8, 13, 21, 34, 55] }, + assignedTo: { type: String }, + comments: [{ type: Schema.Types.ObjectId, ref: "Comment" }], + }, + { timestamps: true } +); + +//Sorting by most recent +UserStorySchema.index({ createdAt: -1 }); + +export default model("UserStory", UserStorySchema); diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..c4dfdc5 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "backend", + "type": "module", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "http-status-codes": "^2.3.0", + "mongoose": "^8.19.2" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } +} diff --git a/backend/routes/comments.route.js b/backend/routes/comments.route.js new file mode 100644 index 0000000..3d79ff0 --- /dev/null +++ b/backend/routes/comments.route.js @@ -0,0 +1,19 @@ +import { Router } from "express"; +import { + createComment, + updateComment, + deleteComment, +} from "../controllers/comments.controller.js"; + +const router = Router(); + +// POST /api/comments +router.post("/", createComment); + +// PUT /api/comments/:id +router.put("/:id", updateComment); + +// DELETE /api/comments/:id +router.delete("/:id", deleteComment); + +export default router; \ No newline at end of file diff --git a/backend/routes/userStories.route.js b/backend/routes/userStories.route.js new file mode 100644 index 0000000..9798958 --- /dev/null +++ b/backend/routes/userStories.route.js @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { + createUserStories, + getUserStory, + updateUserStory, + deleteUserStory, + listUserStories, +} from "../controllers/userStories.controller.js"; + +const router = Router(); + +router.get("/", listUserStories); +router.post("/", createUserStories); +router.get("/:id", getUserStory); +router.put("/:id", updateUserStory); +router.delete("/:id", deleteUserStory); + +export default router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..3a7d820 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,35 @@ +import express from "express"; +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import cors from "cors"; + +import userStoriesRoutes from "./routes/userStories.route.js"; +import commentsRoutes from "./routes/comments.route.js"; + +dotenv.config(); +const app = express(); + +app.use(cors()); +app.use(express.json()); + +// MongoDB connection +mongoose.connect(process.env.MONGO_URI) + .then(() => console.log("MongoDB connected")) + .catch(err => console.log(err)); + +// Health check route +app.get("/health", (req, res) => { + res.status(200).type("text/plain").send("Server works"); + console.log("Server works") +}); + +// User Stories routes +app.use("/api/v1/user-stories", userStoriesRoutes); + +// User stories comments routes +app.use("/api/v1/comments", commentsRoutes); + + +// Start server +const PORT = process.env.PORT || 5050; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..3e212e1 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + settings: { react: { version: '18.2' } }, + plugins: ['react-refresh'], + rules: { + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..7ceb59f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..f768e33 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..51d5fba --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "sleek-board", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@heroicons/react": "^2.2.0", + "@material-tailwind/react": "^2.1.10", + "axios": "^1.13.2", + "dotenv": "^17.2.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.21", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", + "vite": "^5.3.4" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..41896cf --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,29 @@ +import "@/styles/App.css"; + +import UserStories from "@/pages/UserStories"; +import ToastContainer from "@/components/common/ToastContainer"; +import GlobalLoader from "@/components/layout/loaders/GlobalLoader"; + +import { ToastProvider } from "@/context/ToastContext"; +import { LoaderProvider } from "@/context/LoaderContext"; + +import { Navigationbar } from "@/components/layout/navbar/Navbar"; + +function App() { + return ( + + +
+ + +
+ + + + +
+
+ ); +} + +export default App; diff --git a/frontend/src/assets/fonts/Poppins-Regular.ttf b/frontend/src/assets/fonts/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/frontend/src/assets/fonts/Poppins-Regular.ttf differ diff --git a/frontend/src/assets/imgs/dummy b/frontend/src/assets/imgs/dummy new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/comments/CommentsModal.jsx b/frontend/src/components/comments/CommentsModal.jsx new file mode 100644 index 0000000..aec7a12 --- /dev/null +++ b/frontend/src/components/comments/CommentsModal.jsx @@ -0,0 +1,280 @@ +import { useState, useEffect } from "react"; +import { useToast } from "@/context/ToastContext"; +import { useLoader } from "@/context/LoaderContext"; + +import { IoClose } from "react-icons/io5"; +import { FaTrashAlt, FaPencilAlt, FaCheck, FaTimes } from "react-icons/fa"; + +import { getUserStoryByID } from "@/services/apis/UserStories"; + +import { + addComment, + updateComment, + deleteComment, +} from "@/services/apis/comments"; + +import { + Textarea, + Button, + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + IconButton +} from "@material-tailwind/react"; + + +const CommentsModal = (props) => { + const { + isCommentOpen, + handleCommentOpen, + userStoryId, + currentUserName = "Developer", + } = props + + const toast = useToast(); + const { startGlobalLoading, stopGlobalLoading } = useLoader(); + + // --------------------------------------- + + const [comments, setComments] = useState([]); + const [newComment, setNewComment] = useState(""); + const [editingCommentId, setEditingCommentId] = useState(null); + const [editingText, setEditingText] = useState(""); + + // --------------------------------------- + + const handleNewCommentChange = (e) => { + setNewComment(e.target.value); + }; + + const initUserStory = async (id) => { + if (!id) return; + + try { + startGlobalLoading(); + + const userStory = await getUserStoryByID(id); + setComments(userStory?.comments || []); + } + catch (error) { + console.error("Error fetching user story:", error); + toast.error("Failed to load comments."); + } + finally { + stopGlobalLoading(); + } + }; + + useEffect(() => { + if (isCommentOpen && userStoryId) { + initUserStory(userStoryId); + } + }, [isCommentOpen, userStoryId]); + + const submitNewComment = async () => { + if (!newComment.trim()) { + toast.warning("Please write a comment."); + return; + } + + if (!userStoryId) { + toast.error("No user story selected."); + return; + } + + startGlobalLoading(); + + try { + const payload = { + userStoryId, + commentText: newComment.trim(), + commentedBy: currentUserName, + }; + + await addComment(payload); + await initUserStory(userStoryId); + + setNewComment(""); + toast.success("Comment added successfully!"); + } + catch (err) { + console.error("Error adding comment:", err); + toast.error("Failed to add comment."); + } + finally { + stopGlobalLoading(); + } + }; + + const startEditing = (comment) => { + setEditingCommentId(comment._id); + setEditingText(comment.commentText); + }; + + const cancelEditing = () => { + setEditingCommentId(null); + setEditingText(""); + }; + + const saveEditedComment = async () => { + if (!editingText.trim() || !editingCommentId) { + toast.warning("Please write a comment."); + return; + } + + startGlobalLoading(); + + try { + const payload = { commentText: editingText.trim() }; + + await updateComment(editingCommentId, payload); + await initUserStory(userStoryId); + + cancelEditing(); + + toast.success("Comment updated successfully!"); + } + catch (err) { + console.error("Error updating comment:", err); + toast.error("Failed to update comment."); + } + finally { + stopGlobalLoading(); + } + }; + + const handleDelete = async (id) => { + if (!id) return; + + startGlobalLoading(); + + try { + await deleteComment(id); + await initUserStory(userStoryId); + + toast.success("Comment deleted successfully!"); + } + catch (err) { + console.error("Error deleting comment:", err); + toast.error("Failed to delete comment."); + } + finally { + stopGlobalLoading(); + } + }; + + return ( + + +

Comments

+ + + + +
+ + +
+ {comments.length === 0 && ( +

+ No comments yet. Be the first to add one! +

+ )} + + {comments.map((comment) => ( +
+
+

+ By: {comment.commentedBy || "Unknown"} +

+ +
+ {editingCommentId === comment._id ? ( + <> + + + + + + + + + ) : ( + <> + startEditing(comment)} + > + + + + handleDelete(comment._id)} + > + + + + )} +
+
+ + {editingCommentId === comment._id ? ( +