├── public
└── favicon.ico
├── src
├── assets
│ ├── favicon.ico
│ ├── workspace_img_default.png
│ ├── profile_img_a.svg
│ ├── profile_img_j.svg
│ ├── profile_img_o.svg
│ ├── schema.prisma
│ └── assets.js
├── app
│ └── store.js
├── main.jsx
├── App.jsx
├── features
│ ├── themeSlice.js
│ └── workspaceSlice.js
├── pages
│ ├── Layout.jsx
│ ├── Dashboard.jsx
│ ├── Projects.jsx
│ ├── ProjectDetails.jsx
│ ├── TaskDetails.jsx
│ └── Team.jsx
├── index.css
└── components
│ ├── ProjectCard.jsx
│ ├── Navbar.jsx
│ ├── Sidebar.jsx
│ ├── InviteMemberDialog.jsx
│ ├── AddProjectMember.jsx
│ ├── MyTasksSidebar.jsx
│ ├── ProjectsSidebar.jsx
│ ├── WorkspaceDropdown.jsx
│ ├── StatsGrid.jsx
│ ├── TasksSummary.jsx
│ ├── RecentActivity.jsx
│ ├── ProjectOverview.jsx
│ ├── ProjectSettings.jsx
│ ├── CreateTaskDialog.jsx
│ ├── ProjectAnalytics.jsx
│ ├── CreateProjectDialog.jsx
│ ├── ProjectCalendar.jsx
│ └── ProjectTasks.jsx
├── vite.config.js
├── index.html
├── eslint.config.js
├── package.json
├── LICENSE.md
├── README.md
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GreatStackDev/project-management/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GreatStackDev/project-management/HEAD/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/assets/workspace_img_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GreatStackDev/project-management/HEAD/src/assets/workspace_img_default.png
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import tailwindcss from '@tailwindcss/vite'
4 |
5 | // https://vite.dev/config/
6 | export default defineConfig({
7 | plugins: [react(), tailwindcss()],
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import workspaceReducer from '../features/workspaceSlice'
3 | import themeReducer from '../features/themeSlice'
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | workspace: workspaceReducer,
8 | theme: themeReducer,
9 | },
10 | })
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Project Management
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import './index.css'
3 | import App from './App.jsx'
4 | import { BrowserRouter } from 'react-router-dom'
5 | import { store } from './app/store.js'
6 | import { Provider } from 'react-redux'
7 |
8 | createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 | ,
14 | )
--------------------------------------------------------------------------------
/src/assets/profile_img_a.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import { defineConfig, globalIgnores } from 'eslint/config'
6 |
7 | export default defineConfig([
8 | globalIgnores(['dist']),
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | extends: [
12 | js.configs.recommended,
13 | reactHooks.configs['recommended-latest'],
14 | reactRefresh.configs.vite,
15 | ],
16 | languageOptions: {
17 | ecmaVersion: 2020,
18 | globals: globals.browser,
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | ecmaFeatures: { jsx: true },
22 | sourceType: 'module',
23 | },
24 | },
25 | rules: {
26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27 | },
28 | },
29 | ])
30 |
--------------------------------------------------------------------------------
/src/assets/profile_img_j.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from "react-router-dom";
2 | import Layout from "./pages/Layout";
3 | import { Toaster } from "react-hot-toast";
4 | import Dashboard from "./pages/Dashboard";
5 | import Projects from "./pages/Projects";
6 | import Team from "./pages/Team";
7 | import ProjectDetails from "./pages/ProjectDetails";
8 | import TaskDetails from "./pages/TaskDetails";
9 |
10 | const App = () => {
11 | return (
12 | <>
13 |
14 |
15 | }>
16 | } />
17 | } />
18 | } />
19 | } />
20 | } />
21 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "project-management",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@reduxjs/toolkit": "^2.8.2",
14 | "@tailwindcss/vite": "^4.1.12",
15 | "date-fns": "^4.1.0",
16 | "lucide-react": "^0.540.0",
17 | "react": "^19.1.1",
18 | "react-dom": "^19.1.1",
19 | "react-hot-toast": "^2.6.0",
20 | "react-redux": "^9.2.0",
21 | "react-router-dom": "^7.8.1",
22 | "recharts": "^3.1.2",
23 | "tailwindcss": "^4.1.12"
24 | },
25 | "devDependencies": {
26 | "@eslint/js": "^9.33.0",
27 | "@types/react": "^19.1.10",
28 | "@types/react-dom": "^19.1.7",
29 | "@vitejs/plugin-react": "^5.0.0",
30 | "eslint": "^9.33.0",
31 | "eslint-plugin-react-hooks": "^5.2.0",
32 | "eslint-plugin-react-refresh": "^0.4.20",
33 | "globals": "^16.3.0",
34 | "vite": "^7.1.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 GreatStackDev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense and/or sell copies of the Software and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/features/themeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | theme: "light",
5 | };
6 |
7 | const themeSlice = createSlice({
8 | name: "theme",
9 | initialState,
10 | reducers: {
11 | toggleTheme: (state) => {
12 | const theme = state.theme === "light" ? "dark" : "light";
13 | localStorage.setItem("theme", theme);
14 | document.documentElement.classList.toggle("dark");
15 | state.theme = theme;
16 | },
17 | setTheme: (state, action) => {
18 | state.theme = action.payload;
19 | },
20 | loadTheme: (state) => {
21 | const theme = localStorage.getItem("theme");
22 | if (theme) {
23 | state.theme = theme;
24 | if (theme === "dark") {
25 | document.documentElement.classList.add("dark");
26 | }
27 | }
28 | },
29 | },
30 | });
31 |
32 | export const { toggleTheme, setTheme, loadTheme } = themeSlice.actions;
33 | export default themeSlice.reducer;
--------------------------------------------------------------------------------
/src/pages/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import Navbar from '../components/Navbar'
3 | import Sidebar from '../components/Sidebar'
4 | import { Outlet } from 'react-router-dom'
5 | import { useDispatch, useSelector } from 'react-redux'
6 | import { loadTheme } from '../features/themeSlice'
7 | import { Loader2Icon } from 'lucide-react'
8 |
9 | const Layout = () => {
10 | const [isSidebarOpen, setIsSidebarOpen] = useState(false)
11 | const { loading } = useSelector((state) => state.workspace)
12 | const dispatch = useDispatch()
13 |
14 | // Initial load of theme
15 | useEffect(() => {
16 | dispatch(loadTheme())
17 | }, [])
18 |
19 | if (loading) return (
20 |
21 |
22 |
23 | )
24 |
25 | return (
26 |
35 | )
36 | }
37 |
38 | export default Layout
39 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
2 | @import "tailwindcss";
3 |
4 | * {
5 | font-family: 'Outfit', sans-serif;
6 | }
7 |
8 | @custom-variant dark (&:where(.dark, .dark *));
9 |
10 | .no-scrollbar::-webkit-scrollbar {
11 | display: none;
12 | }
13 |
14 | @layer base {
15 | button {
16 | cursor: pointer;
17 | }
18 |
19 | ::-webkit-scrollbar {
20 | width: 0.5rem;
21 | }
22 |
23 | ::-webkit-scrollbar-track {
24 | background-color: #e5e7eb;
25 | }
26 |
27 | .dark ::-webkit-scrollbar-track {
28 | background-color: #1c1c1e;
29 | }
30 |
31 | ::-webkit-scrollbar-thumb {
32 | background-color: #bbc3d1;
33 | border-radius: 0.375rem;
34 | }
35 |
36 | .dark ::-webkit-scrollbar-thumb {
37 | background-color: #374151;
38 | }
39 |
40 | ::-webkit-scrollbar-thumb:hover {
41 | background-color: #99a5bc;
42 | }
43 |
44 | .dark ::-webkit-scrollbar-thumb:hover {
45 | background-color: #4b5563;
46 | }
47 |
48 | .dark input[type="date"]::-webkit-calendar-picker-indicator {
49 | filter: invert(100%);
50 | }
51 |
52 | input[type="checkbox"] {
53 | appearance: none;
54 | background-color: #d1d5db;
55 | border-radius: 9999px;
56 | cursor: pointer;
57 | width: 1rem;
58 | height: 1rem;
59 | }
60 |
61 | .dark input[type="checkbox"] {
62 | background-color: #374151;
63 | }
64 |
65 | input[type="checkbox"]:checked {
66 | background-color: #3b82f6;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/assets/profile_img_o.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { Plus } from 'lucide-react'
2 | import { useState } from 'react'
3 | import StatsGrid from '../components/StatsGrid'
4 | import ProjectOverview from '../components/ProjectOverview'
5 | import RecentActivity from '../components/RecentActivity'
6 | import TasksSummary from '../components/TasksSummary'
7 | import CreateProjectDialog from '../components/CreateProjectDialog'
8 |
9 | const Dashboard = () => {
10 |
11 | const user = { fullName: 'User' }
12 | const [isDialogOpen, setIsDialogOpen] = useState(false)
13 |
14 | return (
15 |
16 |
17 |
18 |
Welcome back, {user?.fullName || 'User'}
19 |
Here's what's happening with your projects today
20 |
21 |
22 |
setIsDialogOpen(true)} className="flex items-center gap-2 px-5 py-2 text-sm rounded bg-gradient-to-br from-blue-500 to-blue-600 text-white space-x-2 hover:opacity-90 transition" >
23 | New Project
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
40 |
41 | )
42 | }
43 |
44 | export default Dashboard
45 |
--------------------------------------------------------------------------------
/src/components/ProjectCard.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const statusColors = {
4 | PLANNING: "bg-gray-200 dark:bg-zinc-600 text-gray-900 dark:text-zinc-200",
5 | ACTIVE: "bg-emerald-200 dark:bg-emerald-500 text-emerald-900 dark:text-emerald-900",
6 | ON_HOLD: "bg-amber-200 dark:bg-amber-500 text-amber-900 dark:text-amber-900",
7 | COMPLETED: "bg-blue-200 dark:bg-blue-500 text-blue-900 dark:text-blue-900",
8 | CANCELLED: "bg-red-200 dark:bg-red-500 text-red-900 dark:text-red-900",
9 | };
10 |
11 | const ProjectCard = ({ project }) => {
12 | return (
13 |
14 | {/* Header */}
15 |
16 |
17 |
18 | {project.name}
19 |
20 |
21 | {project.description || "No description"}
22 |
23 |
24 |
25 |
26 |
27 |
28 | {project.status.replace("_", " ")}
29 |
30 |
31 | {project.priority} priority
32 |
33 |
34 |
35 | {/* Progress */}
36 |
37 |
38 | Progress
39 | {project.progress || 0}%
40 |
41 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ProjectCard;
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | project-management
4 |
5 | An open-source project management platform built with ReactJS and Tailwind CSS.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ---
15 |
16 | ## 📖 Table of Contents
17 |
18 | - [✨ Features](#-features)
19 | - [🛠️ Tech Stack](#-tech-stack)
20 | - [🚀 Getting Started](#-getting-started)
21 | - [🤝 Contributing](#-contributing)
22 | - [📜 License](#-license)
23 |
24 | ---
25 |
26 | ## 📝 Features
27 |
28 | - **Multiple Workspaces:** Allow multiple workspaces to be created, each with its own set of projects, tasks, and members.
29 | - **Project Management:** Manage projects, tasks, and team members.
30 | - **Analytics:** View project analytics, including progress, completion rate, and team size.
31 | - **Task Management:** Assign tasks to team members, set due dates, and track task status.
32 | - **User Management:** Invite team members, manage user roles, and view user activity.
33 |
34 | ## 🛠️ Tech Stack
35 |
36 | - **Framework:** ReactJS
37 | - **Styling:** Tailwind CSS
38 | - **UI Components:** Lucide React for icons
39 | - **State Management:** Redux Toolkit
40 |
41 | ## 🚀 Getting Started
42 |
43 | First, install the dependencies. We recommend using `npm` for this project.
44 |
45 | ```bash
46 | npm install
47 | ```
48 |
49 | Then, run the development server:
50 |
51 | ```bash
52 | npm run dev
53 | # or
54 | yarn dev
55 | # or
56 | pnpm dev
57 | # or
58 | bun dev
59 | ```
60 |
61 | Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
62 |
63 | You can start editing the page by modifying `src/App.jsx`. The page auto-updates as you edit the file.
64 |
65 | ---
66 |
67 | ## 🤝 Contributing
68 |
69 | We welcome contributions! Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for more details on how to get started.
70 |
71 | ---
72 |
73 | ## 📜 License
74 |
75 | This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.md) file for details.
76 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { SearchIcon, PanelLeft } from 'lucide-react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { toggleTheme } from '../features/themeSlice'
4 | import { MoonIcon, SunIcon } from 'lucide-react'
5 | import { assets } from '../assets/assets'
6 |
7 | const Navbar = ({ setIsSidebarOpen }) => {
8 |
9 | const dispatch = useDispatch();
10 | const { theme } = useSelector(state => state.theme);
11 |
12 | return (
13 |
14 |
15 | {/* Left section */}
16 |
17 | {/* Sidebar Trigger */}
18 |
setIsSidebarOpen((prev) => !prev)} className="sm:hidden p-2 rounded-lg transition-colors text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-800" >
19 |
20 |
21 |
22 | {/* Search Input */}
23 |
24 |
25 |
30 |
31 |
32 |
33 | {/* Right section */}
34 |
35 |
36 | {/* Theme Toggle */}
37 |
dispatch(toggleTheme())} className="size-8 flex items-center justify-center bg-white dark:bg-zinc-800 shadow rounded-lg transition hover:scale-105 active:scale-95">
38 | {
39 | theme === "light"
40 | ? ( )
41 | : ( )
42 | }
43 |
44 |
45 | {/* User Button */}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default Navbar
54 |
--------------------------------------------------------------------------------
/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { NavLink } from 'react-router-dom'
3 | import MyTasksSidebar from './MyTasksSidebar'
4 | import ProjectSidebar from './ProjectsSidebar'
5 | import WorkspaceDropdown from './WorkspaceDropdown'
6 | import { FolderOpenIcon, LayoutDashboardIcon, SettingsIcon, UsersIcon } from 'lucide-react'
7 |
8 | const Sidebar = ({ isSidebarOpen, setIsSidebarOpen }) => {
9 |
10 | const menuItems = [
11 | { name: 'Dashboard', href: '/', icon: LayoutDashboardIcon },
12 | { name: 'Projects', href: '/projects', icon: FolderOpenIcon },
13 | { name: 'Team', href: '/team', icon: UsersIcon },
14 | ]
15 |
16 | const sidebarRef = useRef(null);
17 |
18 | useEffect(() => {
19 | function handleClickOutside(event) {
20 | if (sidebarRef.current && !sidebarRef.current.contains(event.target)) {
21 | setIsSidebarOpen(false);
22 | }
23 | }
24 | document.addEventListener("mousedown", handleClickOutside);
25 | return () => document.removeEventListener("mousedown", handleClickOutside);
26 | }, [setIsSidebarOpen]);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | {menuItems.map((item) => (
36 |
`flex items-center gap-3 py-2 px-4 text-gray-800 dark:text-zinc-100 cursor-pointer rounded transition-all ${isActive ? 'bg-gray-100 dark:bg-zinc-900 dark:bg-gradient-to-br dark:from-zinc-800 dark:to-zinc-800/50 dark:ring-zinc-800' : 'hover:bg-gray-50 dark:hover:bg-zinc-800/60'}`} >
37 |
38 | {item.name}
39 |
40 | ))}
41 |
42 |
43 | Settings
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default Sidebar
58 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Project Management
2 |
3 | Thank you for considering contributing to **Project Management**!
4 | We welcome contributions from everyone, whether it's fixing a bug, adding a new feature, or optimizing the codebase.
5 |
6 | ---
7 |
8 | ## Table of Contents
9 |
10 | - [How to Contribute](#how-to-contribute)
11 | - [Development Setup](#development-setup)
12 | - [Contribution Guidelines](#contribution-guidelines)
13 | - [Ideas for Contribution](#ideas-for-contribution)
14 |
15 | ---
16 |
17 | ## How to Contribute
18 |
19 | 1. **Fork** the repo
20 | 2. **Create a new branch** (example: `git checkout -b feature/added-about-page`)
21 | 3. **Make your changes** (UI, animations, pages, etc.)
22 | 4. **Commit and push**
23 | 5. **Open a Pull Request (PR)**
24 |
25 | ---
26 |
27 | ## Development Setup
28 |
29 | - Use **ReactJS** and **Tailwind CSS**
30 | - Run `npm run dev` for local development.
31 | - Keep code **clean, modular and reusable**
32 | - Prefer functional components and hooks
33 | - Follow existing folder structure and naming conventions
34 |
35 | ---
36 |
37 | ## Contribution Guidelines
38 |
39 | - **Small, Focused PRs** → Don’t bundle unrelated changes in one PR
40 | - **Commit Messages** → Use clear and descriptive messages (e.g., `feat: add new feature`, `fix: resolve issue #123`).
41 | - **Code Style** → Follow the existing code style (e.g., indentation, naming conventions, etc.).
42 | - **Accessibility** → Ensure that the website is accessible to all users
43 | - **Discussions First** → For large changes (new features, big design changes), discuss the changes first to avoid wasting time on implementation.
44 | - **Respect Others** → Follow the [Code of Conduct](./CODE_OF_CONDUCT.md)
45 |
46 | ---
47 |
48 | ## Ideas for Contribution
49 |
50 | Here are some areas where you can contribute to improve and expand the Project Management app:
51 |
52 | ### Core UI Features
53 |
54 | - **Project Management**
55 | - Enhance **project list views** and **project detail pages**
56 | - Add **project cards** with status, progress, and deadlines
57 | - Implement **project filtering and sorting** UI
58 |
59 | - **Task Management**
60 | - Build **Kanban-style drag-and-drop boards** for tasks
61 | - Enhance UI for **task comments, subtasks, and attachments**
62 | - Implement **task assignment indicators** and quick actions
63 |
64 | ---
65 |
66 | ### 🎨 UI/UX Improvements
67 | - Improve **responsive design** for mobile and tablet
68 | - Create **skeleton loaders** and **loading states**
69 | - Enhance **accessibility** (keyboard navigation, ARIA roles, color contrast)
70 | - Improve **dashboard layout** with analytics cards (task progress, project summary)
71 |
72 | ---
73 |
74 | ### ⚙️ Frontend Technical Enhancements
75 | - Refactor UI components into **reusable and modular components**
76 | - Improve **form handling and validation**
77 | - Add **error boundaries** and **fallback UI components**
78 |
79 | ---
80 |
81 | ### 🧩 Interactivity & Animations
82 | - Enhance **drag-and-drop interactions** for tasks/projects
83 | - Enhance **smooth animations** for modals, page transitions, and task movements
84 | - Implement **interactive filters and search bars**
85 | - Add **tooltips, popovers, and hover effects** for better UX
86 |
--------------------------------------------------------------------------------
/src/components/InviteMemberDialog.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Mail, UserPlus } from "lucide-react";
3 | import { useSelector } from "react-redux";
4 |
5 | const InviteMemberDialog = ({ isDialogOpen, setIsDialogOpen }) => {
6 |
7 | const currentWorkspace = useSelector((state) => state.workspace?.currentWorkspace || null);
8 | const [isSubmitting, setIsSubmitting] = useState(false);
9 | const [formData, setFormData] = useState({
10 | email: "",
11 | role: "org:member",
12 | });
13 |
14 | const handleSubmit = async (e) => {
15 | e.preventDefault();
16 |
17 | };
18 |
19 | if (!isDialogOpen) return null;
20 |
21 | return (
22 |
23 |
24 | {/* Header */}
25 |
26 |
27 | Invite Team Member
28 |
29 | {currentWorkspace && (
30 |
31 | Inviting to workspace: {currentWorkspace.name}
32 |
33 | )}
34 |
35 |
36 | {/* Form */}
37 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default InviteMemberDialog;
74 |
--------------------------------------------------------------------------------
/src/components/AddProjectMember.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Mail, UserPlus } from "lucide-react";
3 | import { useSelector } from "react-redux";
4 | import { useSearchParams } from "react-router-dom";
5 |
6 | const AddProjectMember = ({ isDialogOpen, setIsDialogOpen }) => {
7 |
8 | const [searchParams] = useSearchParams();
9 |
10 | const id = searchParams.get('id');
11 |
12 | const currentWorkspace = useSelector((state) => state.workspace?.currentWorkspace || null);
13 |
14 | const project = currentWorkspace?.projects.find((p) => p.id === id);
15 | const projectMembersEmails = project?.members.map((member) => member.user.email);
16 |
17 | const [email, setEmail] = useState('');
18 | const [isAdding, setIsAdding] = useState(false);
19 |
20 | const handleSubmit = async (e) => {
21 | e.preventDefault();
22 |
23 | };
24 |
25 | if (!isDialogOpen) return null;
26 |
27 | return (
28 |
29 |
30 | {/* Header */}
31 |
32 |
33 | Add Member to Project
34 |
35 | {currentWorkspace && (
36 |
37 | Adding to Project: {project.name}
38 |
39 | )}
40 |
41 |
42 | {/* Form */}
43 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default AddProjectMember;
79 |
--------------------------------------------------------------------------------
/src/components/MyTasksSidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { CheckSquareIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
3 | import { useSelector } from 'react-redux';
4 | import { Link } from 'react-router-dom';
5 |
6 | function MyTasksSidebar() {
7 |
8 | const user = { id: 'user_1' }
9 |
10 | const { currentWorkspace } = useSelector((state) => state.workspace);
11 | const [showMyTasks, setShowMyTasks] = useState(false);
12 | const [myTasks, setMyTasks] = useState([]);
13 |
14 | const toggleMyTasks = () => setShowMyTasks(prev => !prev);
15 |
16 | const getTaskStatusColor = (status) => {
17 | switch (status) {
18 | case 'DONE':
19 | return 'bg-green-500';
20 | case 'IN_PROGRESS':
21 | return 'bg-yellow-500';
22 | case 'TODO':
23 | return 'bg-gray-500 dark:bg-zinc-500';
24 | default:
25 | return 'bg-gray-400 dark:bg-zinc-400';
26 | }
27 | };
28 |
29 | const fetchUserTasks = () => {
30 | const userId = user?.id || '';
31 | if (!userId || !currentWorkspace) return;
32 | const currentWorkspaceTasks = currentWorkspace.projects.flatMap((project) => {
33 | return project.tasks.filter((task) => task?.assignee?.id === userId);
34 | });
35 |
36 | setMyTasks(currentWorkspaceTasks);
37 | }
38 |
39 | useEffect(() => {
40 | fetchUserTasks()
41 | }, [currentWorkspace])
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
My Tasks
49 |
50 | {myTasks.length}
51 |
52 |
53 | {showMyTasks ? (
54 |
55 | ) : (
56 |
57 | )}
58 |
59 |
60 | {showMyTasks && (
61 |
62 |
63 | {myTasks.length === 0 ? (
64 |
65 | No tasks assigned
66 |
67 | ) : (
68 | myTasks.map((task, index) => (
69 |
70 |
71 |
72 |
73 |
74 | {task.title}
75 |
76 |
77 | {task.status.replace('_', ' ')}
78 |
79 |
80 |
81 |
82 | ))
83 | )}
84 |
85 |
86 | )}
87 |
88 | );
89 | }
90 |
91 | export default MyTasksSidebar;
92 |
--------------------------------------------------------------------------------
/src/components/ProjectsSidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link, useLocation, useSearchParams } from 'react-router-dom';
3 | import { ChevronRightIcon, SettingsIcon, KanbanIcon, ChartColumnIcon, CalendarIcon, ArrowRightIcon } from 'lucide-react';
4 | import { useSelector } from 'react-redux';
5 |
6 | const ProjectSidebar = () => {
7 |
8 | const location = useLocation();
9 |
10 | const [expandedProjects, setExpandedProjects] = useState(new Set());
11 | const [searchParams] = useSearchParams();
12 |
13 | const projects = useSelector(
14 | (state) => state?.workspace?.currentWorkspace?.projects || []
15 | );
16 |
17 | const getProjectSubItems = (projectId) => [
18 | { title: 'Tasks', icon: KanbanIcon, url: `/projectsDetail?id=${projectId}&tab=tasks` },
19 | { title: 'Analytics', icon: ChartColumnIcon, url: `/projectsDetail?id=${projectId}&tab=analytics` },
20 | { title: 'Calendar', icon: CalendarIcon, url: `/projectsDetail?id=${projectId}&tab=calendar` },
21 | { title: 'Settings', icon: SettingsIcon, url: `/projectsDetail?id=${projectId}&tab=settings` }
22 | ];
23 |
24 | const toggleProject = (id) => {
25 | const newSet = new Set(expandedProjects);
26 | newSet.has(id) ? newSet.delete(id) : newSet.add(id);
27 | setExpandedProjects(newSet);
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 | Projects
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {projects.map((project) => (
45 |
46 |
toggleProject(project.id)} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors duration-200 text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-800 hover:text-gray-900 dark:hover:text-white" >
47 |
48 |
49 | {project.name}
50 |
51 |
52 | {expandedProjects.has(project.id) && (
53 |
54 | {getProjectSubItems(project.id).map((subItem) => {
55 | // checking if the current path matches the sub-item's URL
56 | const isActive =
57 | location.pathname === `/projectsDetail` &&
58 | searchParams.get('id') === project.id &&
59 | searchParams.get('tab') === subItem.title.toLowerCase();
60 |
61 | return (
62 |
63 |
64 | {subItem.title}
65 |
66 | );
67 | })}
68 |
69 | )}
70 |
71 | ))}
72 |
73 |
74 | );
75 | };
76 |
77 | export default ProjectSidebar;
--------------------------------------------------------------------------------
/src/assets/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | previewFeatures = ["driverAdapters"]
4 | }
5 |
6 | datasource db {
7 | provider = "postgresql"
8 | url = env("DATABASE_URL")
9 | directUrl = env("DIRECT_URL")
10 | }
11 |
12 | enum WorkspaceRole {
13 | ADMIN
14 | MEMBER
15 | }
16 |
17 | enum TaskStatus {
18 | TODO
19 | IN_PROGRESS
20 | DONE
21 | }
22 |
23 | enum TaskType {
24 | TASK
25 | BUG
26 | FEATURE
27 | IMPROVEMENT
28 | OTHER
29 | }
30 |
31 | enum ProjectStatus {
32 | ACTIVE
33 | PLANNING
34 | COMPLETED
35 | ON_HOLD
36 | CANCELLED
37 | }
38 |
39 | enum Priority {
40 | LOW
41 | MEDIUM
42 | HIGH
43 | }
44 |
45 | model User {
46 | id String @id
47 | name String
48 | email String @unique
49 | image String @default("")
50 | createdAt DateTime @default(now())
51 | updatedAt DateTime @updatedAt
52 |
53 | workspaces WorkspaceMember[]
54 | projects Project[] @relation("ProjectOwner")
55 | tasks Task[] @relation("TaskAssignee")
56 | comments Comment[]
57 | ownedWorkspaces Workspace[]
58 | ProjectMember ProjectMember[]
59 | }
60 |
61 | model Workspace {
62 | id String @id
63 | name String
64 | slug String @unique
65 | description String?
66 | settings Json @default("{}")
67 | ownerId String
68 | createdAt DateTime @default(now())
69 | image_url String @default("")
70 | updatedAt DateTime @updatedAt
71 |
72 | members WorkspaceMember[]
73 | projects Project[]
74 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
75 | }
76 |
77 | model WorkspaceMember {
78 | id String @id @default(uuid())
79 | userId String
80 | workspaceId String
81 | message String @default("")
82 | role WorkspaceRole @default(MEMBER)
83 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
84 | workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
85 |
86 | @@unique([userId, workspaceId]) // prevent duplicate membership
87 | }
88 |
89 | model Project {
90 | id String @id @default(uuid())
91 | name String
92 | description String?
93 | priority Priority @default(MEDIUM)
94 | status ProjectStatus @default(ACTIVE)
95 | start_date DateTime?
96 | end_date DateTime?
97 | team_lead String
98 | workspaceId String
99 | progress Int @default(0)
100 | createdAt DateTime @default(now())
101 | updatedAt DateTime @updatedAt
102 | members ProjectMember[]
103 |
104 | owner User @relation("ProjectOwner", fields: [team_lead], references: [id], onDelete: Cascade)
105 | workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
106 | tasks Task[]
107 | }
108 |
109 | model ProjectMember {
110 | id String @id @default(uuid())
111 | userId String
112 | projectId String
113 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
114 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
115 |
116 | @@unique([userId, projectId]) // prevent duplicate membership
117 | }
118 |
119 | model Task {
120 | id String @id @default(uuid())
121 | projectId String
122 | title String
123 | description String?
124 | status TaskStatus @default(TODO)
125 | type TaskType @default(TASK)
126 | priority Priority @default(MEDIUM)
127 | assigneeId String
128 | due_date DateTime
129 | createdAt DateTime @default(now())
130 | updatedAt DateTime @updatedAt
131 |
132 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
133 | assignee User @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: Cascade)
134 | comments Comment[]
135 | }
136 |
137 | model Comment {
138 | id String @id @default(uuid())
139 | content String
140 | userId String
141 | taskId String
142 | createdAt DateTime @default(now())
143 |
144 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
145 | task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
146 | }
147 |
--------------------------------------------------------------------------------
/src/components/WorkspaceDropdown.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from "react";
2 | import { ChevronDown, Check, Plus } from "lucide-react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { setCurrentWorkspace } from "../features/workspaceSlice";
5 | import { useNavigate } from "react-router-dom";
6 | import { dummyWorkspaces } from "../assets/assets";
7 |
8 | function WorkspaceDropdown() {
9 |
10 | const { workspaces } = useSelector((state) => state.workspace);
11 | const currentWorkspace = useSelector((state) => state.workspace?.currentWorkspace || null);
12 | const [isOpen, setIsOpen] = useState(false);
13 | const dropdownRef = useRef(null);
14 |
15 | const dispatch = useDispatch();
16 | const navigate = useNavigate();
17 |
18 | const onSelectWorkspace = (organizationId) => {
19 | dispatch(setCurrentWorkspace(organizationId))
20 | setIsOpen(false);
21 | navigate('/')
22 | }
23 |
24 | // Close dropdown on outside click
25 | useEffect(() => {
26 | function handleClickOutside(event) {
27 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
28 | setIsOpen(false);
29 | }
30 | }
31 | document.addEventListener("mousedown", handleClickOutside);
32 | return () => document.removeEventListener("mousedown", handleClickOutside);
33 | }, []);
34 |
35 | return (
36 |
37 |
setIsOpen(prev => !prev)} className="w-full flex items-center justify-between p-3 h-auto text-left rounded hover:bg-gray-100 dark:hover:bg-zinc-800" >
38 |
39 |
40 |
41 |
42 | {currentWorkspace?.name || "Select Workspace"}
43 |
44 |
45 | {workspaces.length} workspace{workspaces.length !== 1 ? "s" : ""}
46 |
47 |
48 |
49 |
50 |
51 |
52 | {isOpen && (
53 |
54 |
55 |
56 | Workspaces
57 |
58 | {dummyWorkspaces.map((ws) => (
59 |
onSelectWorkspace(ws.id)} className="flex items-center gap-3 p-2 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800" >
60 |
61 |
62 |
63 | {ws.name}
64 |
65 |
66 | {ws.membersCount || 0} members
67 |
68 |
69 | {currentWorkspace?.id === ws.id && (
70 |
71 | )}
72 |
73 | ))}
74 |
75 |
76 |
77 |
78 |
79 |
80 | Create Workspace
81 |
82 |
83 |
84 | )}
85 |
86 | );
87 | }
88 |
89 | export default WorkspaceDropdown;
90 |
--------------------------------------------------------------------------------
/src/components/StatsGrid.jsx:
--------------------------------------------------------------------------------
1 | import { FolderOpen, CheckCircle, Users, AlertTriangle } from "lucide-react";
2 | import { useEffect, useState } from "react";
3 | import { useSelector } from "react-redux";
4 |
5 | export default function StatsGrid() {
6 | const currentWorkspace = useSelector(
7 | (state) => state?.workspace?.currentWorkspace || null
8 | );
9 |
10 | const [stats, setStats] = useState({
11 | totalProjects: 0,
12 | activeProjects: 0,
13 | completedProjects: 0,
14 | myTasks: 0,
15 | overdueIssues: 0,
16 | });
17 |
18 | const statCards = [
19 | {
20 | icon: FolderOpen,
21 | title: "Total Projects",
22 | value: stats.totalProjects,
23 | subtitle: `projects in ${currentWorkspace?.name}`,
24 | bgColor: "bg-blue-500/10",
25 | textColor: "text-blue-500",
26 | },
27 | {
28 | icon: CheckCircle,
29 | title: "Completed Projects",
30 | value: stats.completedProjects,
31 | subtitle: `of ${stats.totalProjects} total`,
32 | bgColor: "bg-emerald-500/10",
33 | textColor: "text-emerald-500",
34 | },
35 | {
36 | icon: Users,
37 | title: "My Tasks",
38 | value: stats.myTasks,
39 | subtitle: "assigned to me",
40 | bgColor: "bg-purple-500/10",
41 | textColor: "text-purple-500",
42 | },
43 | {
44 | icon: AlertTriangle,
45 | title: "Overdue",
46 | value: stats.overdueIssues,
47 | subtitle: "need attention",
48 | bgColor: "bg-amber-500/10",
49 | textColor: "text-amber-500",
50 | },
51 | ];
52 |
53 | useEffect(() => {
54 | if (currentWorkspace) {
55 | setStats({
56 | totalProjects: currentWorkspace.projects.length,
57 | activeProjects: currentWorkspace.projects.filter(
58 | (p) => p.status !== "CANCELLED" && p.status !== "COMPLETED"
59 | ).length,
60 | completedProjects: currentWorkspace.projects
61 | .filter((p) => p.status === "COMPLETED")
62 | .reduce((acc, project) => acc + project.tasks.length, 0),
63 | myTasks: currentWorkspace.projects.reduce(
64 | (acc, project) =>
65 | acc +
66 | project.tasks.filter(
67 | (t) => t.assignee?.email === currentWorkspace.owner.email
68 | ).length,
69 | 0
70 | ),
71 | overdueIssues: currentWorkspace.projects.reduce(
72 | (acc, project) =>
73 | acc + project.tasks.filter((t) => t.due_date < new Date()).length,
74 | 0
75 | ),
76 | });
77 | }
78 | }, [currentWorkspace]);
79 |
80 | return (
81 |
82 | {statCards.map(
83 | ({ icon: Icon, title, value, subtitle, bgColor, textColor }, i) => (
84 |
85 |
86 |
87 |
88 |
89 | {title}
90 |
91 |
92 | {value}
93 |
94 | {subtitle && (
95 |
96 | {subtitle}
97 |
98 | )}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | )
107 | )}
108 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/TasksSummary.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { ArrowRight, Clock, AlertTriangle, User } from "lucide-react";
3 | import { useSelector } from "react-redux";
4 |
5 | export default function TasksSummary() {
6 |
7 | const { currentWorkspace } = useSelector((state) => state.workspace);
8 | const user = { id: 'user_1' }
9 | const [tasks, setTasks] = useState([]);
10 |
11 | // Get all tasks for all projects in current workspace
12 | useEffect(() => {
13 | if (currentWorkspace) {
14 | setTasks(currentWorkspace.projects.flatMap((project) => project.tasks));
15 | }
16 | }, [currentWorkspace]);
17 |
18 | const myTasks = tasks.filter(i => i.assigneeId === user.id);
19 | const overdueTasks = tasks.filter(t => t.due_date && new Date(t.due_date) < new Date() && t.status !== 'DONE');
20 | const inProgressIssues = tasks.filter(i => i.status === 'IN_PROGRESS');
21 |
22 | const summaryCards = [
23 | {
24 | title: "My Tasks",
25 | count: myTasks.length,
26 | icon: User,
27 | color: "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-400",
28 | items: myTasks.slice(0, 3)
29 | },
30 | {
31 | title: "Overdue",
32 | count: overdueTasks.length,
33 | icon: AlertTriangle,
34 | color: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-400",
35 | items: overdueTasks.slice(0, 3)
36 | },
37 | {
38 | title: "In Progress",
39 | count: inProgressIssues.length,
40 | icon: Clock,
41 | color: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-400",
42 | items: inProgressIssues.slice(0, 3)
43 | }
44 | ];
45 |
46 | return (
47 |
48 | {summaryCards.map((card) => (
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
{card.title}
57 |
58 | {card.count}
59 |
60 |
61 |
62 |
63 |
64 | {card.items.length === 0 ? (
65 |
66 | No {card.title.toLowerCase()}
67 |
68 | ) : (
69 |
70 | {card.items.map((issue) => (
71 |
72 |
73 | {issue.title}
74 |
75 |
76 | {issue.type} • {issue.priority} priority
77 |
78 |
79 | ))}
80 | {card.count > 3 && (
81 |
82 | View {card.count - 3} more
83 |
84 | )}
85 |
86 | )}
87 |
88 |
89 | ))}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/features/workspaceSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { dummyWorkspaces } from "../assets/assets";
3 |
4 | const initialState = {
5 | workspaces: dummyWorkspaces || [],
6 | currentWorkspace: dummyWorkspaces[1],
7 | loading: false,
8 | };
9 |
10 | const workspaceSlice = createSlice({
11 | name: "workspace",
12 | initialState,
13 | reducers: {
14 | setWorkspaces: (state, action) => {
15 | state.workspaces = action.payload;
16 | },
17 | setCurrentWorkspace: (state, action) => {
18 | localStorage.setItem("currentWorkspaceId", action.payload);
19 | state.currentWorkspace = state.workspaces.find((w) => w.id === action.payload);
20 | },
21 | addWorkspace: (state, action) => {
22 | state.workspaces.push(action.payload);
23 |
24 | // set current workspace to the new workspace
25 | if (state.currentWorkspace?.id !== action.payload.id) {
26 | state.currentWorkspace = action.payload;
27 | }
28 | },
29 | updateWorkspace: (state, action) => {
30 | state.workspaces = state.workspaces.map((w) =>
31 | w.id === action.payload.id ? action.payload : w
32 | );
33 |
34 | // if current workspace is updated, set it to the updated workspace
35 | if (state.currentWorkspace?.id === action.payload.id) {
36 | state.currentWorkspace = action.payload;
37 | }
38 | },
39 | deleteWorkspace: (state, action) => {
40 | state.workspaces = state.workspaces.filter((w) => w._id !== action.payload);
41 | },
42 | addProject: (state, action) => {
43 | state.currentWorkspace.projects.push(action.payload);
44 | // find workspace by id and add project to it
45 | state.workspaces = state.workspaces.map((w) =>
46 | w.id === state.currentWorkspace.id ? { ...w, projects: w.projects.concat(action.payload) } : w
47 | );
48 | },
49 | addTask: (state, action) => {
50 |
51 | state.currentWorkspace.projects = state.currentWorkspace.projects.map((p) => {
52 | console.log(p.id, action.payload.projectId, p.id === action.payload.projectId);
53 | if (p.id === action.payload.projectId) {
54 | p.tasks.push(action.payload);
55 | }
56 | return p;
57 | });
58 |
59 | // find workspace and project by id and add task to it
60 | state.workspaces = state.workspaces.map((w) =>
61 | w.id === state.currentWorkspace.id ? {
62 | ...w, projects: w.projects.map((p) =>
63 | p.id === action.payload.projectId ? { ...p, tasks: p.tasks.concat(action.payload) } : p
64 | )
65 | } : w
66 | );
67 | },
68 | updateTask: (state, action) => {
69 | state.currentWorkspace.projects.map((p) => {
70 | if (p.id === action.payload.projectId) {
71 | p.tasks = p.tasks.map((t) =>
72 | t.id === action.payload.id ? action.payload : t
73 | );
74 | }
75 | });
76 | // find workspace and project by id and update task in it
77 | state.workspaces = state.workspaces.map((w) =>
78 | w.id === state.currentWorkspace.id ? {
79 | ...w, projects: w.projects.map((p) =>
80 | p.id === action.payload.projectId ? {
81 | ...p, tasks: p.tasks.map((t) =>
82 | t.id === action.payload.id ? action.payload : t
83 | )
84 | } : p
85 | )
86 | } : w
87 | );
88 | },
89 | deleteTask: (state, action) => {
90 | state.currentWorkspace.projects.map((p) => {
91 | p.tasks = p.tasks.filter((t) => !action.payload.includes(t.id));
92 | return p;
93 | });
94 | // find workspace and project by id and delete task from it
95 | state.workspaces = state.workspaces.map((w) =>
96 | w.id === state.currentWorkspace.id ? {
97 | ...w, projects: w.projects.map((p) =>
98 | p.id === action.payload.projectId ? {
99 | ...p, tasks: p.tasks.filter((t) => !action.payload.includes(t.id))
100 | } : p
101 | )
102 | } : w
103 | );
104 | }
105 |
106 | }
107 | });
108 |
109 | export const { setWorkspaces, setCurrentWorkspace, addWorkspace, updateWorkspace, deleteWorkspace, addProject, addTask, updateTask, deleteTask } = workspaceSlice.actions;
110 | export default workspaceSlice.reducer;
--------------------------------------------------------------------------------
/src/components/RecentActivity.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { GitCommit, MessageSquare, Clock, Bug, Zap, Square } from "lucide-react";
3 | import { format } from "date-fns";
4 | import { useSelector } from "react-redux";
5 |
6 | const typeIcons = {
7 | BUG: { icon: Bug, color: "text-red-500 dark:text-red-400" },
8 | FEATURE: { icon: Zap, color: "text-blue-500 dark:text-blue-400" },
9 | TASK: { icon: Square, color: "text-green-500 dark:text-green-400" },
10 | IMPROVEMENT: { icon: MessageSquare, color: "text-amber-500 dark:text-amber-400" },
11 | OTHER: { icon: GitCommit, color: "text-purple-500 dark:text-purple-400" },
12 | };
13 |
14 | const statusColors = {
15 | TODO: "bg-zinc-200 text-zinc-800 dark:bg-zinc-600 dark:text-zinc-200",
16 | IN_PROGRESS: "bg-amber-200 text-amber-800 dark:bg-amber-500 dark:text-amber-900",
17 | DONE: "bg-emerald-200 text-emerald-800 dark:bg-emerald-500 dark:text-emerald-900",
18 | };
19 |
20 | const RecentActivity = () => {
21 | const [tasks, setTasks] = useState([]);
22 | const { currentWorkspace } = useSelector((state) => state.workspace);
23 |
24 | const getTasksFromCurrentWorkspace = () => {
25 |
26 | if (!currentWorkspace) return;
27 |
28 | const tasks = currentWorkspace.projects.flatMap((project) => project.tasks.map((task) => task));
29 | setTasks(tasks);
30 | };
31 |
32 | useEffect(() => {
33 | getTasksFromCurrentWorkspace();
34 | }, [currentWorkspace]);
35 |
36 | return (
37 |
38 |
39 |
Recent Activity
40 |
41 |
42 |
43 | {tasks.length === 0 ? (
44 |
45 |
46 |
47 |
48 |
No recent activity
49 |
50 | ) : (
51 |
52 | {tasks.map((task) => {
53 | const TypeIcon = typeIcons[task.type]?.icon || Square;
54 | const iconColor = typeIcons[task.type]?.color || "text-gray-500 dark:text-gray-400";
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {task.title}
66 |
67 |
68 | {task.status.replace("_", " ")}
69 |
70 |
71 |
72 |
{task.type.toLowerCase()}
73 | {task.assignee && (
74 |
75 |
76 | {task.assignee.name[0].toUpperCase()}
77 |
78 | {task.assignee.name}
79 |
80 | )}
81 |
82 | {format(new Date(task.updatedAt), "MMM d, h:mm a")}
83 |
84 |
85 |
86 |
87 |
88 | );
89 | })}
90 |
91 | )}
92 |
93 |
94 | );
95 | };
96 |
97 | export default RecentActivity;
98 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Version 2.1
4 |
5 | ## Our Pledge
6 |
7 | We as members, contributors and leaders pledge to make participation in our
8 | community a harassment-free experience for everyone, regardless of age, body
9 | size, visible or invisible disability, ethnicity, sex characteristics, gender
10 | identity and expression, level of experience, education, socio-economic status,
11 | nationality, personal appearance, race, religion or sexual identity
12 | and orientation.
13 |
14 | We pledge to act and interact in ways that contribute to an open, welcoming,
15 | diverse, inclusive and healthy community.
16 |
17 | ## Our Standards
18 |
19 | Examples of behavior that contributes to a positive environment for our
20 | community include:
21 |
22 | * Demonstrating empathy and kindness toward other people
23 | * Being respectful of differing opinions, viewpoints and experiences
24 | * Giving and gracefully accepting constructive feedback
25 | * Accepting responsibility and apologizing to those affected by our mistakes,
26 | and learning from the experience
27 | * Focusing on what is best not just for us as individuals, but for the
28 | overall community
29 |
30 | Examples of unacceptable behavior include:
31 |
32 | * The use of sexualized language or imagery and sexual attention or
33 | advances of any kind
34 | * Trolling, insulting or derogatory comments and personal or political attacks
35 | * Public or private harassment
36 | * Publishing others' private information, such as a physical or email
37 | address, without their explicit permission
38 | * Other conduct which could reasonably be considered inappropriate in a
39 | professional setting
40 |
41 | ## Enforcement Responsibilities
42 |
43 | Community leaders are responsible for clarifying and enforcing our standards of
44 | acceptable behavior and will take appropriate and fair corrective action in
45 | response to any behavior they deem inappropriate, threatening, offensive,
46 | or harmful.
47 |
48 | Community leaders have the right and responsibility to remove, edit or reject
49 | comments, commits, code, wiki edits, issues and other contributions that are
50 | not aligned to this Code of Conduct and will communicate reasons for moderation
51 | decisions when appropriate.
52 |
53 | ## Scope
54 |
55 | This Code of Conduct applies within all community spaces and also applies when
56 | an individual is officially representing the community in public spaces.
57 | Examples of representing our community include using an official e-mail address,
58 | posting via an official social media account or acting as an appointed
59 | representative at an online or offline event.
60 |
61 | ## Enforcement
62 |
63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
64 | reported to the community leaders responsible for enforcement at
65 | greatstackdev@gmail.com.
66 | All complaints will be reviewed and investigated promptly and fairly.
67 |
68 | All community leaders are obligated to respect the privacy and security of the
69 | reporter of any incident.
70 |
71 | ## Enforcement Guidelines
72 |
73 | Community leaders will follow these Community Impact Guidelines in determining
74 | the consequences for any action they deem in violation of this Code of Conduct:
75 |
76 | ### 1. Correction
77 |
78 | **Community Impact**: Use of inappropriate language or other behavior deemed
79 | unprofessional or unwelcome in the community.
80 |
81 | **Consequence**: A private, written warning from community leaders, providing
82 | clarity around the nature of the violation and an explanation of why the
83 | behavior was inappropriate. A public apology may be requested.
84 |
85 | ### 2. Warning
86 |
87 | **Community Impact**: A violation through a single incident or series
88 | of actions.
89 |
90 | **Consequence**: A warning with consequences for continued behavior. No
91 | interaction with the people involved, including unsolicited interaction with
92 | those enforcing the Code of Conduct, for a specified period of time. This
93 | includes avoiding interactions in community spaces as well as external channels
94 | like social media. Violating these terms may lead to a temporary or
95 | permanent ban.
96 |
97 | ### 3. Temporary Ban
98 |
99 | **Community Impact**: A serious violation of community standards, including
100 | sustained inappropriate behavior.
101 |
102 | **Consequence**: A temporary ban from any sort of interaction or public
103 | communication with the community for a specified period of time. No public or
104 | private interaction with the people involved, including unsolicited interaction
105 | with those enforcing the Code of Conduct, is allowed during this period.
106 | Violating these terms may lead to a permanent ban.
107 |
108 | ### 4. Permanent Ban
109 |
110 | **Community Impact**: Demonstrating a pattern of violation of community
111 | standards, including sustained inappropriate behavior, harassment of an
112 | individual or aggression toward or disparagement of classes of individuals.
113 |
114 | **Consequence**: A permanent ban from any sort of public interaction within
115 | the community.
116 |
117 | ## Attribution
118 |
119 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
120 | version 2.1, available at
121 | https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
122 |
123 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
124 | enforcement ladder](https://github.com/mozilla/diversity).
125 |
126 | [homepage]: https://www.contributor-covenant.org
127 |
128 | For answers to common questions about this code of conduct, see the FAQ at
129 | https://www.contributor-covenant.org/faq. Translations are available at
130 | https://www.contributor-covenant.org/translations.
--------------------------------------------------------------------------------
/src/pages/Projects.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useSelector } from "react-redux";
3 | import { Plus, Search, FolderOpen } from "lucide-react";
4 | import ProjectCard from "../components/ProjectCard";
5 | import CreateProjectDialog from "../components/CreateProjectDialog";
6 |
7 | export default function Projects() {
8 |
9 | const projects = useSelector(
10 | (state) => state?.workspace?.currentWorkspace?.projects || []
11 | );
12 |
13 | const [filteredProjects, setFilteredProjects] = useState([]);
14 | const [searchTerm, setSearchTerm] = useState("");
15 | const [isDialogOpen, setIsDialogOpen] = useState(false);
16 | const [filters, setFilters] = useState({
17 | status: "ALL",
18 | priority: "ALL",
19 | });
20 |
21 | const filterProjects = () => {
22 | let filtered = projects;
23 |
24 | if (searchTerm) {
25 | filtered = filtered.filter(
26 | (project) =>
27 | project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
28 | project.description?.toLowerCase().includes(searchTerm.toLowerCase())
29 | );
30 | }
31 |
32 | if (filters.status !== "ALL") {
33 | filtered = filtered.filter((project) => project.status === filters.status);
34 | }
35 |
36 | if (filters.priority !== "ALL") {
37 | filtered = filtered.filter(
38 | (project) => project.priority === filters.priority
39 | );
40 | }
41 |
42 | setFilteredProjects(filtered);
43 | };
44 |
45 | useEffect(() => {
46 | filterProjects();
47 | }, [projects, searchTerm, filters]);
48 |
49 | return (
50 |
51 | {/* Header */}
52 |
53 |
54 |
Projects
55 |
Manage and track your projects
56 |
57 |
setIsDialogOpen(true)} className="flex items-center px-5 py-2 text-sm rounded bg-gradient-to-br from-blue-500 to-blue-600 text-white hover:opacity-90 transition" >
58 | New Project
59 |
60 |
61 |
62 |
63 | {/* Search and Filters */}
64 |
65 |
66 |
67 | setSearchTerm(e.target.value)} value={searchTerm} className="w-full pl-10 text-sm pr-4 py-2 rounded-lg border border-gray-300 dark:border-zinc-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-zinc-400 focus:border-blue-500 outline-none" placeholder="Search projects..." />
68 |
69 |
setFilters({ ...filters, status: e.target.value })} className="px-3 py-2 rounded-lg border border-gray-300 dark:border-zinc-700 text-gray-900 dark:text-white text-sm" >
70 | All Status
71 | Active
72 | Planning
73 | Completed
74 | On Hold
75 | Cancelled
76 |
77 |
setFilters({ ...filters, priority: e.target.value })} className="px-3 py-2 rounded-lg border border-gray-300 dark:border-zinc-700 text-gray-900 dark:text-white text-sm" >
78 | All Priority
79 | High
80 | Medium
81 | Low
82 |
83 |
84 |
85 | {/* Projects Grid */}
86 |
87 | {filteredProjects.length === 0 ? (
88 |
89 |
90 |
91 |
92 |
93 | No projects found
94 |
95 |
96 | Create your first project to get started
97 |
98 |
setIsDialogOpen(true)} className="flex items-center gap-1.5 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded mx-auto text-sm" >
99 |
100 | Create Project
101 |
102 |
103 | ) : (
104 | filteredProjects.map((project) => (
105 |
106 | ))
107 | )}
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/ProjectOverview.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { ArrowRight, Calendar, UsersIcon, FolderOpen } from "lucide-react";
4 | import { format } from "date-fns";
5 | import { useSelector } from "react-redux";
6 | import CreateProjectDialog from "./CreateProjectDialog";
7 |
8 | const ProjectOverview = () => {
9 | const statusColors = {
10 | PLANNING: "bg-zinc-200 text-zinc-800 dark:bg-zinc-600 dark:text-zinc-200",
11 | ACTIVE: "bg-emerald-200 text-emerald-800 dark:bg-emerald-500 dark:text-emerald-900",
12 | ON_HOLD: "bg-amber-200 text-amber-800 dark:bg-amber-500 dark:text-amber-900",
13 | COMPLETED: "bg-blue-200 text-blue-800 dark:bg-blue-500 dark:text-blue-900",
14 | CANCELLED: "bg-red-200 text-red-800 dark:bg-red-500 dark:text-red-900"
15 | };
16 |
17 | const priorityColors = {
18 | LOW: "border-zinc-300 text-zinc-600 dark:border-zinc-600 dark:text-zinc-400",
19 | MEDIUM: "border-amber-300 text-amber-700 dark:border-amber-500 dark:text-amber-400",
20 | HIGH: "border-green-300 text-green-700 dark:border-green-500 dark:text-green-400",
21 | };
22 |
23 | const currentWorkspace = useSelector((state) => state?.workspace?.currentWorkspace || null);
24 | const [isDialogOpen, setIsDialogOpen] = useState(false);
25 | const [projects, setProjects] = useState([]);
26 |
27 | useEffect(() => {
28 | setProjects(currentWorkspace?.projects || []);
29 | }, [currentWorkspace]);
30 |
31 | return currentWorkspace && (
32 |
33 |
34 |
Project Overview
35 |
36 | View all
37 |
38 |
39 |
40 |
41 | {projects.length === 0 ? (
42 |
43 |
44 |
45 |
46 |
No projects yet
47 |
setIsDialogOpen(true)} className="mt-4 px-4 py-2 text-sm bg-gradient-to-br from-blue-500 to-blue-600 text-white dark:text-zinc-200 rounded hover:opacity-90 transition">
48 | Create your First Project
49 |
50 |
51 |
52 | ) : (
53 |
54 | {projects.slice(0, 5).map((project) => (
55 |
56 |
57 |
58 |
59 | {project.name}
60 |
61 |
62 | {project.description || 'No description'}
63 |
64 |
65 |
66 |
67 | {project.status.replace('_', ' ').replaceAll(/\b\w/g, c => c.toUpperCase())}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {project.members?.length > 0 && (
76 |
77 |
78 | {project.members.length} members
79 |
80 | )}
81 | {project.end_date && (
82 |
83 |
84 | {format(new Date(project.end_date), "MMM d, yyyy")}
85 |
86 | )}
87 |
88 |
89 |
90 |
91 |
92 | Progress
93 | {project.progress || 0}%
94 |
95 |
98 |
99 |
100 | ))}
101 |
102 | )}
103 |
104 |
105 | );
106 | }
107 |
108 | export default ProjectOverview;
109 |
--------------------------------------------------------------------------------
/src/components/ProjectSettings.jsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import { Plus, Save } from "lucide-react";
3 | import { useEffect, useState } from "react";
4 | import AddProjectMember from "./AddProjectMember";
5 |
6 | export default function ProjectSettings({ project }) {
7 |
8 | const [formData, setFormData] = useState({
9 | name: "New Website Launch",
10 | description: "Initial launch for new web platform.",
11 | status: "PLANNING",
12 | priority: "MEDIUM",
13 | start_date: "2025-09-10",
14 | end_date: "2025-10-15",
15 | progress: 30,
16 | });
17 |
18 | const [isDialogOpen, setIsDialogOpen] = useState(false);
19 | const [isSubmitting, setIsSubmitting] = useState(false);
20 |
21 | const handleSubmit = async (e) => {
22 | e.preventDefault();
23 |
24 | };
25 |
26 | useEffect(() => {
27 | if (project) setFormData(project);
28 | }, [project]);
29 |
30 | const inputClasses = "w-full px-3 py-2 rounded mt-2 border text-sm dark:bg-zinc-900 border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-300";
31 |
32 | const cardClasses = "rounded-lg border p-6 not-dark:bg-white dark:bg-gradient-to-br dark:from-zinc-800/70 dark:to-zinc-900/50 border-zinc-300 dark:border-zinc-800";
33 |
34 | const labelClasses = "text-sm text-zinc-600 dark:text-zinc-400";
35 |
36 | return (
37 |
38 | {/* Project Details */}
39 |
40 |
Project Details
41 |
100 |
101 |
102 | {/* Team Members */}
103 |
104 |
105 |
106 |
107 | Team Members ({project.members.length})
108 |
109 |
setIsDialogOpen(true)} className="p-2 rounded-lg border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800" >
110 |
111 |
112 |
113 |
114 |
115 | {/* Member List */}
116 | {project.members.length > 0 && (
117 |
118 | {project.members.map((member, index) => (
119 |
120 | {member?.user?.email || "Unknown"}
121 | {project.team_lead === member.user.id && Team Lead }
122 |
123 | ))}
124 |
125 | )}
126 |
127 |
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/src/pages/ProjectDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useSelector } from "react-redux";
3 | import { useNavigate, useSearchParams } from "react-router-dom";
4 | import { ArrowLeftIcon, PlusIcon, SettingsIcon, BarChart3Icon, CalendarIcon, FileStackIcon, ZapIcon } from "lucide-react";
5 | import ProjectAnalytics from "../components/ProjectAnalytics";
6 | import ProjectSettings from "../components/ProjectSettings";
7 | import CreateTaskDialog from "../components/CreateTaskDialog";
8 | import ProjectCalendar from "../components/ProjectCalendar";
9 | import ProjectTasks from "../components/ProjectTasks";
10 |
11 | export default function ProjectDetail() {
12 |
13 | const [searchParams, setSearchParams] = useSearchParams();
14 | const tab = searchParams.get('tab');
15 | const id = searchParams.get('id');
16 |
17 | const navigate = useNavigate();
18 | const projects = useSelector((state) => state?.workspace?.currentWorkspace?.projects || []);
19 |
20 | const [project, setProject] = useState(null);
21 | const [tasks, setTasks] = useState([]);
22 | const [showCreateTask, setShowCreateTask] = useState(false);
23 | const [activeTab, setActiveTab] = useState(tab || "tasks");
24 |
25 | useEffect(() => {
26 | if (tab) setActiveTab(tab);
27 | }, [tab]);
28 |
29 | useEffect(() => {
30 | if (projects && projects.length > 0) {
31 | const proj = projects.find((p) => p.id === id);
32 | setProject(proj);
33 | setTasks(proj?.tasks || []);
34 | }
35 | }, [id, projects]);
36 |
37 | const statusColors = {
38 | PLANNING: "bg-zinc-200 text-zinc-900 dark:bg-zinc-600 dark:text-zinc-200",
39 | ACTIVE: "bg-emerald-200 text-emerald-900 dark:bg-emerald-500 dark:text-emerald-900",
40 | ON_HOLD: "bg-amber-200 text-amber-900 dark:bg-amber-500 dark:text-amber-900",
41 | COMPLETED: "bg-blue-200 text-blue-900 dark:bg-blue-500 dark:text-blue-900",
42 | CANCELLED: "bg-red-200 text-red-900 dark:bg-red-500 dark:text-red-900",
43 | };
44 |
45 | if (!project) {
46 | return (
47 |
48 |
Project not found
49 |
navigate('/projects')} className="mt-4 px-4 py-2 rounded bg-zinc-200 text-zinc-900 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600" >
50 | Back to Projects
51 |
52 |
53 | );
54 | }
55 |
56 | return (
57 |
58 | {/* Header */}
59 |
60 |
61 |
navigate('/projects')}>
62 |
63 |
64 |
65 |
{project.name}
66 |
67 | {project.status.replace("_", " ")}
68 |
69 |
70 |
71 |
setShowCreateTask(true)} className="flex items-center gap-2 px-5 py-2 text-sm rounded bg-gradient-to-br from-blue-500 to-blue-600 text-white" >
72 |
73 | New Task
74 |
75 |
76 |
77 | {/* Info Cards */}
78 |
79 | {[
80 | { label: "Total Tasks", value: tasks.length, color: "text-zinc-900 dark:text-white" },
81 | { label: "Completed", value: tasks.filter((t) => t.status === "DONE").length, color: "text-emerald-700 dark:text-emerald-400" },
82 | { label: "In Progress", value: tasks.filter((t) => t.status === "IN_PROGRESS" || t.status === "TODO").length, color: "text-amber-700 dark:text-amber-400" },
83 | { label: "Team Members", value: project.members?.length || 0, color: "text-blue-700 dark:text-blue-400" },
84 | ].map((card, idx) => (
85 |
86 |
87 |
{card.label}
88 |
{card.value}
89 |
90 |
91 |
92 | ))}
93 |
94 |
95 | {/* Tabs */}
96 |
97 |
98 | {[
99 | { key: "tasks", label: "Tasks", icon: FileStackIcon },
100 | { key: "calendar", label: "Calendar", icon: CalendarIcon },
101 | { key: "analytics", label: "Analytics", icon: BarChart3Icon },
102 | { key: "settings", label: "Settings", icon: SettingsIcon },
103 | ].map((tabItem) => (
104 | { setActiveTab(tabItem.key); setSearchParams({ id: id, tab: tabItem.key }) }} className={`flex items-center gap-2 px-4 py-2 text-sm transition-all ${activeTab === tabItem.key ? "bg-zinc-100 dark:bg-zinc-800/80" : "hover:bg-zinc-50 dark:hover:bg-zinc-700"}`} >
105 |
106 | {tabItem.label}
107 |
108 | ))}
109 |
110 |
111 |
112 | {activeTab === "tasks" && (
113 |
116 | )}
117 | {activeTab === "analytics" && (
118 |
121 | )}
122 | {activeTab === "calendar" && (
123 |
126 | )}
127 | {activeTab === "settings" && (
128 |
131 | )}
132 |
133 |
134 |
135 | {/* Create Task Modal */}
136 | {showCreateTask &&
}
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/src/components/CreateTaskDialog.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Calendar as CalendarIcon } from "lucide-react";
3 | import { useSelector } from "react-redux";
4 | import { format } from "date-fns";
5 |
6 | export default function CreateTaskDialog({ showCreateTask, setShowCreateTask, projectId }) {
7 | const currentWorkspace = useSelector((state) => state.workspace?.currentWorkspace || null);
8 | const project = currentWorkspace?.projects.find((p) => p.id === projectId);
9 | const teamMembers = project?.members || [];
10 |
11 | const [isSubmitting, setIsSubmitting] = useState(false);
12 | const [formData, setFormData] = useState({
13 | title: "",
14 | description: "",
15 | type: "TASK",
16 | status: "TODO",
17 | priority: "MEDIUM",
18 | assigneeId: "",
19 | due_date: "",
20 | });
21 |
22 | const handleSubmit = async (e) => {
23 | e.preventDefault();
24 |
25 |
26 | };
27 |
28 | return showCreateTask ? (
29 |
30 |
31 |
Create New Task
32 |
33 |
34 | {/* Title */}
35 |
36 | Title
37 | setFormData({ ...formData, title: e.target.value })} placeholder="Task title" className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1 focus:outline-none focus:ring-2 focus:ring-blue-500" required />
38 |
39 |
40 | {/* Description */}
41 |
42 | Description
43 | setFormData({ ...formData, description: e.target.value })} placeholder="Describe the task" className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1 h-24 focus:outline-none focus:ring-2 focus:ring-blue-500" />
44 |
45 |
46 | {/* Type & Priority */}
47 |
48 |
49 | Type
50 | setFormData({ ...formData, type: e.target.value })} className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1" >
51 | Bug
52 | Feature
53 | Task
54 | Improvement
55 | Other
56 |
57 |
58 |
59 |
60 | Priority
61 | setFormData({ ...formData, priority: e.target.value })} className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1" >
62 | Low
63 | Medium
64 | High
65 |
66 |
67 |
68 |
69 | {/* Assignee and Status */}
70 |
71 |
72 | Assignee
73 | setFormData({ ...formData, assigneeId: e.target.value })} className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1" >
74 | Unassigned
75 | {teamMembers.map((member) => (
76 |
77 | {member?.user.email}
78 |
79 | ))}
80 |
81 |
82 |
83 |
84 | Status
85 | setFormData({ ...formData, status: e.target.value })} className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1" >
86 | To Do
87 | In Progress
88 | Done
89 |
90 |
91 |
92 |
93 | {/* Due Date */}
94 |
95 |
Due Date
96 |
97 |
98 | setFormData({ ...formData, due_date: e.target.value })} min={new Date().toISOString().split('T')[0]} className="w-full rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-zinc-900 dark:text-zinc-200 text-sm mt-1" />
99 |
100 | {formData.due_date && (
101 |
102 | {format(new Date(formData.due_date), "PPP")}
103 |
104 | )}
105 |
106 |
107 | {/* Footer */}
108 |
109 | setShowCreateTask(false)} className="rounded border border-zinc-300 dark:border-zinc-700 px-5 py-2 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 transition" >
110 | Cancel
111 |
112 |
113 | {isSubmitting ? "Creating..." : "Create Task"}
114 |
115 |
116 |
117 |
118 |
119 | ) : null;
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ProjectAnalytics.jsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, PieChart, Pie, Cell } from "recharts";
3 | import { CheckCircle, Clock, AlertTriangle, Users, ArrowRightIcon } from "lucide-react";
4 |
5 | // Colors for charts and priorities
6 | const COLORS = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6"];
7 | const PRIORITY_COLORS = {
8 | LOW: "text-red-600 bg-red-200 dark:text-red-500 dark:bg-red-600",
9 | MEDIUM: "text-blue-600 bg-blue-200 dark:text-blue-500 dark:bg-blue-600",
10 | HIGH: "text-emerald-600 bg-emerald-200 dark:text-emerald-500 dark:bg-emerald-600",
11 | };
12 |
13 | const ProjectAnalytics = ({ project, tasks }) => {
14 | const { stats, statusData, typeData, priorityData } = useMemo(() => {
15 | const now = new Date();
16 | const total = tasks.length;
17 |
18 | const stats = {
19 | total,
20 | completed: 0,
21 | inProgress: 0,
22 | todo: 0,
23 | overdue: 0,
24 | };
25 |
26 | const statusMap = { TODO: 0, IN_PROGRESS: 0, DONE: 0 };
27 | const typeMap = { TASK: 0, BUG: 0, FEATURE: 0, IMPROVEMENT: 0, OTHER: 0 };
28 | const priorityMap = { LOW: 0, MEDIUM: 0, HIGH: 0 };
29 |
30 | tasks.forEach((t) => {
31 | if (t.status === "DONE") stats.completed++;
32 | if (t.status === "IN_PROGRESS") stats.inProgress++;
33 | if (t.status === "TODO") stats.todo++;
34 | if (new Date(t.due_date) < now && t.status !== "DONE") stats.overdue++;
35 |
36 | if (statusMap[t.status] !== undefined) statusMap[t.status]++;
37 | if (typeMap[t.type] !== undefined) typeMap[t.type]++;
38 | if (priorityMap[t.priority] !== undefined) priorityMap[t.priority]++;
39 | });
40 |
41 | return {
42 | stats,
43 | statusData: Object.entries(statusMap).map(([k, v]) => ({ name: k.replace("_", " "), value: v })),
44 | typeData: Object.entries(typeMap).filter(([_, v]) => v > 0).map(([k, v]) => ({ name: k, value: v })),
45 | priorityData: Object.entries(priorityMap).map(([k, v]) => ({
46 | name: k,
47 | value: v,
48 | percentage: total > 0 ? Math.round((v / total) * 100) : 0,
49 | })),
50 | };
51 | }, [tasks]);
52 |
53 | const completionRate = stats.total ? Math.round((stats.completed / stats.total) * 100) : 0;
54 |
55 | const metrics = [
56 | {
57 | label: "Completion Rate",
58 | value: `${completionRate}%`,
59 | color: "text-emerald-600 dark:text-emerald-400",
60 | icon: ,
61 | bg: "bg-emerald-200 dark:bg-emerald-500/10",
62 | },
63 | {
64 | label: "Active Tasks",
65 | value: stats.inProgress,
66 | color: "text-blue-600 dark:text-blue-400",
67 | icon: ,
68 | bg: "bg-blue-200 dark:bg-blue-500/10",
69 | },
70 | {
71 | label: "Overdue Tasks",
72 | value: stats.overdue,
73 | color: "text-red-600 dark:text-red-400",
74 | icon: ,
75 | bg: "bg-red-200 dark:bg-red-500/10",
76 | },
77 | {
78 | label: "Team Size",
79 | value: project?.members?.length || 0,
80 | color: "text-purple-600 dark:text-purple-400",
81 | icon: ,
82 | bg: "bg-purple-200 dark:bg-purple-500/10",
83 | },
84 | ];
85 |
86 | return (
87 |
88 | {/* Metrics */}
89 |
90 | {metrics.map((m, i) => (
91 |
95 |
96 |
97 |
{m.label}
98 |
{m.value}
99 |
100 |
{m.icon}
101 |
102 |
103 | ))}
104 |
105 |
106 | {/* Charts */}
107 |
108 | {/* Tasks by Status */}
109 |
110 |
Tasks by Status
111 |
112 |
113 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | {/* Tasks by Type */}
126 |
127 |
Tasks by Type
128 |
129 |
130 | `${name}: ${value}`}
138 | >
139 | {typeData.map((_, i) => (
140 | |
141 | ))}
142 |
143 |
144 |
145 |
146 |
147 |
148 | {/* Priority Breakdown */}
149 |
150 |
Tasks by Priority
151 |
152 | {priorityData.map((p) => (
153 |
154 |
155 |
156 |
157 |
{p.name.toLowerCase()}
158 |
159 |
160 | {p.value} tasks
161 |
162 | {p.percentage}%
163 |
164 |
165 |
166 |
172 |
173 | ))}
174 |
175 |
176 |
177 | );
178 | };
179 |
180 | export default ProjectAnalytics;
181 |
--------------------------------------------------------------------------------
/src/pages/TaskDetails.jsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import toast from "react-hot-toast";
3 | import { useSelector } from "react-redux";
4 | import { useEffect, useState } from "react";
5 | import { useSearchParams } from "react-router-dom";
6 | import { CalendarIcon, MessageCircle, PenIcon } from "lucide-react";
7 | import { assets } from "../assets/assets";
8 |
9 | const TaskDetails = () => {
10 |
11 | const [searchParams] = useSearchParams();
12 | const projectId = searchParams.get("projectId");
13 | const taskId = searchParams.get("taskId");
14 |
15 | const user = { id : 'user_1'}
16 | const [task, setTask] = useState(null);
17 | const [project, setProject] = useState(null);
18 | const [comments, setComments] = useState([]);
19 | const [newComment, setNewComment] = useState("");
20 | const [loading, setLoading] = useState(true);
21 |
22 | const { currentWorkspace } = useSelector((state) => state.workspace);
23 |
24 | const fetchComments = async () => {
25 |
26 | };
27 |
28 | const fetchTaskDetails = async () => {
29 | setLoading(true);
30 | if (!projectId || !taskId) return;
31 |
32 | const proj = currentWorkspace.projects.find((p) => p.id === projectId);
33 | if (!proj) return;
34 |
35 | const tsk = proj.tasks.find((t) => t.id === taskId);
36 | if (!tsk) return;
37 |
38 | setTask(tsk);
39 | setProject(proj);
40 | setLoading(false);
41 | };
42 |
43 | const handleAddComment = async () => {
44 | if (!newComment.trim()) return;
45 |
46 | try {
47 |
48 | toast.loading("Adding comment...");
49 |
50 | // Simulate API call
51 | await new Promise((resolve) => setTimeout(resolve, 2000));
52 |
53 | const dummyComment = { id: Date.now(), user: { id: 1, name: "User", image: assets.profile_img_a }, content: newComment, createdAt: new Date() };
54 |
55 | setComments((prev) => [...prev, dummyComment]);
56 | setNewComment("");
57 | toast.dismissAll();
58 | toast.success("Comment added.");
59 | } catch (error) {
60 | toast.dismissAll();
61 | toast.error(error?.response?.data?.message || error.message);
62 | console.error(error);
63 | }
64 | };
65 |
66 | useEffect(() => { fetchTaskDetails(); }, [taskId]);
67 |
68 | useEffect(() => {
69 | if (taskId && task) {
70 | fetchComments();
71 | const interval = setInterval(() => { fetchComments(); }, 10000);
72 | return () => clearInterval(interval);
73 | }
74 | }, [taskId, task]);
75 |
76 | if (loading) return Loading task details...
;
77 | if (!task) return Task not found.
;
78 |
79 | return (
80 |
81 | {/* Left: Comments / Chatbox */}
82 |
83 |
84 |
85 | Task Discussion ({comments.length})
86 |
87 |
88 |
89 | {comments.length > 0 ? (
90 |
91 | {comments.map((comment) => (
92 |
93 |
94 |
95 |
{comment.user.name}
96 |
97 | • {format(new Date(comment.createdAt), "dd MMM yyyy, HH:mm")}
98 |
99 |
100 |
{comment.content}
101 |
102 | ))}
103 |
104 | ) : (
105 |
No comments yet. Be the first!
106 | )}
107 |
108 |
109 | {/* Add Comment */}
110 |
111 | setNewComment(e.target.value)}
114 | placeholder="Write a comment..."
115 | className="w-full dark:bg-zinc-800 border border-gray-300 dark:border-zinc-700 rounded-md p-2 text-sm text-gray-900 dark:text-zinc-200 resize-none focus:outline-none focus:ring-1 focus:ring-blue-600"
116 | rows={3}
117 | />
118 |
119 | Post
120 |
121 |
122 |
123 |
124 |
125 | {/* Right: Task + Project Info */}
126 |
127 | {/* Task Info */}
128 |
129 |
130 |
{task.title}
131 |
132 |
133 | {task.status}
134 |
135 |
136 | {task.type}
137 |
138 |
139 | {task.priority}
140 |
141 |
142 |
143 |
144 | {task.description && (
145 |
{task.description}
146 | )}
147 |
148 |
149 |
150 |
151 |
152 |
153 | {task.assignee?.name || "Unassigned"}
154 |
155 |
156 |
157 | Due : {format(new Date(task.due_date), "dd MMM yyyy")}
158 |
159 |
160 |
161 |
162 | {/* Project Info */}
163 | {project && (
164 |
165 |
Project Details
166 |
{project.name}
167 |
Project Start Date: {format(new Date(project.start_date), "dd MMM yyyy")}
168 |
169 | Status: {project.status}
170 | Priority: {project.priority}
171 | Progress: {project.progress}%
172 |
173 |
174 | )}
175 |
176 |
177 | );
178 | };
179 |
180 | export default TaskDetails;
181 |
--------------------------------------------------------------------------------
/src/components/CreateProjectDialog.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { XIcon } from "lucide-react";
3 | import { useSelector } from "react-redux";
4 |
5 | const CreateProjectDialog = ({ isDialogOpen, setIsDialogOpen }) => {
6 |
7 | const { currentWorkspace } = useSelector((state) => state.workspace);
8 |
9 | const [formData, setFormData] = useState({
10 | name: "",
11 | description: "",
12 | status: "PLANNING",
13 | priority: "MEDIUM",
14 | start_date: "",
15 | end_date: "",
16 | team_members: [],
17 | team_lead: "",
18 | progress: 0,
19 | });
20 |
21 | const [isSubmitting, setIsSubmitting] = useState(false);
22 |
23 | const handleSubmit = async (e) => {
24 | e.preventDefault();
25 |
26 | };
27 |
28 | const removeTeamMember = (email) => {
29 | setFormData((prev) => ({ ...prev, team_members: prev.team_members.filter(m => m !== email) }));
30 | };
31 |
32 | if (!isDialogOpen) return null;
33 |
34 | return (
35 |
36 |
37 |
setIsDialogOpen(false)} >
38 |
39 |
40 |
41 |
Create New Project
42 | {currentWorkspace && (
43 |
44 | In workspace: {currentWorkspace.name}
45 |
46 | )}
47 |
48 |
49 | {/* Project Name */}
50 |
51 | Project Name
52 | setFormData({ ...formData, name: e.target.value })} placeholder="Enter project name" className="w-full px-3 py-2 rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 mt-1 text-zinc-900 dark:text-zinc-200 text-sm" required />
53 |
54 |
55 | {/* Description */}
56 |
57 | Description
58 | setFormData({ ...formData, description: e.target.value })} placeholder="Describe your project" className="w-full px-3 py-2 rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 mt-1 text-zinc-900 dark:text-zinc-200 text-sm h-20" />
59 |
60 |
61 | {/* Status & Priority */}
62 |
63 |
64 | Status
65 | setFormData({ ...formData, status: e.target.value })} className="w-full px-3 py-2 rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 mt-1 text-zinc-900 dark:text-zinc-200 text-sm" >
66 | Planning
67 | Active
68 | Completed
69 | On Hold
70 | Cancelled
71 |
72 |
73 |
74 |
75 | Priority
76 | setFormData({ ...formData, priority: e.target.value })} className="w-full px-3 py-2 rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 mt-1 text-zinc-900 dark:text-zinc-200 text-sm" >
77 | Low
78 | Medium
79 | High
80 |
81 |
82 |
83 |
84 | {/* Dates */}
85 |
95 |
96 | {/* Lead */}
97 |
98 | Project Lead
99 | setFormData({ ...formData, team_lead: e.target.value, team_members: e.target.value ? [...new Set([...formData.team_members, e.target.value])] : formData.team_members, })} className="w-full px-3 py-2 rounded dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 mt-1 text-zinc-900 dark:text-zinc-200 text-sm" >
100 | No lead
101 | {currentWorkspace?.members?.map((member) => (
102 |
103 | {member.user.email}
104 |
105 | ))}
106 |
107 |
108 |
109 | {/* Team Members */}
110 |
111 |
Team Members
112 |
{
114 | if (e.target.value && !formData.team_members.includes(e.target.value)) {
115 | setFormData((prev) => ({ ...prev, team_members: [...prev.team_members, e.target.value] }));
116 | }
117 | }}
118 | >
119 | Add team members
120 | {currentWorkspace?.members
121 | ?.filter((email) => !formData.team_members.includes(email))
122 | .map((member) => (
123 |
124 | {member.user.email}
125 |
126 | ))}
127 |
128 |
129 | {formData.team_members.length > 0 && (
130 |
131 | {formData.team_members.map((email) => (
132 |
133 | {email}
134 | removeTeamMember(email)} className="ml-1 hover:bg-blue-300/30 dark:hover:bg-blue-500/30 rounded" >
135 |
136 |
137 |
138 | ))}
139 |
140 | )}
141 |
142 |
143 | {/* Footer */}
144 |
145 | setIsDialogOpen(false)} className="px-4 py-2 rounded border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-800" >
146 | Cancel
147 |
148 |
149 | {isSubmitting ? "Creating..." : "Create Project"}
150 |
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default CreateProjectDialog;
--------------------------------------------------------------------------------
/src/components/ProjectCalendar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { format, isSameDay, isBefore, startOfMonth, endOfMonth, eachDayOfInterval, addMonths, subMonths } from "date-fns";
3 | import { CalendarIcon, Clock, User, ChevronLeft, ChevronRight } from "lucide-react";
4 |
5 | const typeColors = {
6 | BUG: "bg-red-200 text-red-800 dark:bg-red-500 dark:text-red-900",
7 | FEATURE: "bg-blue-200 text-blue-800 dark:bg-blue-500 dark:text-blue-900",
8 | TASK: "bg-green-200 text-green-800 dark:bg-green-500 dark:text-green-900",
9 | IMPROVEMENT: "bg-purple-200 text-purple-800 dark:bg-purple-500 dark:text-purple-900",
10 | OTHER: "bg-amber-200 text-amber-800 dark:bg-amber-500 dark:text-amber-900",
11 | };
12 |
13 | const priorityBorders = {
14 | LOW: "border-zinc-300 dark:border-zinc-600",
15 | MEDIUM: "border-amber-300 dark:border-amber-500",
16 | HIGH: "border-orange-300 dark:border-orange-500",
17 | };
18 |
19 | const ProjectCalendar = ({ tasks }) => {
20 | const [selectedDate, setSelectedDate] = useState(new Date());
21 | const [currentMonth, setCurrentMonth] = useState(new Date());
22 |
23 | const today = new Date();
24 | const getTasksForDate = (date) => tasks.filter((task) => isSameDay(task.due_date, date));
25 |
26 | const upcomingTasks = tasks
27 | .filter((task) => task.due_date && !isBefore(task.due_date, today) && task.status !== "DONE")
28 | .sort((a, b) => new Date(a.due_date) - new Date(b.due_date))
29 | .slice(0, 5);
30 |
31 | const overdueTasks = tasks.filter((task) => task.due_date && isBefore(task.due_date, today) && task.status !== "DONE");
32 |
33 | const daysInMonth = eachDayOfInterval({
34 | start: startOfMonth(currentMonth),
35 | end: endOfMonth(currentMonth),
36 | });
37 |
38 |
39 | const handleMonthChange = (direction) => {
40 | setCurrentMonth((prev) => (direction === "next" ? addMonths(prev, 1) : subMonths(prev, 1)));
41 | };
42 |
43 | return (
44 |
45 | {/* Calendar View */}
46 |
47 |
48 |
49 |
50 | Task Calendar
51 |
52 |
53 | handleMonthChange("prev")}>
54 |
55 |
56 | {format(currentMonth, "MMMM yyyy")}
57 | handleMonthChange("next")}>
58 |
59 |
60 |
61 |
62 |
63 |
64 | {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
65 |
{day}
66 | ))}
67 |
68 |
69 |
70 | {daysInMonth.map((day) => {
71 | const dayTasks = getTasksForDate(day);
72 | const isSelected = isSameDay(day, selectedDate);
73 | const hasOverdue = dayTasks.some((t) => t.status !== "DONE" && isBefore(t.due_date, today));
74 |
75 | return (
76 | setSelectedDate(day)}
79 | className={`sm:h-14 rounded-md flex flex-col items-center justify-center text-sm
80 | ${isSelected ? "bg-blue-200 text-blue-900 dark:bg-blue-600 dark:text-white" : "bg-zinc-50 text-zinc-900 dark:bg-zinc-800/40 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700"}
81 | ${hasOverdue ? "border border-red-300 dark:border-red-500" : ""}`}
82 | >
83 | {format(day, "d")}
84 | {dayTasks.length > 0 && (
85 | {dayTasks.length} tasks
86 | )}
87 |
88 | );
89 | })}
90 |
91 |
92 |
93 | {/* Tasks for Selected Day */}
94 | {getTasksForDate(selectedDate).length > 0 && (
95 |
96 |
97 | Tasks for {format(selectedDate, "MMM d, yyyy")}
98 |
99 |
100 | {getTasksForDate(selectedDate).map((task) => (
101 |
105 |
106 |
{task.title}
107 |
108 | {task.type}
109 |
110 |
111 |
112 | {task.priority.toLowerCase()} priority
113 | {task.assignee && (
114 |
115 |
116 | {task.assignee.name}
117 |
118 | )}
119 |
120 |
121 | ))}
122 |
123 |
124 | )}
125 |
126 |
127 | {/* Sidebar */}
128 |
129 | {/* Upcoming Tasks */}
130 |
131 |
132 | Upcoming Tasks
133 |
134 | {upcomingTasks.length === 0 ? (
135 |
No upcoming tasks
136 | ) : (
137 |
138 | {upcomingTasks.map((task) => (
139 |
143 |
144 | {task.title}
145 |
146 | {task.type}
147 |
148 |
149 |
{format(task.due_date, "MMM d")}
150 |
151 | ))}
152 |
153 | )}
154 |
155 |
156 | {/* Overdue Tasks */}
157 | {overdueTasks.length > 0 && (
158 |
159 |
160 | Overdue Tasks ({overdueTasks.length})
161 |
162 |
163 | {overdueTasks.slice(0, 5).map((task) => (
164 |
165 |
166 | {task.title}
167 |
168 | {task.type}
169 |
170 |
171 |
172 | Due {format(task.due_date, "MMM d")}
173 |
174 |
175 | ))}
176 | {overdueTasks.length > 5 && (
177 |
178 | +{overdueTasks.length - 5} more
179 |
180 | )}
181 |
182 |
183 | )}
184 |
185 |
186 | );
187 | };
188 |
189 | export default ProjectCalendar;
190 |
--------------------------------------------------------------------------------
/src/pages/Team.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { UsersIcon, Search, UserPlus, Shield, Activity } from "lucide-react";
3 | import InviteMemberDialog from "../components/InviteMemberDialog";
4 | import { useSelector } from "react-redux";
5 |
6 | const Team = () => {
7 |
8 | const [tasks, setTasks] = useState([]);
9 | const [searchTerm, setSearchTerm] = useState("");
10 | const [isDialogOpen, setIsDialogOpen] = useState(false);
11 | const [users, setUsers] = useState([]);
12 | const currentWorkspace = useSelector((state) => state?.workspace?.currentWorkspace || null);
13 | const projects = currentWorkspace?.projects || [];
14 |
15 | const filteredUsers = users.filter(
16 | (user) =>
17 | user?.user?.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
18 | user?.user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
19 | );
20 |
21 | useEffect(() => {
22 | setUsers(currentWorkspace?.members || []);
23 | setTasks(currentWorkspace?.projects?.reduce((acc, project) => [...acc, ...project.tasks], []) || []);
24 | }, [currentWorkspace]);
25 |
26 | return (
27 |
28 | {/* Header */}
29 |
30 |
31 |
Team
32 |
33 | Manage team members and their contributions
34 |
35 |
36 |
setIsDialogOpen(true)} className="flex items-center px-5 py-2 rounded text-sm bg-gradient-to-br from-blue-500 to-blue-600 hover:opacity-90 text-white transition" >
37 | Invite Member
38 |
39 |
40 |
41 |
42 | {/* Stats Cards */}
43 |
44 | {/* Total Members */}
45 |
46 |
47 |
48 |
Total Members
49 |
{users.length}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {/* Active Projects */}
58 |
59 |
60 |
61 |
Active Projects
62 |
63 | {projects.filter((p) => p.status !== "CANCELLED" && p.status !== "COMPLETED").length}
64 |
65 |
66 |
69 |
70 |
71 |
72 | {/* Total Tasks */}
73 |
74 |
75 |
76 |
Total Tasks
77 |
{tasks.length}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {/* Search */}
87 |
88 |
89 | setSearchTerm(e.target.value)} className="pl-8 w-full text-sm rounded-md border border-gray-300 dark:border-zinc-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-zinc-400 py-2 focus:outline-none focus:border-blue-500" />
90 |
91 |
92 | {/* Team Members */}
93 |
94 | {filteredUsers.length === 0 ? (
95 |
96 |
97 |
98 |
99 |
100 | {users.length === 0
101 | ? "No team members yet"
102 | : "No members match your search"}
103 |
104 |
105 | {users.length === 0
106 | ? "Invite team members to start collaborating"
107 | : "Try adjusting your search term"}
108 |
109 |
110 | ) : (
111 |
112 | {/* Desktop Table */}
113 |
114 |
115 |
116 |
117 |
118 | Name
119 |
120 |
121 | Email
122 |
123 |
124 | Role
125 |
126 |
127 |
128 |
129 | {filteredUsers.map((user) => (
130 |
134 |
135 |
140 |
141 | {user.user?.name || "Unknown User"}
142 |
143 |
144 |
145 | {user.user.email}
146 |
147 |
148 |
154 | {user.role || "User"}
155 |
156 |
157 |
158 | ))}
159 |
160 |
161 |
162 |
163 | {/* Mobile Cards */}
164 |
165 | {filteredUsers.map((user) => (
166 |
170 |
171 |
176 |
177 |
178 | {user.user?.name || "Unknown User"}
179 |
180 |
181 | {user.user.email}
182 |
183 |
184 |
185 |
186 |
192 | {user.role || "User"}
193 |
194 |
195 |
196 | ))}
197 |
198 |
199 | )}
200 |
201 |
202 |
203 |
204 | );
205 | };
206 |
207 | export default Team;
208 |
--------------------------------------------------------------------------------
/src/components/ProjectTasks.jsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import toast from "react-hot-toast";
3 | import { useDispatch } from "react-redux";
4 | import { useState, useMemo } from "react";
5 | import { useNavigate } from "react-router-dom";
6 | import { deleteTask, updateTask } from "../features/workspaceSlice";
7 | import { Bug, CalendarIcon, GitCommit, MessageSquare, Square, Trash, XIcon, Zap } from "lucide-react";
8 |
9 | const typeIcons = {
10 | BUG: { icon: Bug, color: "text-red-600 dark:text-red-400" },
11 | FEATURE: { icon: Zap, color: "text-blue-600 dark:text-blue-400" },
12 | TASK: { icon: Square, color: "text-green-600 dark:text-green-400" },
13 | IMPROVEMENT: { icon: GitCommit, color: "text-purple-600 dark:text-purple-400" },
14 | OTHER: { icon: MessageSquare, color: "text-amber-600 dark:text-amber-400" },
15 | };
16 |
17 | const priorityTexts = {
18 | LOW: { background: "bg-red-100 dark:bg-red-950", prioritycolor: "text-red-600 dark:text-red-400" },
19 | MEDIUM: { background: "bg-blue-100 dark:bg-blue-950", prioritycolor: "text-blue-600 dark:text-blue-400" },
20 | HIGH: { background: "bg-emerald-100 dark:bg-emerald-950", prioritycolor: "text-emerald-600 dark:text-emerald-400" },
21 | };
22 |
23 | const ProjectTasks = ({ tasks }) => {
24 | const dispatch = useDispatch();
25 | const navigate = useNavigate();
26 | const [selectedTasks, setSelectedTasks] = useState([]);
27 |
28 | const [filters, setFilters] = useState({
29 | status: "",
30 | type: "",
31 | priority: "",
32 | assignee: "",
33 | });
34 |
35 | const assigneeList = useMemo(
36 | () => Array.from(new Set(tasks.map((t) => t.assignee?.name).filter(Boolean))),
37 | [tasks]
38 | );
39 |
40 | const filteredTasks = useMemo(() => {
41 | return tasks.filter((task) => {
42 | const { status, type, priority, assignee } = filters;
43 | return (
44 | (!status || task.status === status) &&
45 | (!type || task.type === type) &&
46 | (!priority || task.priority === priority) &&
47 | (!assignee || task.assignee?.name === assignee)
48 | );
49 | });
50 | }, [filters, tasks]);
51 |
52 | const handleFilterChange = (e) => {
53 | const { name, value } = e.target;
54 | setFilters((prev) => ({ ...prev, [name]: value }));
55 | };
56 |
57 | const handleStatusChange = async (taskId, newStatus) => {
58 | try {
59 | toast.loading("Updating status...");
60 |
61 | // Simulate API call
62 | await new Promise((resolve) => setTimeout(resolve, 2000));
63 |
64 | let updatedTask = structuredClone(tasks.find((t) => t.id === taskId));
65 | updatedTask.status = newStatus;
66 | dispatch(updateTask(updatedTask));
67 |
68 | toast.dismissAll();
69 | toast.success("Task status updated successfully");
70 | } catch (error) {
71 | toast.dismissAll();
72 | toast.error(error?.response?.data?.message || error.message);
73 | }
74 | };
75 |
76 | const handleDelete = async () => {
77 | try {
78 | const confirm = window.confirm("Are you sure you want to delete the selected tasks?");
79 | if (!confirm) return;
80 |
81 | toast.loading("Deleting tasks...");
82 |
83 | // Simulate API call
84 | await new Promise((resolve) => setTimeout(resolve, 2000));
85 |
86 | dispatch(deleteTask(selectedTasks));
87 |
88 | toast.dismissAll();
89 | toast.success("Tasks deleted successfully");
90 | } catch (error) {
91 | toast.dismissAll();
92 | toast.error(error?.response?.data?.message || error.message);
93 | }
94 | };
95 |
96 | return (
97 |
98 | {/* Filters */}
99 |
100 | {["status", "type", "priority", "assignee"].map((name) => {
101 | const options = {
102 | status: [
103 | { label: "All Statuses", value: "" },
104 | { label: "To Do", value: "TODO" },
105 | { label: "In Progress", value: "IN_PROGRESS" },
106 | { label: "Done", value: "DONE" },
107 | ],
108 | type: [
109 | { label: "All Types", value: "" },
110 | { label: "Task", value: "TASK" },
111 | { label: "Bug", value: "BUG" },
112 | { label: "Feature", value: "FEATURE" },
113 | { label: "Improvement", value: "IMPROVEMENT" },
114 | { label: "Other", value: "OTHER" },
115 | ],
116 | priority: [
117 | { label: "All Priorities", value: "" },
118 | { label: "Low", value: "LOW" },
119 | { label: "Medium", value: "MEDIUM" },
120 | { label: "High", value: "HIGH" },
121 | ],
122 | assignee: [
123 | { label: "All Assignees", value: "" },
124 | ...assigneeList.map((n) => ({ label: n, value: n })),
125 | ],
126 | };
127 | return (
128 |
129 | {options[name].map((opt, idx) => (
130 | {opt.label}
131 | ))}
132 |
133 | );
134 | })}
135 |
136 | {/* Reset filters */}
137 | {(filters.status || filters.type || filters.priority || filters.assignee) && (
138 | setFilters({ status: "", type: "", priority: "", assignee: "" })} className="px-3 py-1 flex items-center gap-2 rounded bg-gradient-to-br from-purple-400 to-purple-500 text-zinc-100 dark:text-zinc-200 text-sm transition-colors" >
139 | Reset
140 |
141 | )}
142 |
143 | {selectedTasks.length > 0 && (
144 |
145 | Delete
146 |
147 | )}
148 |
149 |
150 | {/* Tasks Table */}
151 |
152 |
153 | {/* Desktop/Table View */}
154 |
224 |
225 | {/* Mobile/Card View */}
226 |
227 | {filteredTasks.length > 0 ? (
228 | filteredTasks.map((task) => {
229 | const { icon: Icon, color } = typeIcons[task.type] || {};
230 | const { background, prioritycolor } = priorityTexts[task.priority] || {};
231 |
232 | return (
233 |
234 |
235 |
{task.title}
236 | selectedTasks.includes(task.id) ? setSelectedTasks(selectedTasks.filter((i) => i !== task.id)) : setSelectedTasks((prev) => [...prev, task.id])} checked={selectedTasks.includes(task.id)} />
237 |
238 |
239 |
240 | {Icon && }
241 | {task.type}
242 |
243 |
244 |
245 |
246 | {task.priority}
247 |
248 |
249 |
250 |
251 | Status
252 | handleStatusChange(task.id, e.target.value)} value={task.status} className="w-full mt-1 bg-zinc-100 dark:bg-zinc-800 ring-1 ring-zinc-300 dark:ring-zinc-700 outline-none px-2 py-1 rounded text-sm text-zinc-900 dark:text-zinc-200" >
253 | To Do
254 | In Progress
255 | Done
256 |
257 |
258 |
259 |
260 |
261 | {task.assignee?.name || "-"}
262 |
263 |
264 |
265 |
266 | {format(new Date(task.due_date), "dd MMMM")}
267 |
268 |
269 | );
270 | })
271 | ) : (
272 |
273 | No tasks found for the selected filters.
274 |
275 | )}
276 |
277 |
278 |
279 |
280 | );
281 | };
282 |
283 | export default ProjectTasks;
284 |
--------------------------------------------------------------------------------
/src/assets/assets.js:
--------------------------------------------------------------------------------
1 | import workspace_img_default from "./workspace_img_default.png";
2 | import profile_img_a from "./profile_img_a.svg";
3 | import profile_img_o from "./profile_img_o.svg";
4 | import profile_img_j from "./profile_img_j.svg";
5 |
6 | export const assets = {
7 | workspace_img_default,
8 | profile_img_a,
9 | profile_img_o,
10 | profile_img_j,
11 | }
12 |
13 | export const dummyUsers = [
14 | {
15 | "id": "user_1",
16 | "name": "Alex Smith",
17 | "email": "alexsmith@example.com",
18 | "image": profile_img_a,
19 | "createdAt": "2025-10-06T11:04:03.485Z",
20 | "updatedAt": "2025-10-06T11:04:03.485Z"
21 | },
22 | {
23 | "id": "user_2",
24 | "name": "John Warrel",
25 | "email": "johnwarrel@example.com",
26 | "image": profile_img_j,
27 | "createdAt": "2025-10-09T13:20:24.360Z",
28 | "updatedAt": "2025-10-09T13:20:24.360Z"
29 | },
30 | {
31 | "id": "user_3",
32 | "name": "Oliver Watts",
33 | "email": "oliverwatts@example.com",
34 | "image": profile_img_o,
35 | "createdAt": "2025-09-01T04:31:22.043Z",
36 | "updatedAt": "2025-09-26T09:03:37.866Z"
37 | }
38 | ]
39 |
40 | export const dummyWorkspaces = [
41 | {
42 | "id": "org_1",
43 | "name": "Corp Workspace",
44 | "slug": "corp-workspace",
45 | "description": null,
46 | "settings": {},
47 | "ownerId": "user_3",
48 | "createdAt": "2025-10-13T06:55:44.423Z",
49 | "image_url": workspace_img_default,
50 | "updatedAt": "2025-10-13T07:17:36.890Z",
51 | "members": [
52 | {
53 | "id": "a7422a50-7dfb-4e34-989c-881481250f0e",
54 | "userId": "user_1",
55 | "workspaceId": "org_1",
56 | "message": "",
57 | "role": "ADMIN",
58 | "user": dummyUsers[0],
59 | },
60 | {
61 | "id": "b325ed10-00d8-4e22-b94d-33a9994fd06b",
62 | "userId": "user_2",
63 | "workspaceId": "org_1",
64 | "message": "",
65 | "role": "ADMIN",
66 | "user": dummyUsers[1],
67 | },
68 | {
69 | "id": "0f786ac0-62f7-493f-a5a0-787fd7c9c8b3",
70 | "userId": "user_3",
71 | "workspaceId": "org_1",
72 | "message": "",
73 | "role": "ADMIN",
74 | "user": dummyUsers[2],
75 | }
76 | ],
77 | "projects": [
78 | {
79 | "id": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
80 | "name": "LaunchPad CRM",
81 | "description": "A next-gen CRM for startups to manage customer pipelines, analytics, and automation.",
82 | "priority": "HIGH",
83 | "status": "ACTIVE",
84 | "start_date": "2025-10-10T00:00:00.000Z",
85 | "end_date": "2026-02-28T00:00:00.000Z",
86 | "team_lead": "user_3",
87 | "workspaceId": "org_1",
88 | "progress": 65,
89 | "createdAt": "2025-10-13T08:01:35.491Z",
90 | "updatedAt": "2025-10-13T08:01:45.620Z",
91 | "tasks": [
92 | {
93 | "id": "24ca6d74-7d32-41db-a257-906a90bca8f4",
94 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
95 | "title": "Design Dashboard UI",
96 | "description": "Create a modern, responsive CRM dashboard layout.",
97 | "status": "IN_PROGRESS",
98 | "type": "FEATURE",
99 | "priority": "HIGH",
100 | "assigneeId": "user_1",
101 | "due_date": "2025-10-31T00:00:00.000Z",
102 | "createdAt": "2025-10-13T08:04:04.084Z",
103 | "updatedAt": "2025-10-13T08:04:04.084Z",
104 | "assignee": dummyUsers[0],
105 | "comments": []
106 | },
107 | {
108 | "id": "9dbd5f04-5a29-4232-9e8c-a1d8e4c566df",
109 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
110 | "title": "Integrate Email API",
111 | "description": "Set up SendGrid integration for email campaigns.",
112 | "status": "TODO",
113 | "type": "TASK",
114 | "priority": "MEDIUM",
115 | "assigneeId": "user_2",
116 | "due_date": "2025-11-30T00:00:00.000Z",
117 | "createdAt": "2025-10-13T08:10:31.922Z",
118 | "updatedAt": "2025-10-13T08:10:31.922Z",
119 | "assignee": dummyUsers[1],
120 | "comments": []
121 | },
122 | {
123 | "id": "0e6798ad-8a1d-4bca-b0cd-8199491dbf03",
124 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
125 | "title": "Fix Duplicate Contact Bug",
126 | "description": "Duplicate records appear when importing CSV files.",
127 | "status": "TODO",
128 | "type": "BUG",
129 | "priority": "HIGH",
130 | "assigneeId": "user_1",
131 | "due_date": "2025-12-05T00:00:00.000Z",
132 | "createdAt": "2025-10-13T08:11:33.779Z",
133 | "updatedAt": "2025-10-13T08:11:33.779Z",
134 | "assignee": dummyUsers[0],
135 | "comments": []
136 | },
137 | {
138 | "id": "7989b4cc-1234-4816-a1d9-cc86cd09596a",
139 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
140 | "title": "Add Role-Based Access Control (RBAC)",
141 | "description": "Define user roles and permissions for the dashboard.",
142 | "status": "IN_PROGRESS",
143 | "type": "IMPROVEMENT",
144 | "priority": "MEDIUM",
145 | "assigneeId": "user_2",
146 | "due_date": "2025-12-20T00:00:00.000Z",
147 | "createdAt": "2025-10-13T08:12:35.146Z",
148 | "updatedAt": "2025-10-13T08:12:35.146Z",
149 | "assignee": dummyUsers[1],
150 | "comments": []
151 | }
152 | ],
153 | "members": [
154 | {
155 | "id": "17dc3764-737f-4584-9b54-d1a3b401527d",
156 | "userId": "user_1",
157 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
158 | "user": dummyUsers[0]
159 | },
160 | {
161 | "id": "774b0f38-7fd7-431a-b3bd-63262f036ca9",
162 | "userId": "user_2",
163 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
164 | "user": dummyUsers[1]
165 | },
166 | {
167 | "id": "573354b2-6649-4c7e-b4cc-7c94c93df340",
168 | "userId": "user_3",
169 | "projectId": "4d0f6ef3-e798-4d65-a864-00d9f8085c51",
170 | "user": dummyUsers[2]
171 | }
172 | ]
173 | },
174 | {
175 | "id": "e5f0a667-e883-41c4-8c87-acb6494d6341",
176 | "name": "Brand Identity Overhaul",
177 | "description": "Rebranding client products with cohesive color palettes and typography systems.",
178 | "priority": "MEDIUM",
179 | "status": "PLANNING",
180 | "start_date": "2025-10-18T00:00:00.000Z",
181 | "end_date": "2026-03-10T00:00:00.000Z",
182 | "team_lead": "user_3",
183 | "workspaceId": "org_1",
184 | "progress": 25,
185 | "createdAt": "2025-10-13T08:15:27.895Z",
186 | "updatedAt": "2025-10-13T08:16:32.157Z",
187 | "tasks": [
188 | {
189 | "id": "a51bd102-6789-4e60-81ba-57768c63b7db",
190 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
191 | "title": "Create New Logo Concepts",
192 | "description": "Sketch and finalize 3 logo concepts for client review.",
193 | "status": "IN_PROGRESS",
194 | "type": "FEATURE",
195 | "priority": "MEDIUM",
196 | "assigneeId": "user_2",
197 | "due_date": "2025-10-31T00:00:00.000Z",
198 | "createdAt": "2025-10-13T08:16:19.936Z",
199 | "updatedAt": "2025-10-13T08:16:19.936Z",
200 | "assignee": dummyUsers[1],
201 | "comments": []
202 | },
203 | {
204 | "id": "c7cafc09-5138-4918-9277-5ab94b520410",
205 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
206 | "title": "Update Typography System",
207 | "description": "Introduce new font hierarchy with responsive scaling.",
208 | "status": "TODO",
209 | "type": "IMPROVEMENT",
210 | "priority": "MEDIUM",
211 | "assigneeId": "user_1",
212 | "due_date": "2025-11-15T00:00:00.000Z",
213 | "createdAt": "2025-10-13T08:17:36.730Z",
214 | "updatedAt": "2025-10-13T08:17:36.730Z",
215 | "assignee": dummyUsers[0],
216 | "comments": []
217 | },
218 | {
219 | "id": "53883b41-1912-460e-8501-43363ff3f5d4",
220 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
221 | "title": "Client Feedback Integration",
222 | "description": "Implement client-requested adjustments to the brand guide.",
223 | "status": "TODO",
224 | "type": "TASK",
225 | "priority": "LOW",
226 | "assigneeId": "user_2",
227 | "due_date": "2025-10-31T00:00:00.000Z",
228 | "createdAt": "2025-10-13T08:18:16.611Z",
229 | "updatedAt": "2025-10-13T08:18:16.611Z",
230 | "assignee": dummyUsers[1],
231 | "comments": []
232 | }
233 | ],
234 | "members": [
235 | {
236 | "id": "32ad603e-c290-4f6e-860b-10212e1b080d",
237 | "userId": "user_1",
238 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
239 | "user": dummyUsers[0],
240 | },
241 | {
242 | "id": "10e8e546-ac59-474a-a3fc-768795810c65",
243 | "userId": "user_2",
244 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
245 | "user": dummyUsers[1],
246 | },
247 | {
248 | "id": "5a1f3c12-fcb2-40ef-91ee-dbd582219a8b",
249 | "userId": "user_3",
250 | "projectId": "e5f0a667-e883-41c4-8c87-acb6494d6341",
251 | "user": dummyUsers[2],
252 | }
253 | ]
254 | }
255 | ],
256 | "owner": dummyUsers[2],
257 | },
258 | {
259 | "id": "org_2",
260 | "name": "Cloud Ops Hub",
261 | "slug": "cloud-ops-hub",
262 | "description": null,
263 | "settings": {},
264 | "ownerId": "user_3",
265 | "createdAt": "2025-10-13T08:19:36.035Z",
266 | "image_url": workspace_img_default,
267 | "updatedAt": "2025-10-13T08:19:36.035Z",
268 | "members": [
269 | {
270 | "id": "f5d37afc-c287-4bd8-a607-b50d20837234",
271 | "userId": "user_3",
272 | "workspaceId": "org_2",
273 | "message": "",
274 | "role": "ADMIN",
275 | "user": dummyUsers[2],
276 | },
277 | {
278 | "id": "f5c04fe5-a0f5-4d34-bcf6-ea54dce1b546",
279 | "userId": "user_1",
280 | "workspaceId": "org_2",
281 | "message": "",
282 | "role": "ADMIN",
283 | "user": dummyUsers[0],
284 | },
285 | {
286 | "id": "9b29463a-e828-4d4e-9d64-8e57a3ad1a90",
287 | "userId": "user_2",
288 | "workspaceId": "org_2",
289 | "message": "",
290 | "role": "ADMIN",
291 | "user": dummyUsers[1],
292 | }
293 | ],
294 | "projects": [
295 | {
296 | "id": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
297 | "name": "Kubernetes Migration",
298 | "description": "Migrate the monolithic app infrastructure to Kubernetes for scalability.",
299 | "priority": "HIGH",
300 | "status": "ACTIVE",
301 | "start_date": "2025-10-15T00:00:00.000Z",
302 | "end_date": "2026-01-20T00:00:00.000Z",
303 | "team_lead": "user_3",
304 | "workspaceId": "org_2",
305 | "progress": 0,
306 | "createdAt": "2025-10-13T09:04:30.225Z",
307 | "updatedAt": "2025-10-13T09:04:30.225Z",
308 | "tasks": [
309 | {
310 | "id": "fc8ac710-ad12-4508-b934-9d59dea01872",
311 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
312 | "title": "Security Audit",
313 | "description": "Run container vulnerability scans and review IAM roles.",
314 | "status": "TODO",
315 | "type": "OTHER",
316 | "priority": "MEDIUM",
317 | "assigneeId": "user_3",
318 | "due_date": "2025-12-10T00:00:00.000Z",
319 | "createdAt": "2025-10-13T09:05:59.062Z",
320 | "updatedAt": "2025-10-13T09:05:59.062Z",
321 | "assignee": dummyUsers[2],
322 | "comments": []
323 | },
324 | {
325 | "id": "1cd6f85d-889a-4a5b-901f-ed8fa221d62b",
326 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
327 | "title": "Set Up EKS Cluster",
328 | "description": "Provision EKS cluster on AWS and configure nodes.",
329 | "status": "TODO",
330 | "type": "TASK",
331 | "priority": "HIGH",
332 | "assigneeId": "user_1",
333 | "due_date": "2025-12-15T00:00:00.000Z",
334 | "createdAt": "2025-10-13T09:04:58.859Z",
335 | "updatedAt": "2025-10-13T09:04:58.859Z",
336 | "assignee": dummyUsers[0],
337 | "comments": []
338 | },
339 | {
340 | "id": "8125eeac-196d-4797-8b14-21260f46abcc",
341 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
342 | "title": "Implement CI/CD with GitHub Actions",
343 | "description": "Add build, test, and deploy steps using GitHub Actions.",
344 | "status": "TODO",
345 | "type": "TASK",
346 | "priority": "MEDIUM",
347 | "assigneeId": "user_2",
348 | "due_date": "2025-10-31T00:00:00.000Z",
349 | "createdAt": "2025-10-13T09:05:25.518Z",
350 | "updatedAt": "2025-10-13T09:05:25.518Z",
351 | "assignee": dummyUsers[1],
352 | "comments": []
353 | }
354 | ],
355 | "members": [
356 | {
357 | "id": "511552d5-eddd-4b12-a60d-fad0821682a7",
358 | "userId": "user_3",
359 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
360 | "user": dummyUsers[2],
361 | },
362 | {
363 | "id": "79c364eb-eca5-4056-bea9-46c2f54efe4c",
364 | "userId": "user_1",
365 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
366 | "user": dummyUsers[0],
367 | },
368 | {
369 | "id": "5fcbda36-d327-4615-bb38-d871a014fe52",
370 | "userId": "user_2",
371 | "projectId": "c45e93ec-2f68-4f07-af4b-aa84f1bd407c",
372 | "user": dummyUsers[1],
373 | }
374 | ]
375 | },
376 | {
377 | "id": "b190343f-a7b1-4a40-b483-ecc59835cba3",
378 | "name": "Project: Automated Regression Suite",
379 | "description": "Selenium + Playwright hybrid test framework for regression testing.",
380 | "priority": "MEDIUM",
381 | "status": "ACTIVE",
382 | "start_date": "2025-10-03T00:00:00.000Z",
383 | "end_date": "2025-10-15T00:00:00.000Z",
384 | "team_lead": "user_3",
385 | "workspaceId": "org_2",
386 | "progress": 0,
387 | "createdAt": "2025-10-13T09:08:30.202Z",
388 | "updatedAt": "2025-10-13T09:08:30.202Z",
389 | "tasks": [
390 | {
391 | "id": "8836edf0-b4d7-4eec-a170-960d715a0b7f",
392 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
393 | "title": "Migrate to Playwright 1.48",
394 | "description": "Update scripts to use latest Playwright features.",
395 | "status": "IN_PROGRESS",
396 | "type": "IMPROVEMENT",
397 | "priority": "HIGH",
398 | "assigneeId": "user_1",
399 | "due_date": "2025-10-31T00:00:00.000Z",
400 | "createdAt": "2025-10-13T09:09:15.029Z",
401 | "updatedAt": "2025-10-13T09:09:15.029Z",
402 | "assignee": dummyUsers[0],
403 | "comments": []
404 | },
405 | {
406 | "id": "ce3dc378-f959-42f4-b12b-4c6cae6195c9",
407 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
408 | "title": "Parallel Test Execution",
409 | "description": "Enable concurrent test runs across CI pipelines.",
410 | "status": "TODO",
411 | "type": "TASK",
412 | "priority": "MEDIUM",
413 | "assigneeId": "user_2",
414 | "due_date": "2025-11-28T00:00:00.000Z",
415 | "createdAt": "2025-10-13T09:09:55.827Z",
416 | "updatedAt": "2025-10-13T09:09:55.827Z",
417 | "assignee": dummyUsers[1],
418 | "comments": []
419 | },
420 | {
421 | "id": "e01fda50-8818-4635-bcb6-9cde5c140b3d",
422 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
423 | "title": "Visual Snapshot Comparison",
424 | "description": "Implement screenshot diffing for UI regression detection.",
425 | "status": "TODO",
426 | "type": "FEATURE",
427 | "priority": "LOW",
428 | "assigneeId": "user_1",
429 | "due_date": "2025-11-20T00:00:00.000Z",
430 | "createdAt": "2025-10-13T09:10:27.049Z",
431 | "updatedAt": "2025-10-13T09:10:27.049Z",
432 | "assignee": dummyUsers[0],
433 | "comments": []
434 | }
435 | ],
436 | "members": [
437 | {
438 | "id": "1a0d5a66-c2ca-4294-9735-f3bd287500fa",
439 | "userId": "user_3",
440 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
441 | "user": dummyUsers[2],
442 | },
443 | {
444 | "id": "5ea89fe0-64b5-4737-a379-a9d89790ea3a",
445 | "userId": "user_1",
446 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
447 | "user": dummyUsers[0],
448 | },
449 | {
450 | "id": "320b617a-165e-42ec-8065-05da2d10b622",
451 | "userId": "user_2",
452 | "projectId": "b190343f-a7b1-4a40-b483-ecc59835cba3",
453 | "user": dummyUsers[1],
454 | }
455 | ]
456 | }
457 | ],
458 | "owner": dummyUsers[2],
459 | }
460 | ]
--------------------------------------------------------------------------------