├── 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 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
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 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 |
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 |
42 |
43 |
44 |
45 | 46 | 47 | ); 48 | }; 49 | 50 | export default ProjectCard; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

project-management Favicon 3 | project-management

4 |

5 | An open-source project management platform built with ReactJS and Tailwind CSS. 6 |

7 |

8 | License 9 | PRs Welcome 10 | GitHub issues 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 | 21 | 22 | {/* Search Input */} 23 |
24 | 25 | 30 |
31 |
32 | 33 | {/* Right section */} 34 |
35 | 36 | {/* Theme Toggle */} 37 | 44 | 45 | {/* User Button */} 46 | User Avatar 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 | 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 |
38 | {/* Email */} 39 |
40 | 43 |
44 | 45 | setFormData({ ...formData, email: e.target.value })} placeholder="Enter email address" className="pl-10 mt-1 w-full rounded border border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900 text-zinc-900 dark:text-zinc-200 text-sm placeholder-zinc-400 dark:placeholder-zinc-500 py-2 focus:outline-none focus:border-blue-500" required /> 46 |
47 |
48 | 49 | {/* Role */} 50 |
51 | 52 | 56 |
57 | 58 | {/* Footer */} 59 |
60 | 63 | 66 |
67 |
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 |
44 | {/* Email */} 45 |
46 | 49 |
50 | 51 | {/* List All non project members from current workspace */} 52 | 60 |
61 |
62 | 63 | {/* Footer */} 64 |
65 | 68 | 71 |
72 |
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 | 40 | 41 |
42 | 43 |
44 | {projects.map((project) => ( 45 |
46 | 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 | 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 | {ws.name} 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 | 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 | 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 | 77 | 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 | 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 | 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 |
96 |
97 |
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 |
42 | {/* Name */} 43 |
44 | 45 | setFormData({ ...formData, name: e.target.value })} className={inputClasses} required /> 46 |
47 | 48 | {/* Description */} 49 |
50 | 51 |