├── .eslintrc.json
├── .gitignore
├── README.md
├── actions
├── issues.js
├── organizations.js
├── projects.js
└── sprints.js
├── app
├── (auth)
│ ├── layout.js
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.jsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.jsx
├── (main)
│ ├── layout.js
│ ├── onboarding
│ │ └── [[...onboarding]]
│ │ │ └── page.jsx
│ ├── organization
│ │ └── [orgId]
│ │ │ ├── _components
│ │ │ ├── delete-project.jsx
│ │ │ ├── project-list.jsx
│ │ │ └── user-issues.jsx
│ │ │ └── page.jsx
│ └── project
│ │ ├── [projectId]
│ │ ├── layout.jsx
│ │ └── page.jsx
│ │ ├── _components
│ │ ├── board-filters.jsx
│ │ ├── create-issue.jsx
│ │ ├── create-sprint.jsx
│ │ ├── sprint-board.jsx
│ │ └── sprint-manager.jsx
│ │ └── create
│ │ └── page.jsx
├── android-chrome-512x512.png
├── favicon.ico
├── globals.css
├── layout.js
├── lib
│ └── validators.js
├── not-found.jsx
└── page.js
├── components.json
├── components
├── header.jsx
├── issue-card.jsx
├── issue-details-dialog.jsx
├── org-switcher.jsx
├── theme_provider.jsx
├── ui
│ ├── accordion.jsx
│ ├── avatar.jsx
│ ├── badge.jsx
│ ├── button.jsx
│ ├── card.jsx
│ ├── carousel.jsx
│ ├── dialog.jsx
│ ├── drawer.jsx
│ ├── input.jsx
│ ├── popover.jsx
│ ├── select.jsx
│ ├── sonner.jsx
│ ├── tabs.jsx
│ └── textarea.jsx
├── user-avatar.jsx
├── user-loading.jsx
└── user-menu.jsx
├── data
├── faqs.json
└── status.json
├── hooks
└── use-fetch.js
├── jsconfig.json
├── lib
├── checkUser.js
├── prisma.js
└── utils.js
├── middleware.js
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
├── migrations
│ ├── 20250329074727_init
│ │ └── migration.sql
│ ├── 20250329075249_init
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── scrumLogo.png
└── taskVaultLogo.png
└── tailwind.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules" : {
4 | "no-unused-vars":["warn"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🧠 TaskVault – Scalable Multi-Tenant Issue Tracker
2 |
3 | A powerful, ERP-style issue-tracking system tailored for modern organizations. TaskVault supports **multi-tenancy**, **role-based access**, **real-time updates**, and **rich analytics** to streamline issue resolution across diverse teams and departments.
4 |
5 | ---
6 |
7 | ## 🚀 Overview
8 |
9 | **TaskVault** is a web-based, full-stack issue tracker built with **PostgreSQL**, **Node.js**, and **React.js**. It is designed for organizations managing multiple clients or departments under one platform, delivering secure, scalable, and insightful tracking capabilities.
10 |
11 | ---
12 |
13 | ## 🎯 Problem Statement
14 |
15 | Traditional issue-tracking tools often fall short when scaling across departments or client teams:
16 |
17 | - ❌ Single-tenant limitations
18 | - ❌ Weak role-based access control
19 | - ❌ Limited analytics and scalability
20 |
21 | **TaskVault** addresses these challenges with:
22 |
23 | - 🏢 Multi-tenant architecture
24 | - 🔐 Structured user roles (Admin, Manager, Employee)
25 | - 📊 Responsive dashboards and analytics
26 |
27 | ---
28 |
29 | ## 🧩 Core Features
30 |
31 | - 🔐 **Role-Based Access Control (RBAC)** – Secure, role-specific permissions
32 | - 🏢 **Multi-Tenant Support** – Isolated data per organization/client
33 | - 🧾 **Comprehensive Issue Management** – Create, prioritize, assign, resolve
34 | - 📊 **Admin Dashboard** – Visualize issue metrics and team activity
35 | - 🔔 **Real-Time Notifications** – Stay instantly updated
36 | - 📱 **Responsive UI** – Optimized for desktop and mobile
37 | - 📈 **Analytics & Reports** – Track trends, SLA adherence, and performance
38 |
39 | ---
40 |
41 | ## 🛠️ Technology Stack
42 |
43 | ### 🔙 Backend
44 |
45 | - **Node.js**
46 | - **PostgreSQL** (multi-tenant schema)
47 | - **Clerk** – Authentication and security
48 |
49 | ### 🔜 Frontend
50 |
51 | - **React.js** – SPA with dynamic routing
52 | - **Tailwind CSS** & **ShadCN** – Modern, accessible UI components
53 |
54 | ---
55 |
56 | ## 🧱 Database Schema Overview
57 |
58 | | Table | Description |
59 | |------------------|--------------------------------------------|
60 | | `users` | User data, roles, and organization linkage |
61 | | `organizations` | Organization-specific metadata |
62 | | `issues` | Issue records with status, priority, and ownership |
63 |
64 | ---
65 |
66 | ## 📌 Key Modules
67 |
68 | - ✅ User Authentication (Clerk)
69 | - 🔄 Organization Switching
70 | - 🔧 Issue Lifecycle Management
71 | - 🔔 Notifications & Alerts
72 | - 📊 Admin Controls & Analytics
73 |
74 | ---
75 |
76 | ## 📅 Development Roadmap
77 |
78 | ### Phase 1 – Backend
79 |
80 | - PostgreSQL multi-tenant schema
81 | - Organization filtering via Clerk
82 |
83 | ### Phase 2 – Frontend
84 |
85 | - React dashboard setup
86 | - Role-based navigation
87 | - Tailwind CSS & ShadCN styling
88 |
89 | ### Phase 3 – Integration
90 |
91 | - API integration
92 | - Protected routes & session handling
93 |
94 | ### Final Phase – Add-ons
95 |
96 | - Auto-assignment of engineers
97 | - Admin statistics & reporting tools
98 |
99 | ---
100 |
101 | ## 🔮 Future Enhancements
102 |
103 | - 📱 Mobile App (React Native)
104 | - 🤖 AI-based Issue Prioritization
105 | - 📂 ERP Module Integration (HR, Finance, etc.)
106 |
107 | ---
108 |
109 | ## 📄 Environment Variables
110 |
111 | Create a `.env` file in the root directory with the following keys:
112 |
113 | ```env
114 | DATABASE_URL=
115 |
116 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
117 | CLERK_SECRET_KEY=
118 |
119 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
120 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
121 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/onboarding
122 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
123 | ```
124 |
125 | ---
126 |
127 | ## 💻 Running the Project Locally
128 |
129 | To run TaskVault locally, follow these steps:
130 |
131 | 1. **Clone the repository**
132 | ```bash
133 | git clone https://github.com/your-username/taskvault.git
134 | cd taskvault
135 | ```
136 |
137 | 2. **Install dependencies**
138 | ```bash
139 | npm install
140 | ```
141 |
142 | 3. **Set up environment variables**
143 | Create a `.env` file in the root folder and add all required variables listed above.
144 |
145 | 4. **Run the development server**
146 | ```bash
147 | npm run dev
148 | ```
149 |
150 | 5. Visit `http://localhost:3000` to start using the application.
151 |
152 | ---
153 |
154 | ## 🤝 Contributing
155 |
156 | We welcome contributions to **TaskVault**! To contribute:
157 |
158 | 1. Fork the repository
159 | 2. Create a new branch (`git checkout -b feature-name`)
160 | 3. Commit your changes (`git commit -m "Add feature"`)
161 | 4. Push to the branch (`git push origin feature-name`)
162 | 5. Create a Pull Request
163 |
164 | Feel free to open issues for suggestions or bugs. Let's build something great together!
165 |
166 | ---
167 |
--------------------------------------------------------------------------------
/actions/issues.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 |
6 | export async function getIssuesForSprint(sprintId) {
7 | const { userId, orgId } = auth();
8 |
9 | if (!userId || !orgId) {
10 | throw new Error("Unauthorized");
11 | }
12 |
13 | const issues = await db.issue.findMany({
14 | where: { sprintId: sprintId },
15 | orderBy: [{ status: "asc" }, { order: "asc" }],
16 | include: {
17 | assignee: true,
18 | reporter: true,
19 | },
20 | });
21 |
22 | return issues;
23 | }
24 |
25 | export async function createIssue(projectId, data) {
26 | const { userId, orgId } = auth();
27 |
28 | if (!userId || !orgId) {
29 | throw new Error("Unauthorized");
30 | }
31 |
32 | let user = await db.user.findUnique({ where: { clerkUserId: userId } });
33 |
34 | const lastIssue = await db.issue.findFirst({
35 | where: { projectId, status: data.status },
36 | orderBy: { order: "desc" },
37 | });
38 |
39 | const newOrder = lastIssue ? lastIssue.order + 1 : 0;
40 |
41 | const issue = await db.issue.create({
42 | data: {
43 | title: data.title,
44 | description: data.description,
45 | status: data.status,
46 | priority: data.priority,
47 | projectId: projectId,
48 | sprintId: data.sprintId,
49 | reporterId: user.id,
50 | assigneeId: data.assigneeId || null, // Add this line
51 | order: newOrder,
52 | },
53 | include: {
54 | assignee: true,
55 | reporter: true,
56 | },
57 | });
58 |
59 | return issue;
60 | }
61 |
62 | export async function updateIssueOrder(updatedIssues) {
63 | const { userId, orgId } = auth();
64 |
65 | if (!userId || !orgId) {
66 | throw new Error("Unauthorized");
67 | }
68 |
69 | // Start a transaction
70 | await db.$transaction(async (prisma) => {
71 | // Update each issue
72 | for (const issue of updatedIssues) {
73 | await prisma.issue.update({
74 | where: { id: issue.id },
75 | data: {
76 | status: issue.status,
77 | order: issue.order,
78 | },
79 | });
80 | }
81 | });
82 |
83 | return { success: true };
84 | }
85 |
86 | export async function deleteIssue(issueId) {
87 | const { userId, orgId } = auth();
88 |
89 | if (!userId || !orgId) {
90 | throw new Error("Unauthorized");
91 | }
92 |
93 | const user = await db.user.findUnique({
94 | where: { clerkUserId: userId },
95 | });
96 |
97 | if (!user) {
98 | throw new Error("User not found");
99 | }
100 |
101 | const issue = await db.issue.findUnique({
102 | where: { id: issueId },
103 | include: { project: true },
104 | });
105 |
106 | if (!issue) {
107 | throw new Error("Issue not found");
108 | }
109 |
110 | if (
111 | issue.reporterId !== user.id &&
112 | !issue.project.adminIds.includes(user.id)
113 | ) {
114 | throw new Error("You don't have permission to delete this issue");
115 | }
116 |
117 | await db.issue.delete({ where: { id: issueId } });
118 |
119 | return { success: true };
120 | }
121 |
122 | export async function updateIssue(issueId, data) {
123 | const { userId, orgId } = auth();
124 |
125 | if (!userId || !orgId) {
126 | throw new Error("Unauthorized");
127 | }
128 |
129 | try {
130 | const issue = await db.issue.findUnique({
131 | where: { id: issueId },
132 | include: { project: true },
133 | });
134 |
135 | if (!issue) {
136 | throw new Error("Issue not found");
137 | }
138 |
139 | if (issue.project.organizationId !== orgId) {
140 | throw new Error("Unauthorized");
141 | }
142 |
143 | const updatedIssue = await db.issue.update({
144 | where: { id: issueId },
145 | data: {
146 | status: data.status,
147 | priority: data.priority,
148 | },
149 | include: {
150 | assignee: true,
151 | reporter: true,
152 | },
153 | });
154 |
155 | return updatedIssue;
156 | } catch (error) {
157 | throw new Error("Error updating issue: " + error.message);
158 | }
159 | }
160 |
161 | export async function getUserIssues(userId){
162 | const {orgId} = auth();
163 |
164 | if(!userId || !ordId){
165 | throw new Error("No user id or organization id found");
166 | }
167 |
168 | const user = await db.user.findUnique({
169 | where: {clerkUserId: userId},
170 | });
171 |
172 | if(!user){
173 | throw new Error("User not found");
174 | }
175 |
176 | const issues= await db.issue.findMany({
177 | where:{
178 | OR : [{assigneeId:user.id},{reporterId: user.id}],
179 | project:{
180 | organizationId: orgId,
181 | },
182 | },
183 | include : {
184 | project: true,
185 | assignee: true,
186 | reporter: true,
187 | },
188 | orderBy: {updateAt : "desc"},
189 | });
190 |
191 | return issues;
192 | }
--------------------------------------------------------------------------------
/actions/organizations.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth, clerkClient } from "@clerk/nextjs/server";
5 |
6 | export async function getOrganization(slug) {
7 | const { userId } = auth();
8 | if (!userId) {
9 | throw new Error("Unauthorized");
10 | }
11 |
12 | const user = await db.user.findUnique({
13 | where: { clerkUserId: userId },
14 | });
15 |
16 | if (!user) {
17 | throw new Error("User not found");
18 | }
19 |
20 | // Get the organization details
21 | const organization = await clerkClient().organizations.getOrganization({
22 | slug,
23 | });
24 |
25 | if (!organization) {
26 | return null;
27 | }
28 |
29 | // Check if user belongs to this organization
30 | const { data: membership } =
31 | await clerkClient().organizations.getOrganizationMembershipList({
32 | organizationId: organization.id,
33 | });
34 |
35 | const userMembership = membership.find(
36 | (member) => member.publicUserData.userId === userId
37 | );
38 |
39 | // If user is not a member, return null
40 | if (!userMembership) {
41 | return null;
42 | }
43 |
44 | return organization;
45 | }
46 |
47 | export async function getProjects(orgId) {
48 | const { userId } = auth();
49 | if (!userId) {
50 | throw new Error("Unauthorized");
51 | }
52 |
53 | const user = await db.user.findUnique({
54 | where: { clerkUserId: userId },
55 | });
56 |
57 | if (!user) {
58 | throw new Error("User not found");
59 | }
60 |
61 | const projects = await db.project.findMany({
62 | where: { organizationId: orgId },
63 | orderBy: { createdAt: "desc" },
64 | });
65 |
66 | return projects;
67 | }
68 |
69 | export async function getUserIssues(userId) {
70 | const { orgId } = auth();
71 |
72 | if (!userId || !orgId) {
73 | throw new Error("No user id or organization id found");
74 | }
75 |
76 | const user = await db.user.findUnique({
77 | where: { clerkUserId: userId },
78 | });
79 |
80 | if (!user) {
81 | throw new Error("User not found");
82 | }
83 |
84 | const issues = await db.issue.findMany({
85 | where: {
86 | OR: [{ assigneeId: user.id }, { reporterId: user.id }],
87 | project: {
88 | organizationId: orgId,
89 | },
90 | },
91 | include: {
92 | project: true,
93 | assignee: true,
94 | reporter: true,
95 | },
96 | orderBy: { updatedAt: "desc" },
97 | });
98 |
99 | return issues;
100 | }
101 |
102 | export async function getOrganizationUsers(orgId) {
103 | const { userId } = auth();
104 | if (!userId) {
105 | throw new Error("Unauthorized");
106 | }
107 |
108 | const user = await db.user.findUnique({
109 | where: { clerkUserId: userId },
110 | });
111 |
112 | if (!user) {
113 | throw new Error("User not found");
114 | }
115 |
116 | const organizationMemberships =
117 | await clerkClient().organizations.getOrganizationMembershipList({
118 | organizationId: orgId,
119 | });
120 |
121 | const userIds = organizationMemberships.data.map(
122 | (membership) => membership.publicUserData.userId
123 | );
124 |
125 | const users = await db.user.findMany({
126 | where: {
127 | clerkUserId: {
128 | in: userIds,
129 | },
130 | },
131 | });
132 |
133 | return users;
134 | }
--------------------------------------------------------------------------------
/actions/projects.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth, clerkClient } from "@clerk/nextjs/server";
5 |
6 | export async function createProject(data) {
7 | const { userId, orgId } = auth();
8 |
9 | if (!userId) {
10 | throw new Error("Unauthorized");
11 | }
12 |
13 | if (!orgId) {
14 | throw new Error("No Organization Selected");
15 | }
16 |
17 | // Check if the user is an admin of the organization
18 | const { data: membershipList } =
19 | await clerkClient().organizations.getOrganizationMembershipList({
20 | organizationId: orgId,
21 | });
22 |
23 | const userMembership = membershipList.find(
24 | (membership) => membership.publicUserData.userId === userId
25 | );
26 |
27 | if (!userMembership || userMembership.role !== "org:admin") {
28 | throw new Error("Only organization admins can create projects");
29 | }
30 |
31 | try {
32 | const project = await db.project.create({
33 | data: {
34 | name: data.name,
35 | key: data.key,
36 | description: data.description,
37 | organizationId: orgId,
38 | },
39 | });
40 |
41 | return project;
42 | } catch (error) {
43 | throw new Error("Error creating project: " + error.message);
44 | }
45 | }
46 |
47 | export async function getProjects(orgId) {
48 | const { userId } = auth();
49 |
50 | if (!userId || !orgId) {
51 | throw new Error("Unauthorized");
52 | }
53 |
54 | // Find user to verify existence
55 | const user = await db.user.findUnique({
56 | where: { clerkUserId: userId },
57 | });
58 |
59 | if (!user) {
60 | throw new Error("User not found");
61 | }
62 |
63 | // Get project with sprints and organization
64 | const projects = await db.project.findMany({
65 | where : { organizationId: orgId},
66 | orderBy: {createdAt: "desc"},
67 | });
68 |
69 | // if (!project) {
70 | // throw new Error("Project not found");
71 | // }
72 |
73 | // // Verify project belongs to the organization
74 | // if (project.organizationId !== orgId) {
75 | // return null;
76 | // }
77 |
78 | return project;
79 | }
80 |
81 | export async function deleteProject(projectId) {
82 | const { userId, orgId, orgRole } = auth();
83 |
84 | if (!userId || !orgId) {
85 | throw new Error("Unauthorized");
86 | }
87 |
88 | if (orgRole !== "org:admin") {
89 | throw new Error("Only organization admins can delete projects");
90 | }
91 |
92 | const project = await db.project.findUnique({
93 | where: { id: projectId },
94 | });
95 |
96 | if (!project || project.organizationId !== orgId) {
97 | throw new Error(
98 | "Project not found or you don't have permission to delete it"
99 | );
100 | }
101 |
102 | await db.project.delete({
103 | where: { id: projectId },
104 | });
105 |
106 | return { success: true };
107 | }
108 |
109 | export async function getProject(projectId) {
110 | const { userId, orgId } = auth();
111 |
112 | if (!userId || !orgId) {
113 | throw new Error("Unauthorized");
114 | }
115 |
116 | // Find user to verify existence
117 | const user = await db.user.findUnique({
118 | where: { clerkUserId: userId },
119 | });
120 |
121 | if (!user) {
122 | throw new Error("User not found");
123 | }
124 |
125 | // Get project with sprints and organization
126 | const project = await db.project.findUnique({
127 | where: { id: projectId },
128 | include: {
129 | sprints: {
130 | orderBy: { createdAt: "desc" },
131 | },
132 | },
133 | });
134 |
135 | if (!project) {
136 | return null;
137 | }
138 |
139 | // Verify project belongs to the organization
140 | if (project.organizationId !== orgId) {
141 | return null;
142 | }
143 |
144 | return project;
145 | }
--------------------------------------------------------------------------------
/actions/sprints.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 |
6 | export async function createSprint(projectId, data) {
7 | const { userId, orgId } = auth();
8 |
9 | if (!userId || !orgId) {
10 | throw new Error("Unauthorized");
11 | }
12 |
13 | const project = await db.project.findUnique({
14 | where: { id: projectId },
15 | include: { sprints: { orderBy: { createdAt: "desc" } } },
16 | });
17 |
18 | if (!project || project.organizationId !== orgId) {
19 | throw new Error("Project not found");
20 | }
21 |
22 | const sprint = await db.sprint.create({
23 | data: {
24 | name: data.name,
25 | startDate: data.startDate,
26 | endDate: data.endDate,
27 | status: "PLANNED",
28 | projectId: projectId,
29 | },
30 | });
31 |
32 | return sprint;
33 | }
34 |
35 | export async function updateSprintStatus(sprintId, newStatus) {
36 | const { userId, orgId, orgRole } = auth();
37 |
38 | if (!userId || !orgId) {
39 | throw new Error("Unauthorized");
40 | }
41 |
42 | try {
43 | const sprint = await db.sprint.findUnique({
44 | where: { id: sprintId },
45 | include: { project: true },
46 | });
47 | console.log(sprint, orgRole);
48 |
49 | if (!sprint) {
50 | throw new Error("Sprint not found");
51 | }
52 |
53 | if (sprint.project.organizationId !== orgId) {
54 | throw new Error("Unauthorized");
55 | }
56 |
57 | if (orgRole !== "org:admin") {
58 | throw new Error("Only Admin can make this change");
59 | }
60 |
61 | const now = new Date();
62 | const startDate = new Date(sprint.startDate);
63 | const endDate = new Date(sprint.endDate);
64 |
65 | if (newStatus === "ACTIVE" && (now < startDate || now > endDate)) {
66 | throw new Error("Cannot start sprint outside of its date range");
67 | }
68 |
69 | if (newStatus === "COMPLETED" && sprint.status !== "ACTIVE") {
70 | throw new Error("Can only complete an active sprint");
71 | }
72 |
73 | const updatedSprint = await db.sprint.update({
74 | where: { id: sprintId },
75 | data: { status: newStatus },
76 | });
77 |
78 | return { success: true, sprint: updatedSprint };
79 | } catch (error) {
80 | throw new Error(error.message);
81 | }
82 | }
--------------------------------------------------------------------------------
/app/(auth)/layout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SignInPage = ({children}) => {
4 | return
{children}
;
5 | }
6 |
7 | export default SignInPage;
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.jsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 | import React from "react";
3 |
4 | const SignInPage = () => {
5 | return ;
6 | }
7 |
8 | export default SignInPage;
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.jsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 | import React from "react";
3 |
4 | const SignInPage = () => {
5 | return ;
6 | }
7 |
8 | export default SignInPage;
--------------------------------------------------------------------------------
/app/(main)/layout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Layout = ({ children }) => {
4 | return {children}
;
5 | };
6 |
7 | export default Layout;
--------------------------------------------------------------------------------
/app/(main)/onboarding/[[...onboarding]]/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { OrganizationList, useOrganization } from "@clerk/nextjs";
4 | import { useRouter } from "next/navigation";
5 | import { useEffect } from "react";
6 |
7 | export default function Onboarding() {
8 | const { organization } = useOrganization();
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | if (organization) {
13 | router.push(`/organization/${organization.slug}`);
14 | }
15 | // eslint-disable-next-line react-hooks/exhaustive-deps
16 | }, [organization]);
17 |
18 | return (
19 |
20 |
25 |
26 | );
27 | }
--------------------------------------------------------------------------------
/app/(main)/organization/[orgId]/_components/delete-project.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { Trash2 } from "lucide-react";
6 | import { useOrganization } from "@clerk/nextjs";
7 | import { deleteProject } from "@/actions/projects";
8 | import { useRouter } from "next/navigation";
9 | import useFetch from "@/hooks/use-fetch";
10 |
11 | export default function DeleteProject({ projectId }) {
12 | const { membership } = useOrganization();
13 | const router = useRouter();
14 |
15 | const {
16 | loading: isDeleting,
17 | error,
18 | fn: deleteProjectFn,
19 | data: deleted,
20 | } = useFetch(deleteProject);
21 |
22 | const isAdmin = membership?.role === "org:admin";
23 |
24 | const handleDelete = async () => {
25 | if (window.confirm("Are you sure you want to delete this project?")) {
26 | deleteProjectFn(projectId);
27 | }
28 | };
29 |
30 | useEffect(() => {
31 | if (deleted) {
32 | router.refresh();
33 | }
34 | // eslint-disable-next-line react-hooks/exhaustive-deps
35 | }, [deleted]);
36 |
37 | if (!isAdmin) return null;
38 |
39 | return (
40 | <>
41 |
48 |
49 |
50 | {error && {error.message}
}
51 | >
52 | );
53 | }
--------------------------------------------------------------------------------
/app/(main)/organization/[orgId]/_components/project-list.jsx:
--------------------------------------------------------------------------------
1 | // components/ProjectList.jsx
2 | import Link from "next/link";
3 | import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
4 | import { getProjects } from "@/actions/organizations";
5 | import DeleteProject from "./delete-project";
6 |
7 | export default async function ProjectList({ orgId }) {
8 | const projects = await getProjects(orgId);
9 |
10 | if (projects.length === 0) {
11 | return (
12 |
13 | No projects found.{" "}
14 |
18 | Create New.
19 |
20 |
21 | );
22 | }
23 |
24 | return (
25 |
26 | {projects.map((project) => (
27 |
28 |
29 |
30 | {project.name}
31 |
32 |
33 |
34 |
35 | {project.description}
36 |
40 | View Project
41 |
42 |
43 |
44 | ))}
45 |
46 | );
47 | }
--------------------------------------------------------------------------------
/app/(main)/organization/[orgId]/_components/user-issues.jsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Suspense } from "react";
4 | import { getUserIssues } from "@/actions/organizations";
5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6 | import IssueCard from "@/components/issue-card";
7 | import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from "recharts";
8 |
9 | export default async function UserIssues({ userId }) {
10 | const issues = await getUserIssues(userId);
11 |
12 | if (issues.length === 0) {
13 | return null;
14 | }
15 |
16 | const assignedIssues = issues.filter(
17 | (issue) => issue.assignee.clerkUserId === userId
18 | );
19 | const reportedIssues = issues.filter(
20 | (issue) => issue.reporter.clerkUserId === userId
21 | );
22 |
23 | const chartData = [
24 | {
25 | name: "Assigned",
26 | count: assignedIssues.length,
27 | },
28 | {
29 | name: "Reported",
30 | count: reportedIssues.length,
31 | },
32 | ];
33 |
34 | return (
35 | <>
36 | My Issues
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Assigned to You
54 | Reported by You
55 |
56 |
57 | Loading...}>
58 |
59 |
60 |
61 |
62 | Loading...}>
63 |
64 |
65 |
66 |
67 | >
68 | );
69 | }
70 |
71 | function IssueGrid({ issues }) {
72 | return (
73 |
74 | {issues.map((issue) => (
75 |
76 | ))}
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/app/(main)/organization/[orgId]/page.jsx:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server";
2 | import { redirect } from "next/navigation";
3 | import { getOrganization } from "@/actions/organizations";
4 | import OrgSwitcher from "@/components/org-switcher";
5 | import ProjectList from "./_components/project-list";
6 | import UserIssues from "./_components/user-issues";
7 |
8 | export default async function OrganizationPage({ params }) {
9 | const { orgId } = params;
10 | const { userId } = auth();
11 |
12 | if (!userId) {
13 | redirect("/sign-in");
14 | }
15 |
16 | const organization = await getOrganization(orgId);
17 |
18 | if (!organization) {
19 | return Organization not found
;
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 | {organization.name}’s Projects
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 | );
39 | }
--------------------------------------------------------------------------------
/app/(main)/project/[projectId]/layout.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { BarLoader } from "react-spinners";
3 |
4 | export default async function ProjectLayout({ children }) {
5 | return (
6 |
7 | Loading Projects....}>
8 | {children}
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/(main)/project/[projectId]/page.jsx:
--------------------------------------------------------------------------------
1 | import { getProject } from "@/actions/projects";
2 | import { notFound } from "next/navigation";
3 | import SprintCreationForm from "../_components/create-sprint";
4 | import SprintBoard from "../_components/sprint-board";
5 |
6 | export default async function ProjectPage({ params }) {
7 | const { projectId } = params;
8 | const project = await getProject(projectId);
9 |
10 | if (!project) {
11 | notFound();
12 | }
13 |
14 | return (
15 |
16 |
22 |
23 | {project.sprints.length > 0 ? (
24 |
29 | ) : (
30 |
Create a Sprint from button above
31 | )}
32 |
33 | );
34 | }
--------------------------------------------------------------------------------
/app/(main)/project/_components/board-filters.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { Input } from "@/components/ui/input";
5 | import { Button } from "@/components/ui/button";
6 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
7 | import { X } from "lucide-react";
8 | import {
9 | Select,
10 | SelectContent,
11 | SelectItem,
12 | SelectTrigger,
13 | SelectValue,
14 | } from "@/components/ui/select";
15 |
16 | const priorities = ["LOW", "MEDIUM", "HIGH", "URGENT"];
17 |
18 | export default function BoardFilters({ issues, onFilterChange }) {
19 | const [searchTerm, setSearchTerm] = useState("");
20 | const [selectedAssignees, setSelectedAssignees] = useState([]);
21 | const [selectedPriority, setSelectedPriority] = useState("");
22 |
23 | const assignees = issues
24 | .map((issue) => issue.assignee)
25 | .filter(
26 | (item, index, self) => index === self.findIndex((t) => t.id === item.id)
27 | );
28 |
29 | useEffect(() => {
30 | const filteredIssues = issues.filter(
31 | (issue) =>
32 | issue.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
33 | (selectedAssignees.length === 0 ||
34 | selectedAssignees.includes(issue.assignee?.id)) &&
35 | (selectedPriority === "" || issue.priority === selectedPriority)
36 | );
37 | onFilterChange(filteredIssues);
38 | }, [searchTerm, selectedAssignees, selectedPriority, issues]);
39 |
40 | const toggleAssignee = (assigneeId) => {
41 | setSelectedAssignees((prev) =>
42 | prev.includes(assigneeId)
43 | ? prev.filter((id) => id !== assigneeId)
44 | : [...prev, assigneeId]
45 | );
46 | };
47 |
48 | const clearFilters = () => {
49 | setSearchTerm("");
50 | setSelectedAssignees([]);
51 | setSelectedPriority("");
52 | };
53 |
54 | const isFiltersApplied =
55 | searchTerm !== "" ||
56 | selectedAssignees.length > 0 ||
57 | selectedPriority !== "";
58 |
59 | return (
60 |
61 |
62 |
setSearchTerm(e.target.value)}
67 | />
68 |
69 |
70 |
71 | {assignees.map((assignee, i) => {
72 | const selected = selectedAssignees.includes(assignee.id);
73 |
74 | return (
75 |
0 ? "-ml-6" : ""}`}
80 | style={{
81 | zIndex: i,
82 | }}
83 | onClick={() => toggleAssignee(assignee.id)}
84 | >
85 |
86 |
87 | {assignee.name[0]}
88 |
89 |
90 | );
91 | })}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | {priorities.map((priority) => (
101 |
102 | {priority}
103 |
104 | ))}
105 |
106 |
107 |
108 | {isFiltersApplied && (
109 |
114 | Clear Filters
115 |
116 | )}
117 |
118 |
119 | );
120 | }
--------------------------------------------------------------------------------
/app/(main)/project/_components/create-issue.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { BarLoader } from "react-spinners";
5 | import { useForm, Controller } from "react-hook-form";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import {
8 | Drawer,
9 | DrawerContent,
10 | DrawerHeader,
11 | DrawerTitle,
12 | } from "@/components/ui/drawer";
13 | import { Input } from "@/components/ui/input";
14 | import { Button } from "@/components/ui/button";
15 | import {
16 | Select,
17 | SelectContent,
18 | SelectItem,
19 | SelectTrigger,
20 | SelectValue,
21 | } from "@/components/ui/select";
22 | import MDEditor from "@uiw/react-md-editor";
23 | import useFetch from "@/hooks/use-fetch";
24 | import { createIssue } from "@/actions/issues";
25 | import { getOrganizationUsers } from "@/actions/organizations";
26 | import { issueSchema } from "@/app/lib/validators";
27 |
28 | export default function IssueCreationDrawer({
29 | isOpen,
30 | onClose,
31 | sprintId,
32 | status,
33 | projectId,
34 | onIssueCreated,
35 | orgId,
36 | }) {
37 | const {
38 | loading: createIssueLoading,
39 | fn: createIssueFn,
40 | error,
41 | data: newIssue,
42 | } = useFetch(createIssue);
43 |
44 | const {
45 | loading: usersLoading,
46 | fn: fetchUsers,
47 | data: users,
48 | } = useFetch(getOrganizationUsers);
49 |
50 | const {
51 | control,
52 | register,
53 | handleSubmit,
54 | formState: { errors },
55 | reset,
56 | } = useForm({
57 | resolver: zodResolver(issueSchema),
58 | defaultValues: {
59 | priority: "MEDIUM",
60 | description: "",
61 | assigneeId: "",
62 | },
63 | });
64 |
65 | useEffect(() => {
66 | if (isOpen && orgId) {
67 | fetchUsers(orgId);
68 | }
69 | }, [isOpen, orgId]);
70 |
71 | const onSubmit = async (data) => {
72 | await createIssueFn(projectId, {
73 | ...data,
74 | status,
75 | sprintId,
76 | });
77 | };
78 |
79 | useEffect(() => {
80 | if (newIssue) {
81 | reset();
82 | onClose();
83 | onIssueCreated();
84 | }
85 | // eslint-disable-next-line react-hooks/exhaustive-deps
86 | }, [newIssue, createIssueLoading]);
87 |
88 | return (
89 |
90 |
91 |
92 | Create New Issue
93 |
94 | {usersLoading && }
95 |
197 |
198 |
199 | );
200 | }
--------------------------------------------------------------------------------
/app/(main)/project/_components/create-sprint.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import { Input } from "@/components/ui/input";
7 | import {
8 | Popover,
9 | PopoverContent,
10 | PopoverTrigger,
11 | } from "@/components/ui/popover";
12 | import { Card, CardContent } from "@/components/ui/card";
13 |
14 | import { useForm, Controller } from "react-hook-form";
15 | import { zodResolver } from "@hookform/resolvers/zod";
16 | import { useRouter } from "next/navigation";
17 | import { CalendarIcon } from "lucide-react";
18 | import { DayPicker } from "react-day-picker";
19 | import { format, addDays } from "date-fns";
20 |
21 | import { sprintSchema } from "@/app/lib/validators";
22 | import useFetch from "@/hooks/use-fetch";
23 | import { createSprint } from "@/actions/sprints";
24 | import { toast } from "sonner";
25 |
26 | export default function SprintCreationForm({
27 | projectTitle,
28 | projectKey,
29 | projectId,
30 | sprintKey,
31 | }) {
32 | const [showForm, setShowForm] = useState(false);
33 | const [dateRange, setDateRange] = useState({
34 | from: new Date(),
35 | to: addDays(new Date(), 14),
36 | });
37 | const router = useRouter();
38 |
39 | const { loading: createSprintLoading, fn: createSprintFn } =
40 | useFetch(createSprint);
41 |
42 | const {
43 | register,
44 | control,
45 | handleSubmit,
46 | formState: { errors },
47 | } = useForm({
48 | resolver: zodResolver(sprintSchema),
49 | defaultValues: {
50 | name: `${projectKey}-${sprintKey}`,
51 | startDate: dateRange.from,
52 | endDate: dateRange.to,
53 | },
54 | });
55 |
56 | const onSubmit = async (data) => {
57 | await createSprintFn(projectId, {
58 | ...data,
59 | startDate: dateRange.from,
60 | endDate: dateRange.to,
61 | });
62 | setShowForm(false);
63 | toast.success("Sprint Created Successfully");
64 | router.refresh(); // Refresh the page to show updated data
65 | };
66 |
67 | return (
68 | <>
69 |
70 |
71 | {projectTitle}
72 |
73 | setShowForm(!showForm)}
76 | variant={!showForm ? "default" : "destructive"}
77 | >
78 | {!showForm ? "Create New Sprint" : "Cancel"}
79 |
80 |
81 | {showForm && (
82 |
83 |
84 |
165 |
166 |
167 | )}
168 | >
169 | );
170 | }
--------------------------------------------------------------------------------
/app/(main)/project/_components/sprint-board.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { toast } from "sonner";
5 | import { Plus } from "lucide-react";
6 | import { Button } from "@/components/ui/button";
7 | import { BarLoader } from "react-spinners";
8 | import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd";
9 | import useFetch from "@/hooks/use-fetch";
10 |
11 | import statuses from "@/data/status";
12 | import { getIssuesForSprint, updateIssueOrder } from "@/actions/issues";
13 |
14 | import SprintManager from "./sprint-manager";
15 | import IssueCreationDrawer from "./create-issue";
16 | import IssueCard from "@/components/issue-card";
17 | import BoardFilters from "./board-filters";
18 |
19 | function reorder(list, startIndex, endIndex) {
20 | const result = Array.from(list);
21 | const [removed] = result.splice(startIndex, 1);
22 | result.splice(endIndex, 0, removed);
23 |
24 | return result;
25 | }
26 |
27 | export default function SprintBoard({ sprints, projectId, orgId }) {
28 | const [currentSprint, setCurrentSprint] = useState(
29 | sprints.find((spr) => spr.status === "ACTIVE") || sprints[0]
30 | );
31 |
32 | const [isDrawerOpen, setIsDrawerOpen] = useState(false);
33 | const [selectedStatus, setSelectedStatus] = useState(null);
34 |
35 | const {
36 | loading: issuesLoading,
37 | error: issuesError,
38 | fn: fetchIssues,
39 | data: issues,
40 | setData: setIssues,
41 | } = useFetch(getIssuesForSprint);
42 |
43 | const [filteredIssues, setFilteredIssues] = useState(issues);
44 |
45 | const handleFilterChange = (newFilteredIssues) => {
46 | setFilteredIssues(newFilteredIssues);
47 | };
48 |
49 | useEffect(() => {
50 | if (currentSprint.id) {
51 | fetchIssues(currentSprint.id);
52 | }
53 | // eslint-disable-next-line react-hooks/exhaustive-deps
54 | }, [currentSprint.id]);
55 |
56 | const handleAddIssue = (status) => {
57 | setSelectedStatus(status);
58 | setIsDrawerOpen(true);
59 | };
60 |
61 | const handleIssueCreated = () => {
62 | fetchIssues(currentSprint.id);
63 | };
64 |
65 | const {
66 | fn: updateIssueOrderFn,
67 | loading: updateIssuesLoading,
68 | error: updateIssuesError,
69 | } = useFetch(updateIssueOrder);
70 |
71 | const onDragEnd = async (result) => {
72 | if (currentSprint.status === "PLANNED") {
73 | toast.warning("Start the sprint to update board");
74 | return;
75 | }
76 | if (currentSprint.status === "COMPLETED") {
77 | toast.warning("Cannot update board after sprint end");
78 | return;
79 | }
80 | const { destination, source } = result;
81 |
82 | if (!destination) {
83 | return;
84 | }
85 |
86 | if (
87 | destination.droppableId === source.droppableId &&
88 | destination.index === source.index
89 | ) {
90 | return;
91 | }
92 |
93 | const newOrderedData = [...issues];
94 |
95 | // source and destination list
96 | const sourceList = newOrderedData.filter(
97 | (list) => list.status === source.droppableId
98 | );
99 |
100 | const destinationList = newOrderedData.filter(
101 | (list) => list.status === destination.droppableId
102 | );
103 |
104 | if (source.droppableId === destination.droppableId) {
105 | const reorderedCards = reorder(
106 | sourceList,
107 | source.index,
108 | destination.index
109 | );
110 |
111 | reorderedCards.forEach((card, i) => {
112 | card.order = i;
113 | });
114 | } else {
115 | // remove card from the source list
116 | const [movedCard] = sourceList.splice(source.index, 1);
117 |
118 | // assign the new list id to the moved card
119 | movedCard.status = destination.droppableId;
120 |
121 | // add new card to the destination list
122 | destinationList.splice(destination.index, 0, movedCard);
123 |
124 | sourceList.forEach((card, i) => {
125 | card.order = i;
126 | });
127 |
128 | // update the order for each card in destination list
129 | destinationList.forEach((card, i) => {
130 | card.order = i;
131 | });
132 | }
133 |
134 | const sortedIssues = newOrderedData.sort((a, b) => a.order - b.order);
135 | setIssues(newOrderedData, sortedIssues);
136 |
137 | updateIssueOrderFn(sortedIssues);
138 | };
139 |
140 | if (issuesError) return Error loading issues
;
141 |
142 | return (
143 |
144 |
150 |
151 | {issues && !issuesLoading && (
152 |
153 | )}
154 |
155 | {updateIssuesError && (
156 |
{updateIssuesError.message}
157 | )}
158 | {(updateIssuesLoading || issuesLoading) && (
159 |
160 | )}
161 |
162 |
163 |
164 | {statuses.map((column) => (
165 |
166 | {(provided) => (
167 |
172 |
173 | {column.name}
174 |
175 | {filteredIssues
176 | ?.filter((issue) => issue.status === column.key)
177 | .map((issue, index) => (
178 |
184 | {(provided) => (
185 |
190 | fetchIssues(currentSprint.id)}
193 | onUpdate={(updated) =>
194 | setIssues((issues) =>
195 | issues.map((issue) => {
196 | if (issue.id === updated.id) return updated;
197 | return issue;
198 | })
199 | )
200 | }
201 | />
202 |
203 | )}
204 |
205 | ))}
206 | {provided.placeholder}
207 | {column.key === "TODO" &&
208 | currentSprint.status !== "COMPLETED" && (
209 |
handleAddIssue(column.key)}
213 | >
214 |
215 | Create Issue
216 |
217 | )}
218 |
219 | )}
220 |
221 | ))}
222 |
223 |
224 |
225 |
setIsDrawerOpen(false)}
228 | sprintId={currentSprint.id}
229 | status={selectedStatus}
230 | projectId={projectId}
231 | onIssueCreated={handleIssueCreated}
232 | orgId={orgId}
233 | />
234 |
235 | );
236 | }
--------------------------------------------------------------------------------
/app/(main)/project/_components/sprint-manager.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import {
6 | Select,
7 | SelectContent,
8 | SelectItem,
9 | SelectTrigger,
10 | SelectValue,
11 | } from "@/components/ui/select";
12 | import { Badge } from "@/components/ui/badge";
13 |
14 | import { BarLoader } from "react-spinners";
15 | import { formatDistanceToNow, isAfter, isBefore, format } from "date-fns";
16 |
17 | import useFetch from "@/hooks/use-fetch";
18 | import { useRouter, useSearchParams } from "next/navigation";
19 |
20 | import { updateSprintStatus } from "@/actions/sprints";
21 |
22 | export default function SprintManager({
23 | sprint,
24 | setSprint,
25 | sprints,
26 | projectId,
27 | }) {
28 | const [status, setStatus] = useState(sprint.status);
29 | const router = useRouter();
30 | const searchParams = useSearchParams();
31 |
32 | const {
33 | fn: updateStatus,
34 | loading,
35 | error,
36 | data: updatedStatus,
37 | } = useFetch(updateSprintStatus);
38 |
39 | const startDate = new Date(sprint.startDate);
40 | const endDate = new Date(sprint.endDate);
41 | const now = new Date();
42 |
43 | const canStart =
44 | isBefore(now, endDate) && isAfter(now, startDate) && status === "PLANNED";
45 |
46 | const canEnd = status === "ACTIVE";
47 |
48 | const handleStatusChange = async (newStatus) => {
49 | updateStatus(sprint.id, newStatus);
50 | };
51 |
52 | useEffect(() => {
53 | if (updatedStatus && updatedStatus.success) {
54 | setStatus(updatedStatus.sprint.status);
55 | setSprint({
56 | ...sprint,
57 | status: updatedStatus.sprint.status,
58 | });
59 | }
60 | }, [updatedStatus, loading]);
61 |
62 | const getStatusText = () => {
63 | if (status === "COMPLETED") {
64 | return `Sprint Ended`;
65 | }
66 | if (status === "ACTIVE" && isAfter(now, endDate)) {
67 | return `Overdue by ${formatDistanceToNow(endDate)}`;
68 | }
69 | if (status === "PLANNED" && isBefore(now, startDate)) {
70 | return `Starts in ${formatDistanceToNow(startDate)}`;
71 | }
72 | return null;
73 | };
74 |
75 | useEffect(() => {
76 | const sprintId = searchParams.get("sprint");
77 | if (sprintId && sprintId !== sprint.id) {
78 | const selectedSprint = sprints.find((s) => s.id === sprintId);
79 | if (selectedSprint) {
80 | setSprint(selectedSprint);
81 | setStatus(selectedSprint.status);
82 | }
83 | }
84 | }, [searchParams, sprints]);
85 |
86 | const handleSprintChange = (value) => {
87 | const selectedSprint = sprints.find((s) => s.id === value);
88 | setSprint(selectedSprint);
89 | setStatus(selectedSprint.status);
90 | router.replace(`/project/${projectId}`, undefined, { shallow: true });
91 | };
92 |
93 | return (
94 | <>
95 |
96 |
97 |
98 |
99 |
100 |
101 | {sprints.map((sprint) => (
102 |
103 | {sprint.name} ({format(sprint.startDate, "MMM d, yyyy")} to{" "}
104 | {format(sprint.endDate, "MMM d, yyyy")})
105 |
106 | ))}
107 |
108 |
109 |
110 | {canStart && (
111 | handleStatusChange("ACTIVE")}
113 | disabled={loading}
114 | className="bg-green-900 text-white"
115 | >
116 | Start Sprint
117 |
118 | )}
119 | {canEnd && (
120 | handleStatusChange("COMPLETED")}
122 | disabled={loading}
123 | variant="destructive"
124 | >
125 | End Sprint
126 |
127 | )}
128 |
129 | {loading && }
130 | {getStatusText() && (
131 |
132 | {getStatusText()}
133 |
134 | )}
135 | >
136 | );
137 | }
--------------------------------------------------------------------------------
/app/(main)/project/create/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useForm } from "react-hook-form";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { useRouter } from "next/navigation";
7 | import { useOrganization, useUser } from "@clerk/nextjs";
8 | import { Button } from "@/components/ui/button";
9 | import { Input } from "@/components/ui/input";
10 | import { Textarea } from "@/components/ui/textarea";
11 | import useFetch from "@/hooks/use-fetch";
12 | import { projectSchema } from "@/app/lib/validators";
13 | import { createProject } from "@/actions/projects";
14 | import { BarLoader } from "react-spinners";
15 | import OrgSwitcher from "@/components/org-switcher";
16 |
17 | export default function CreateProjectPage() {
18 | const router = useRouter();
19 | const { isLoaded: isOrgLoaded, membership } = useOrganization();
20 | const { isLoaded: isUserLoaded } = useUser();
21 | const [isAdmin, setIsAdmin] = useState(false);
22 |
23 | const {
24 | register,
25 | handleSubmit,
26 | formState: { errors },
27 | } = useForm({
28 | resolver: zodResolver(projectSchema),
29 | });
30 |
31 | useEffect(() => {
32 | if (isOrgLoaded && isUserLoaded && membership) {
33 | setIsAdmin(membership.role === "org:admin");
34 | }
35 | }, [isOrgLoaded, isUserLoaded, membership]);
36 |
37 | const {
38 | loading,
39 | error,
40 | data: project,
41 | fn: createProjectFn,
42 | } = useFetch(createProject);
43 |
44 | const onSubmit = async (data) => {
45 | if (!isAdmin) {
46 | alert("Only organization admins can create projects");
47 | return;
48 | }
49 |
50 | createProjectFn(data);
51 | };
52 |
53 | useEffect(() => {
54 | if (project) router.push(`/project/${project.id}`);
55 | }, [loading]);
56 |
57 | if (!isOrgLoaded || !isUserLoaded) {
58 | return null;
59 | }
60 |
61 | if (!isAdmin) {
62 | return (
63 |
64 |
65 | Oops! Only Admins can create projects.
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | return (
73 |
74 |
75 | Create New Project
76 |
77 |
78 |
130 |
131 | );
132 | }
--------------------------------------------------------------------------------
/app/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atharva-aak/TaskVault/a93be7841242795609676a24b477ba2fd21290b8/app/android-chrome-512x512.png
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atharva-aak/TaskVault/a93be7841242795609676a24b477ba2fd21290b8/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer utilities {
10 | .text-balance {
11 | text-wrap: balance;
12 | }
13 | .gradient-title{
14 | @apply font-extrabold bg-gradient-to-br from-blue-500 via-blue-100 to-blue-400 bg-clip-text
15 | tracking-tighter text-transparent pr-2 pb-2;
16 |
17 | }
18 | }
19 |
20 | @layer base {
21 | :root {
22 | --background: 0 0% 100%;
23 | --foreground: 0 0% 3.9%;
24 | --card: 0 0% 100%;
25 | --card-foreground: 0 0% 3.9%;
26 | --popover: 0 0% 100%;
27 | --popover-foreground: 0 0% 3.9%;
28 | --primary: 0 0% 9%;
29 | --primary-foreground: 0 0% 98%;
30 | --secondary: 0 0% 96.1%;
31 | --secondary-foreground: 0 0% 9%;
32 | --muted: 0 0% 96.1%;
33 | --muted-foreground: 0 0% 45.1%;
34 | --accent: 0 0% 96.1%;
35 | --accent-foreground: 0 0% 9%;
36 | --destructive: 0 84.2% 60.2%;
37 | --destructive-foreground: 0 0% 98%;
38 | --border: 0 0% 89.8%;
39 | --input: 0 0% 89.8%;
40 | --ring: 0 0% 3.9%;
41 | --chart-1: 12 76% 61%;
42 | --chart-2: 173 58% 39%;
43 | --chart-3: 197 37% 24%;
44 | --chart-4: 43 74% 66%;
45 | --chart-5: 27 87% 67%;
46 | --radius: 0.5rem;
47 | }
48 | .dark {
49 | --background: 0 0% 3.9%;
50 | --foreground: 0 0% 98%;
51 | --card: 0 0% 3.9%;
52 | --card-foreground: 0 0% 98%;
53 | --popover: 0 0% 3.9%;
54 | --popover-foreground: 0 0% 98%;
55 | --primary: 0 0% 98%;
56 | --primary-foreground: 0 0% 9%;
57 | --secondary: 0 0% 14.9%;
58 | --secondary-foreground: 0 0% 98%;
59 | --muted: 0 0% 14.9%;
60 | --muted-foreground: 0 0% 63.9%;
61 | --accent: 0 0% 14.9%;
62 | --accent-foreground: 0 0% 98%;
63 | --destructive: 0 62.8% 30.6%;
64 | --destructive-foreground: 0 0% 98%;
65 | --border: 0 0% 14.9%;
66 | --input: 0 0% 14.9%;
67 | --ring: 0 0% 83.1%;
68 | --chart-1: 220 70% 50%;
69 | --chart-2: 160 60% 45%;
70 | --chart-3: 30 80% 55%;
71 | --chart-4: 280 65% 60%;
72 | --chart-5: 340 75% 55%;
73 | }
74 | }
75 |
76 | @layer base {
77 | * {
78 | @apply border-border;
79 | }
80 | body {
81 | @apply bg-background text-foreground;
82 | }
83 | }
84 | .dotted-background{
85 | background-color: #0a111f;
86 | background-image: radial-gradient(#4a5568 1px,transparent 1px);
87 | background-size:30px 30px ;
88 | }
89 |
90 | .wmde-markdown ul{
91 | list-style-type: disc;
92 | padding-left: 15px !important;
93 | color: white;
94 | }
--------------------------------------------------------------------------------
/app/layout.js:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "next-themes";
2 | import "./globals.css";
3 | import { Inter } from "next/font/google"
4 | import Header from "@/components/header";
5 | import { ClerkProvider } from "@clerk/nextjs";
6 | import { shadesOfPurple } from "@clerk/themes";
7 | import { Toaster } from "sonner";
8 |
9 | const inter =Inter({subsets : ["latin"]});
10 |
11 | export const metadata = {
12 | title: "TaskVault",
13 | description: "Project Managment App",
14 | };
15 |
16 | export default function RootLayout({ children }) {
17 | return (
18 |
35 |
36 |
39 |
40 |
41 | {children}
42 |
43 |
44 |
45 |
Made by team TaskVault
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
--------------------------------------------------------------------------------
/app/lib/validators.js:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const projectSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, "Project name is required")
7 | .max(100, "Project name must be 100 characters or less"),
8 | key: z
9 | .string()
10 | .min(2, "Project key must be at least 2 characters")
11 | .max(10, "Project key must be 10 characters or less")
12 | .toUpperCase(),
13 | description: z
14 | .string()
15 | .max(500, "Description must be 500 characters or less")
16 | .optional(),
17 | });
18 |
19 | export const sprintSchema = z.object({
20 | name: z.string().min(1, "Sprint name is required"),
21 | startDate: z.date(),
22 | endDate: z.date(),
23 | });
24 |
25 | export const issueSchema = z.object({
26 | title: z.string().min(1, "Title is required"),
27 | assigneeId: z.string().cuid("Please select assignee"),
28 | description: z.string().optional(),
29 | priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]),
30 | });
--------------------------------------------------------------------------------
/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | export default function NotFound() {
2 | return (
3 |
4 |
404 - Page Not Found
5 |
6 | );
7 | }
--------------------------------------------------------------------------------
/app/page.js:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import Image from "next/image";
3 | import { ArrowRight, BarChart, Calendar, ChevronRight, Layout } from "lucide-react";
4 |
5 | import faqs from '@/data/faqs'
6 | import Link from "next/link";
7 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
8 | import { Card, CardContent } from "@/components/ui/card";
9 |
10 | // this is a temp text
11 |
12 | const features = [
13 | {
14 | title: "Intuitive Kanban Boards",
15 | description:
16 | "Visualze your workflow and optimize team productivity with our easy-to-use kanban boards.",
17 | icon: Layout,
18 | },
19 | {
20 | title: "Powerful Sprint Planning",
21 | description:
22 | "Plan and manage sprints effectively,ensuring your team stays focused on delivering value.",
23 | icon: Calendar,
24 | },
25 | {
26 | title: "Comprehensive Reporting",
27 | description:
28 | "Gaininsight into your team's performance with detailed,customizable reports and analytics.",
29 | icon: BarChart,
30 | },
31 | ];
32 |
33 | export default function Home() {
34 | return
35 | {/* Hero Section*/}
36 |
37 |
38 | Streamline Your Workflow
39 |
40 | with{""}
41 |
48 |
49 |
50 |
51 | Taskvault is a sleek, minimalist website with an intuitive, user-friendly interface focused on seamless
52 | navigation and engaging content. It features a responsive design that adapts perfectly across devices,
53 | offering a smooth and interactive experience. The site prioritizes speed, security, and personalized content for every user.
54 |
55 |
56 |
57 |
58 | Get Started
59 |
60 |
61 |
62 |
63 |
64 | Learn More
65 |
66 |
67 |
68 |
69 |
70 |
Key Features
71 |
{features.map((feature, index) => {
72 | return (
73 |
74 |
75 |
76 |
77 | {feature.title}
78 | {feature.description}
79 |
80 |
81 |
82 | );
83 | })
84 | }
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Frequently Asked Question
93 |
94 |
95 | {faqs.map((faq, index) => (
96 |
97 | {faq.question}
98 |
99 | {faq.answer}
100 |
101 |
102 | ))}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | Ready to Transform Your Workflow
113 |
114 |
Join our team for new and better experiment.
115 |
116 |
Start For free
117 |
118 |
119 |
120 |
121 |
;
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": false,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/components/header.jsx:
--------------------------------------------------------------------------------
1 | import { SignedIn, SignedOut, SignInButton} from "@clerk/nextjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 | import { Button } from "./ui/button";
5 | import { PenBox } from "lucide-react";
6 | import UserMenu from "./user-menu";
7 | import { checkUser } from "@/lib/checkUser";
8 | import UserLoading from "./user-loading";
9 | const Header =async () => {
10 | await checkUser();
11 | return (
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Create Project
24 |
25 |
26 |
27 |
28 |
29 |
30 | Login
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default Header;
46 |
--------------------------------------------------------------------------------
/components/issue-card.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { Badge } from "@/components/ui/badge";
5 | import {
6 | Card,
7 | CardContent,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import { formatDistanceToNow } from "date-fns";
13 | import IssueDetailsDialog from "./issue-details-dialog";
14 | import UserAvatar from "./user-avatar";
15 | import { useRouter } from "next/navigation";
16 |
17 | const priorityColor = {
18 | LOW: "border-green-600",
19 | MEDIUM: "border-yellow-300",
20 | HIGH: "border-orange-400",
21 | URGENT: "border-red-400",
22 | };
23 |
24 | export default function IssueCard({
25 | issue,
26 | showStatus = false,
27 | onDelete = () => {},
28 | onUpdate = () => {},
29 | }) {
30 | const [isDialogOpen, setIsDialogOpen] = useState(false);
31 | const router = useRouter();
32 |
33 | const onDeleteHandler = (...params) => {
34 | router.refresh();
35 | onDelete(...params);
36 | };
37 |
38 | const onUpdateHandler = (...params) => {
39 | router.refresh();
40 | onUpdate(...params);
41 | };
42 |
43 | const created = formatDistanceToNow(new Date(issue.createdAt), {
44 | addSuffix: true,
45 | });
46 |
47 | return (
48 | <>
49 | setIsDialogOpen(true)}
52 | >
53 |
56 | {issue.title}
57 |
58 |
59 |
60 | {showStatus && {issue.status} }
61 |
62 | {issue.priority}
63 |
64 |
65 |
66 |
67 |
68 | Created {created}
69 |
70 |
71 |
72 | {isDialogOpen && (
73 | setIsDialogOpen(false)}
76 | issue={issue}
77 | onDelete={onDeleteHandler}
78 | onUpdate={onUpdateHandler}
79 | borderCol={priorityColor[issue.priority]}
80 | />
81 | )}
82 | >
83 | );
84 | }
--------------------------------------------------------------------------------
/components/issue-details-dialog.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "@/components/ui/dialog";
10 | import { Button } from "@/components/ui/button";
11 | import MDEditor from "@uiw/react-md-editor";
12 | import UserAvatar from "./user-avatar";
13 | import useFetch from "@/hooks/use-fetch";
14 | import { useOrganization, useUser } from "@clerk/nextjs";
15 | import {
16 | Select,
17 | SelectContent,
18 | SelectItem,
19 | SelectTrigger,
20 | SelectValue,
21 | } from "@/components/ui/select";
22 | import { BarLoader } from "react-spinners";
23 | import { ExternalLink } from "lucide-react";
24 | import { usePathname, useRouter } from "next/navigation";
25 |
26 | import statuses from "@/data/status";
27 | import { deleteIssue, updateIssue } from "@/actions/issues";
28 |
29 | const priorityOptions = ["LOW", "MEDIUM", "HIGH", "URGENT"];
30 |
31 | export default function IssueDetailsDialog({
32 | isOpen,
33 | onClose,
34 | issue,
35 | onDelete = () => {},
36 | onUpdate = () => {},
37 | borderCol = "",
38 | }) {
39 | const [status, setStatus] = useState(issue.status);
40 | const [priority, setPriority] = useState(issue.priority);
41 | const { user } = useUser();
42 | const { membership } = useOrganization();
43 | const router = useRouter();
44 | const pathname = usePathname();
45 |
46 | const {
47 | loading: deleteLoading,
48 | error: deleteError,
49 | fn: deleteIssueFn,
50 | data: deleted,
51 | } = useFetch(deleteIssue);
52 |
53 | const {
54 | loading: updateLoading,
55 | error: updateError,
56 | fn: updateIssueFn,
57 | data: updated,
58 | } = useFetch(updateIssue);
59 |
60 | const handleDelete = async () => {
61 | if (window.confirm("Are you sure you want to delete this issue?")) {
62 | deleteIssueFn(issue.id);
63 | }
64 | };
65 |
66 | const handleStatusChange = async (newStatus) => {
67 | setStatus(newStatus);
68 | updateIssueFn(issue.id, { status: newStatus, priority });
69 | };
70 |
71 | const handlePriorityChange = async (newPriority) => {
72 | setPriority(newPriority);
73 | updateIssueFn(issue.id, { status, priority: newPriority });
74 | };
75 |
76 | useEffect(() => {
77 | if (deleted) {
78 | onClose();
79 | onDelete();
80 | }
81 | if (updated) {
82 | onUpdate(updated);
83 | }
84 | }, [deleted, updated, deleteLoading, updateLoading]);
85 |
86 | const canChange =
87 | user.id === issue.reporter.clerkUserId || membership.role === "org:admin";
88 |
89 | const handleGoToProject = () => {
90 | router.push(`/project/${issue.projectId}?sprint=${issue.sprintId}`);
91 | };
92 |
93 | const isProjectPage = !pathname.startsWith("/project/");
94 |
95 | return (
96 |
97 |
98 |
99 |
100 | {issue.title}
101 | {isProjectPage && (
102 |
108 |
109 |
110 | )}
111 |
112 |
113 | {(updateLoading || deleteLoading) && (
114 |
115 | )}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | {statuses.map((option) => (
124 |
125 | {option.name}
126 |
127 | ))}
128 |
129 |
130 |
135 |
136 |
137 |
138 |
139 | {priorityOptions.map((option) => (
140 |
141 | {option}
142 |
143 | ))}
144 |
145 |
146 |
147 |
148 |
Description
149 |
153 |
154 |
155 |
156 |
Assignee
157 |
158 |
159 |
160 |
Reporter
161 |
162 |
163 |
164 | {canChange && (
165 |
170 | {deleteLoading ? "Deleting..." : "Delete Issue"}
171 |
172 | )}
173 | {(deleteError || updateError) && (
174 |
175 | {deleteError?.message || updateError?.message}
176 |
177 | )}
178 |
179 |
180 |
181 | );
182 | }
--------------------------------------------------------------------------------
/components/org-switcher.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname } from "next/navigation";
4 | import {
5 | OrganizationSwitcher,
6 | SignedIn,
7 | useOrganization,
8 | useUser,
9 | } from "@clerk/nextjs";
10 |
11 | const OrgSwitcher = () => {
12 | const { isLoaded } = useOrganization();
13 | const { isLoaded: isUserLoaded } = useUser();
14 | const pathname = usePathname();
15 |
16 | if (pathname === "/") {
17 | return null;
18 | }
19 |
20 | if (!isLoaded || !isUserLoaded) {
21 | return null;
22 | }
23 |
24 | return (
25 |
26 |
27 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default OrgSwitcher;
--------------------------------------------------------------------------------
/components/theme_provider.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }) {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/components/ui/accordion.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { cn } from "@/lib/utils"
6 | import { ChevronDownIcon } from "@radix-ui/react-icons"
7 |
8 | const Accordion = AccordionPrimitive.Root
9 |
10 | const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
11 |
12 | ))
13 | AccordionItem.displayName = "AccordionItem"
14 |
15 | const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
16 |
17 | svg]:rotate-180",
21 | className
22 | )}
23 | {...props}>
24 | {children}
25 |
27 |
28 |
29 | ))
30 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
31 |
32 | const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
33 |
37 | {children}
38 |
39 | ))
40 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
41 |
42 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
43 |
--------------------------------------------------------------------------------
/components/ui/avatar.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef(({ className, ...props }, ref) => (
9 |
13 | ))
14 | Avatar.displayName = AvatarPrimitive.Root.displayName
15 |
16 | const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
17 |
21 | ))
22 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
23 |
24 | const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
25 |
32 | ))
33 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
34 |
35 | export { Avatar, AvatarImage, AvatarFallback }
36 |
--------------------------------------------------------------------------------
/components/ui/badge.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | function Badge({
27 | className,
28 | variant,
29 | ...props
30 | }) {
31 | return (
);
32 | }
33 |
34 | export { Badge, badgeVariants }
35 |
--------------------------------------------------------------------------------
/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
38 | const Comp = asChild ? Slot : "button"
39 | return (
40 |
44 | );
45 | })
46 | Button.displayName = "Button"
47 |
48 | export { Button, buttonVariants }
49 |
--------------------------------------------------------------------------------
/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef(({ className, ...props }, ref) => (
6 |
10 | ))
11 | Card.displayName = "Card"
12 |
13 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
14 |
18 | ))
19 | CardHeader.displayName = "CardHeader"
20 |
21 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
22 |
26 | ))
27 | CardTitle.displayName = "CardTitle"
28 |
29 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
30 |
34 | ))
35 | CardDescription.displayName = "CardDescription"
36 |
37 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
38 |
39 | ))
40 | CardContent.displayName = "CardContent"
41 |
42 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
43 |
47 | ))
48 | CardFooter.displayName = "CardFooter"
49 |
50 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
51 |
--------------------------------------------------------------------------------
/components/ui/carousel.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from "react"
3 | import useEmblaCarousel from "embla-carousel-react";
4 | import { cn } from "@/lib/utils"
5 | import { Button } from "@/components/ui/button"
6 | import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
7 |
8 | const CarouselContext = React.createContext(null)
9 |
10 | function useCarousel() {
11 | const context = React.useContext(CarouselContext)
12 |
13 | if (!context) {
14 | throw new Error("useCarousel must be used within a ")
15 | }
16 |
17 | return context
18 | }
19 |
20 | const Carousel = React.forwardRef((
21 | {
22 | orientation = "horizontal",
23 | opts,
24 | setApi,
25 | plugins,
26 | className,
27 | children,
28 | ...props
29 | },
30 | ref
31 | ) => {
32 | const [carouselRef, api] = useEmblaCarousel({
33 | ...opts,
34 | axis: orientation === "horizontal" ? "x" : "y",
35 | }, plugins)
36 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
37 | const [canScrollNext, setCanScrollNext] = React.useState(false)
38 |
39 | const onSelect = React.useCallback((api) => {
40 | if (!api) {
41 | return
42 | }
43 |
44 | setCanScrollPrev(api.canScrollPrev())
45 | setCanScrollNext(api.canScrollNext())
46 | }, [])
47 |
48 | const scrollPrev = React.useCallback(() => {
49 | api?.scrollPrev()
50 | }, [api])
51 |
52 | const scrollNext = React.useCallback(() => {
53 | api?.scrollNext()
54 | }, [api])
55 |
56 | const handleKeyDown = React.useCallback((event) => {
57 | if (event.key === "ArrowLeft") {
58 | event.preventDefault()
59 | scrollPrev()
60 | } else if (event.key === "ArrowRight") {
61 | event.preventDefault()
62 | scrollNext()
63 | }
64 | }, [scrollPrev, scrollNext])
65 |
66 | React.useEffect(() => {
67 | if (!api || !setApi) {
68 | return
69 | }
70 |
71 | setApi(api)
72 | }, [api, setApi])
73 |
74 | React.useEffect(() => {
75 | if (!api) {
76 | return
77 | }
78 |
79 | onSelect(api)
80 | api.on("reInit", onSelect)
81 | api.on("select", onSelect)
82 |
83 | return () => {
84 | api?.off("select", onSelect)
85 | };
86 | }, [api, onSelect])
87 |
88 | return (
89 |
101 |
108 | {children}
109 |
110 |
111 | );
112 | })
113 | Carousel.displayName = "Carousel"
114 |
115 | const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
116 | const { carouselRef, orientation } = useCarousel()
117 |
118 | return (
119 |
129 | );
130 | })
131 | CarouselContent.displayName = "CarouselContent"
132 |
133 | const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
134 | const { orientation } = useCarousel()
135 |
136 | return (
137 |
147 | );
148 | })
149 | CarouselItem.displayName = "CarouselItem"
150 |
151 | const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
152 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
153 |
154 | return (
155 |
165 |
166 | Previous slide
167 |
168 | );
169 | })
170 | CarouselPrevious.displayName = "CarouselPrevious"
171 |
172 | const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
173 | const { orientation, scrollNext, canScrollNext } = useCarousel()
174 |
175 | return (
176 |
186 |
187 | Next slide
188 |
189 | );
190 | })
191 | CarouselNext.displayName = "CarouselNext"
192 |
193 | export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
194 |
--------------------------------------------------------------------------------
/components/ui/dialog.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { cn } from "@/lib/utils"
6 | import { Cross2Icon } from "@radix-ui/react-icons"
7 |
8 | const Dialog = DialogPrimitive.Root
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger
11 |
12 | const DialogPortal = DialogPrimitive.Portal
13 |
14 | const DialogClose = DialogPrimitive.Close
15 |
16 | const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
17 |
24 | ))
25 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
26 |
27 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
28 |
29 |
30 |
37 | {children}
38 |
40 |
41 | Close
42 |
43 |
44 |
45 | ))
46 | DialogContent.displayName = DialogPrimitive.Content.displayName
47 |
48 | const DialogHeader = ({
49 | className,
50 | ...props
51 | }) => (
52 |
55 | )
56 | DialogHeader.displayName = "DialogHeader"
57 |
58 | const DialogFooter = ({
59 | className,
60 | ...props
61 | }) => (
62 |
65 | )
66 | DialogFooter.displayName = "DialogFooter"
67 |
68 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
69 |
73 | ))
74 | DialogTitle.displayName = DialogPrimitive.Title.displayName
75 |
76 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
77 |
81 | ))
82 | DialogDescription.displayName = DialogPrimitive.Description.displayName
83 |
84 | export {
85 | Dialog,
86 | DialogPortal,
87 | DialogOverlay,
88 | DialogTrigger,
89 | DialogClose,
90 | DialogContent,
91 | DialogHeader,
92 | DialogFooter,
93 | DialogTitle,
94 | DialogDescription,
95 | }
96 |
--------------------------------------------------------------------------------
/components/ui/drawer.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }) => (
12 |
13 | )
14 | Drawer.displayName = "Drawer"
15 |
16 | const DrawerTrigger = DrawerPrimitive.Trigger
17 |
18 | const DrawerPortal = DrawerPrimitive.Portal
19 |
20 | const DrawerClose = DrawerPrimitive.Close
21 |
22 | const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
23 |
27 | ))
28 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
29 |
30 | const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
31 |
32 |
33 |
40 |
41 | {children}
42 |
43 |
44 | ))
45 | DrawerContent.displayName = "DrawerContent"
46 |
47 | const DrawerHeader = ({
48 | className,
49 | ...props
50 | }) => (
51 |
54 | )
55 | DrawerHeader.displayName = "DrawerHeader"
56 |
57 | const DrawerFooter = ({
58 | className,
59 | ...props
60 | }) => (
61 |
62 | )
63 | DrawerFooter.displayName = "DrawerFooter"
64 |
65 | const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
66 |
70 | ))
71 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
72 |
73 | const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
74 |
78 | ))
79 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
80 |
81 | export {
82 | Drawer,
83 | DrawerPortal,
84 | DrawerOverlay,
85 | DrawerTrigger,
86 | DrawerClose,
87 | DrawerContent,
88 | DrawerHeader,
89 | DrawerFooter,
90 | DrawerTitle,
91 | DrawerDescription,
92 | }
93 |
--------------------------------------------------------------------------------
/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
6 | return (
7 |
15 | );
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/components/ui/popover.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
15 |
16 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
30 |
--------------------------------------------------------------------------------
/components/ui/select.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { cn } from "@/lib/utils"
6 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
7 |
8 | const Select = SelectPrimitive.Root
9 |
10 | const SelectGroup = SelectPrimitive.Group
11 |
12 | const SelectValue = SelectPrimitive.Value
13 |
14 | const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
15 | span]:line-clamp-1",
19 | className
20 | )}
21 | {...props}>
22 | {children}
23 |
24 |
25 |
26 |
27 | ))
28 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
29 |
30 | const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
31 |
35 |
36 |
37 | ))
38 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
39 |
40 | const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
41 |
45 |
46 |
47 | ))
48 | SelectScrollDownButton.displayName =
49 | SelectPrimitive.ScrollDownButton.displayName
50 |
51 | const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
52 |
53 |
63 |
64 |
67 | {children}
68 |
69 |
70 |
71 |
72 | ))
73 | SelectContent.displayName = SelectPrimitive.Content.displayName
74 |
75 | const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
76 |
80 | ))
81 | SelectLabel.displayName = SelectPrimitive.Label.displayName
82 |
83 | const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
84 |
91 |
92 |
93 |
94 |
95 |
96 | {children}
97 |
98 | ))
99 | SelectItem.displayName = SelectPrimitive.Item.displayName
100 |
101 | const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
102 |
106 | ))
107 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
108 |
109 | export {
110 | Select,
111 | SelectGroup,
112 | SelectValue,
113 | SelectTrigger,
114 | SelectContent,
115 | SelectLabel,
116 | SelectItem,
117 | SelectSeparator,
118 | SelectScrollUpButton,
119 | SelectScrollDownButton,
120 | }
121 |
--------------------------------------------------------------------------------
/components/ui/sonner.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTheme } from "next-themes"
3 | import { Toaster as Sonner } from "sonner"
4 |
5 | const Toaster = ({
6 | ...props
7 | }) => {
8 | const { theme = "system" } = useTheme()
9 |
10 | return (
11 |
26 | );
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/components/ui/tabs.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef(({ className, ...props }, ref) => (
11 |
18 | ))
19 | TabsList.displayName = TabsPrimitive.List.displayName
20 |
21 | const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
22 |
29 | ))
30 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
31 |
32 | const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
33 |
40 | ))
41 | TabsContent.displayName = TabsPrimitive.Content.displayName
42 |
43 | export { Tabs, TabsList, TabsTrigger, TabsContent }
44 |
--------------------------------------------------------------------------------
/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 |
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/components/user-avatar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
3 |
4 | const UserAvatar = ({ user }) => {
5 | return (
6 |
7 |
8 |
9 |
10 | {user ? user.name : "?"}
11 |
12 |
13 |
14 | {user ? user.name : "Unassigned"}
15 |
16 |
17 | );
18 | };
19 |
20 | export default UserAvatar;
--------------------------------------------------------------------------------
/components/user-loading.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useOrganization, useUser } from "@clerk/nextjs";
4 | import React from "react";
5 | import { BarLoader } from "react-spinners";
6 |
7 | const UserLoading = () => {
8 | const { isLoaded } = useOrganization();
9 | const { isLoaded: isUserLoaded } = useUser();
10 |
11 | if (!isLoaded || !isUserLoaded) {
12 | return ;
13 | } else <>>;
14 | };
15 |
16 | export default UserLoading;
--------------------------------------------------------------------------------
/components/user-menu.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { UserButton } from "@clerk/nextjs";
4 | import { ChartNoAxesGantt } from "lucide-react";
5 | import React from "react";
6 |
7 | const UserMenu = () => {
8 | return
13 |
14 | }
17 | href="/onboarding"
18 | />
19 |
20 |
21 | ;
22 | };
23 |
24 | export default UserMenu;
--------------------------------------------------------------------------------
/data/faqs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "question": "What is Taskvault?",
4 | "answer":
5 | "Taskvault is a powerful project management tool designed to help teams organize, track, and manage their work efficiently. It combines intuitive design with robust features to streamline your workflow and boost productivity."
6 | },
7 | {
8 | "question": "How does Taskvault compare to other project management tools?",
9 | "answer":
10 | "Taskvault offers a unique combination of intuitive design, powerful features, and flexibility. Unlike other tools, we focus on providing a seamless experience for both agile and traditional project management methodologies, making it versatile for various team structures and project types."
11 | },
12 | {
13 | "question": "Is Taskvault suitable for small teams?",
14 | "answer":
15 | "Absolutely! Taskvault is designed to be scalable and flexible. It works great for small teams and can easily grow with your organization as it expands. Our user-friendly interface ensures that teams of any size can quickly adapt and start benefiting from ZCRUM's features."
16 | },
17 | {
18 | "question": "What key features does Taskvault offer?",
19 | "answer":
20 | "Taskvault provides a range of powerful features including intuitive Kanban boards for visualizing workflow, robust sprint planning tools for agile teams, comprehensive reporting for data-driven decisions, customizable workflows, time tracking, and team collaboration tools. These features work seamlessly together to enhance your project management experience."
21 | },
22 | {
23 | "question": "Can Taskvault handle multiple projects simultaneously?",
24 | "answer":
25 | "Yes, Taskvault is built to manage multiple projects concurrently. You can easily switch between projects, and get a bird's-eye view of all your ongoing work. This makes ZCRUM ideal for organizations juggling multiple projects or clients."
26 | },
27 | {
28 | "question": "Is there a learning curve for new users?",
29 | "answer":
30 | "While Taskvault is packed with features, we've designed it with user-friendliness in mind. New users can quickly get up to speed thanks to our intuitive interface, helpful onboarding process, and comprehensive documentation."
31 | }
32 | ]
--------------------------------------------------------------------------------
/data/status.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Todo",
4 | "key": "TODO"
5 | },
6 | {
7 | "name": "In Progress",
8 | "key": "IN_PROGRESS"
9 | },
10 | {
11 | "name": "In Review",
12 | "key": "IN_REVIEW"
13 | },
14 | {
15 | "name": "Done",
16 | "key": "DONE"
17 | }
18 | ]
--------------------------------------------------------------------------------
/hooks/use-fetch.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { toast } from "sonner";
3 |
4 | const useFetch = (cb) => {
5 | const [data, setData] = useState(undefined);
6 | const [loading, setLoading] = useState(null);
7 | const [error, setError] = useState(null);
8 |
9 | const fn = async (...args) => {
10 | setLoading(true);
11 | setError(null);
12 |
13 | try {
14 | const response = await cb(...args);
15 | setData(response);
16 | setError(null);
17 | } catch (error) {
18 | setError(error);
19 | toast.error(error.message);
20 | } finally {
21 | setLoading(false);
22 | }
23 | };
24 |
25 | return { data, loading, error, fn, setData };
26 | };
27 |
28 | export default useFetch;
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/lib/checkUser.js:
--------------------------------------------------------------------------------
1 | import { currentUser } from "@clerk/nextjs/server";
2 | import { db } from "@/lib/prisma";
3 |
4 | export const checkUser = async () => {
5 | const user = await currentUser();
6 |
7 | if (!user) {
8 | return null;
9 | }
10 |
11 | try {
12 | const loggedInUser = await db?.user.findUnique({
13 | where: {
14 | clerkUserId: user.id,
15 | },
16 | });
17 |
18 | if (loggedInUser) {
19 | return loggedInUser;
20 | }
21 |
22 | const name = `${user.firstName} ${user.lastName}`;
23 |
24 | const newUser = await db.user.create({
25 | data: {
26 | clerkUserId: user.id,
27 | name,
28 | imageUrl: user.imageUrl,
29 | email: user.emailAddresses[0].emailAddress,
30 | },
31 | });
32 |
33 | return newUser;
34 | } catch (error) {
35 | console.log(error);
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/lib/prisma.js:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = globalThis.prisma || new PrismaClient();
4 |
5 | if (process.env.NODE_ENV !== "production") {
6 | globalThis.prisma = db;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.js:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 | import { NextResponse } from "next/server";
3 |
4 | const isProtectedRoute = createRouteMatcher([
5 | "/onboarding(.*)",
6 | "/organisation(.*)",
7 | "/project(.*)",
8 | "/issue(.*)",
9 | ])
10 |
11 | export default clerkMiddleware((auth,req)=>{
12 | if(!auth().userId && isProtectedRoute(req)){
13 | return auth().redirectToSignIn();
14 | }
15 |
16 | if (
17 | auth().userId &&
18 | !auth().orgId &&
19 | req.nextUrl.pathname !== "/onboarding" &&
20 | req.nextUrl.pathname !== "/"
21 | ) {
22 | return NextResponse.redirect(new URL("/onboarding", req.url));
23 | }
24 | });
25 |
26 | export const config = {
27 | matcher: [
28 | // Skip Next.js internals and all static files, unless found in search params
29 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
30 | // Always run for API routes
31 | '/(api|trpc)(.*)',
32 | ],
33 | };
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taskvault",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^5.7.5",
14 | "@clerk/themes": "^2.2.23",
15 | "@hello-pangea/dnd": "^18.0.1",
16 | "@hookform/resolvers": "^4.1.3",
17 | "@prisma/client": "^6.5.0",
18 | "@radix-ui/react-accordion": "^1.2.3",
19 | "@radix-ui/react-avatar": "^1.1.3",
20 | "@radix-ui/react-dialog": "^1.1.6",
21 | "@radix-ui/react-icons": "^1.3.2",
22 | "@radix-ui/react-popover": "^1.1.6",
23 | "@radix-ui/react-select": "^2.1.6",
24 | "@radix-ui/react-slot": "^1.1.2",
25 | "@radix-ui/react-tabs": "^1.1.3",
26 | "@uiw/react-md-editor": "^4.0.5",
27 | "class-variance-authority": "^0.7.1",
28 | "clsx": "^2.1.1",
29 | "date-fns": "^4.1.0",
30 | "embla-carousel-react": "^8.5.2",
31 | "lucide-react": "^0.483.0",
32 | "next": "^14.2.26",
33 | "next-themes": "^0.4.6",
34 | "react": "^18",
35 | "react-day-picker": "^9.6.4",
36 | "react-dom": "^18",
37 | "react-hook-form": "^7.55.0",
38 | "react-spinners": "^0.15.0",
39 | "recharts": "^2.15.3",
40 | "sonner": "^2.0.2",
41 | "tailwind-merge": "^3.0.2",
42 | "tailwindcss-animate": "^1.0.7",
43 | "vaul": "^1.1.2",
44 | "zod": "^3.24.2"
45 | },
46 | "devDependencies": {
47 | "dotenv-cli": "^8.0.0",
48 | "eslint": "^8",
49 | "eslint-config-next": "14.2.16",
50 | "postcss": "^8",
51 | "prisma": "^6.5.0",
52 | "tailwindcss": "^3.4.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20250329074727_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Task" (
3 | "id" SERIAL NOT NULL,
4 | "title" TEXT NOT NULL,
5 | "description" TEXT,
6 | "completed" BOOLEAN NOT NULL DEFAULT false,
7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8 |
9 | CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
10 | );
11 |
--------------------------------------------------------------------------------
/prisma/migrations/20250329075249_init/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `Task` table. If the table is not empty, all the data it contains will be lost.
5 |
6 | */
7 | -- CreateEnum
8 | CREATE TYPE "SprintStatus" AS ENUM ('PLANNED', 'ACTIVE', 'COMPLETED');
9 |
10 | -- CreateEnum
11 | CREATE TYPE "IssueStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE');
12 |
13 | -- CreateEnum
14 | CREATE TYPE "IssuePriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
15 |
16 | -- DropTable
17 | DROP TABLE "Task";
18 |
19 | -- CreateTable
20 | CREATE TABLE "User" (
21 | "id" TEXT NOT NULL,
22 | "clerkUserId" TEXT NOT NULL,
23 | "email" TEXT NOT NULL,
24 | "name" TEXT,
25 | "imageUrl" TEXT,
26 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
27 | "updatedAt" TIMESTAMP(3) NOT NULL,
28 |
29 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
30 | );
31 |
32 | -- CreateTable
33 | CREATE TABLE "Project" (
34 | "id" TEXT NOT NULL,
35 | "name" TEXT NOT NULL,
36 | "key" TEXT NOT NULL,
37 | "description" TEXT,
38 | "organizationId" TEXT NOT NULL,
39 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
40 | "updatedAt" TIMESTAMP(3) NOT NULL,
41 |
42 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
43 | );
44 |
45 | -- CreateTable
46 | CREATE TABLE "Sprint" (
47 | "id" TEXT NOT NULL,
48 | "name" TEXT NOT NULL,
49 | "startDate" TIMESTAMP(3) NOT NULL,
50 | "endDate" TIMESTAMP(3) NOT NULL,
51 | "status" "SprintStatus" NOT NULL DEFAULT 'PLANNED',
52 | "projectId" TEXT NOT NULL,
53 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
54 | "updatedAt" TIMESTAMP(3) NOT NULL,
55 |
56 | CONSTRAINT "Sprint_pkey" PRIMARY KEY ("id")
57 | );
58 |
59 | -- CreateTable
60 | CREATE TABLE "Issue" (
61 | "id" TEXT NOT NULL,
62 | "title" TEXT NOT NULL,
63 | "description" TEXT,
64 | "status" "IssueStatus" NOT NULL,
65 | "order" INTEGER NOT NULL,
66 | "priority" "IssuePriority" NOT NULL,
67 | "assigneeId" TEXT,
68 | "reporterId" TEXT NOT NULL,
69 | "projectId" TEXT NOT NULL,
70 | "sprintId" TEXT,
71 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
72 | "updatedAt" TIMESTAMP(3) NOT NULL,
73 |
74 | CONSTRAINT "Issue_pkey" PRIMARY KEY ("id")
75 | );
76 |
77 | -- CreateIndex
78 | CREATE UNIQUE INDEX "User_clerkUserId_key" ON "User"("clerkUserId");
79 |
80 | -- CreateIndex
81 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
82 |
83 | -- CreateIndex
84 | CREATE UNIQUE INDEX "Project_organizationId_key_key" ON "Project"("organizationId", "key");
85 |
86 | -- CreateIndex
87 | CREATE UNIQUE INDEX "Sprint_name_key" ON "Sprint"("name");
88 |
89 | -- CreateIndex
90 | CREATE INDEX "Issue_status_order_idx" ON "Issue"("status", "order");
91 |
92 | -- AddForeignKey
93 | ALTER TABLE "Sprint" ADD CONSTRAINT "Sprint_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
94 |
95 | -- AddForeignKey
96 | ALTER TABLE "Issue" ADD CONSTRAINT "Issue_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
97 |
98 | -- AddForeignKey
99 | ALTER TABLE "Issue" ADD CONSTRAINT "Issue_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
100 |
101 | -- AddForeignKey
102 | ALTER TABLE "Issue" ADD CONSTRAINT "Issue_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
103 |
104 | -- AddForeignKey
105 | ALTER TABLE "Issue" ADD CONSTRAINT "Issue_sprintId_fkey" FOREIGN KEY ("sprintId") REFERENCES "Sprint"("id") ON DELETE SET NULL ON UPDATE CASCADE;
106 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url ="postgresql://coder:npg_N8F7nGbAreyO@ep-sweet-bread-a8ohqrh5-pooler.eastus2.azure.neon.tech/taskvault?sslmode=require"
8 | }
9 |
10 |
11 | model User {
12 | id String @id @default(cuid())
13 | clerkUserId String @unique
14 | email String @unique
15 | name String?
16 | imageUrl String?
17 | createdIssues Issue[] @relation("Reporter")
18 | assignedIssues Issue[] @relation("Assignee")
19 | createdAt DateTime @default(now())
20 | updatedAt DateTime @updatedAt
21 | }
22 |
23 | model Project {
24 | id String @id @default(cuid())
25 | name String
26 | key String
27 | description String?
28 | organizationId String // This will store the Clerk organization ID
29 | sprints Sprint[]
30 | issues Issue[]
31 | createdAt DateTime @default(now())
32 | updatedAt DateTime @updatedAt
33 |
34 | @@unique([organizationId, key])
35 | }
36 |
37 | model Sprint {
38 | id String @id @default(cuid())
39 | name String @unique
40 | startDate DateTime
41 | endDate DateTime
42 | status SprintStatus @default(PLANNED)
43 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
44 | projectId String
45 | issues Issue[]
46 | createdAt DateTime @default(now())
47 | updatedAt DateTime @updatedAt
48 | }
49 |
50 | model Issue {
51 | id String @id @default(cuid())
52 | title String
53 | description String?
54 | status IssueStatus
55 | order Int // This will store the order within its status column
56 | priority IssuePriority
57 | assignee User? @relation("Assignee", fields: [assigneeId], references: [id])
58 | assigneeId String?
59 | reporter User @relation("Reporter", fields: [reporterId], references: [id])
60 | reporterId String
61 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
62 | projectId String
63 | sprint Sprint? @relation(fields: [sprintId], references: [id], onDelete: SetNull)
64 | sprintId String?
65 | createdAt DateTime @default(now())
66 | updatedAt DateTime @updatedAt
67 |
68 | @@index([status, order])
69 | }
70 |
71 | enum SprintStatus {
72 | PLANNED
73 | ACTIVE
74 | COMPLETED
75 | }
76 |
77 | enum IssueStatus {
78 | TODO
79 | IN_PROGRESS
80 | IN_REVIEW
81 | DONE
82 | }
83 |
84 | enum IssuePriority {
85 | LOW
86 | MEDIUM
87 | HIGH
88 | URGENT
89 | }
--------------------------------------------------------------------------------
/public/scrumLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atharva-aak/TaskVault/a93be7841242795609676a24b477ba2fd21290b8/public/scrumLogo.png
--------------------------------------------------------------------------------
/public/taskVaultLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atharva-aak/TaskVault/a93be7841242795609676a24b477ba2fd21290b8/public/taskVaultLogo.png
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: 'hsl(var(--background))',
13 | foreground: 'hsl(var(--foreground))',
14 | card: {
15 | DEFAULT: 'hsl(var(--card))',
16 | foreground: 'hsl(var(--card-foreground))'
17 | },
18 | popover: {
19 | DEFAULT: 'hsl(var(--popover))',
20 | foreground: 'hsl(var(--popover-foreground))'
21 | },
22 | primary: {
23 | DEFAULT: 'hsl(var(--primary))',
24 | foreground: 'hsl(var(--primary-foreground))'
25 | },
26 | secondary: {
27 | DEFAULT: 'hsl(var(--secondary))',
28 | foreground: 'hsl(var(--secondary-foreground))'
29 | },
30 | muted: {
31 | DEFAULT: 'hsl(var(--muted))',
32 | foreground: 'hsl(var(--muted-foreground))'
33 | },
34 | accent: {
35 | DEFAULT: 'hsl(var(--accent))',
36 | foreground: 'hsl(var(--accent-foreground))'
37 | },
38 | destructive: {
39 | DEFAULT: 'hsl(var(--destructive))',
40 | foreground: 'hsl(var(--destructive-foreground))'
41 | },
42 | border: 'hsl(var(--border))',
43 | input: 'hsl(var(--input))',
44 | ring: 'hsl(var(--ring))',
45 | chart: {
46 | '1': 'hsl(var(--chart-1))',
47 | '2': 'hsl(var(--chart-2))',
48 | '3': 'hsl(var(--chart-3))',
49 | '4': 'hsl(var(--chart-4))',
50 | '5': 'hsl(var(--chart-5))'
51 | }
52 | },
53 | borderRadius: {
54 | lg: 'var(--radius)',
55 | md: 'calc(var(--radius) - 2px)',
56 | sm: 'calc(var(--radius) - 4px)'
57 | },
58 | keyframes: {
59 | 'accordion-down': {
60 | from: {
61 | height: '0'
62 | },
63 | to: {
64 | height: 'var(--radix-accordion-content-height)'
65 | }
66 | },
67 | 'accordion-up': {
68 | from: {
69 | height: 'var(--radix-accordion-content-height)'
70 | },
71 | to: {
72 | height: '0'
73 | }
74 | }
75 | },
76 | animation: {
77 | 'accordion-down': 'accordion-down 0.2s ease-out',
78 | 'accordion-up': 'accordion-up 0.2s ease-out'
79 | }
80 | }
81 | },
82 | plugins: [require("tailwindcss-animate")],
83 | };
84 |
--------------------------------------------------------------------------------