Skip to main content

Command Palette

Search for a command to run...

πŸš€ Building a Full-Stack Project Management System T3 Stack

Published
β€’35 min read

β€˜Learn how to build a scalable project management system β€” from wireframing and authentication to dashboards and CRUD APIs β€” using modern web technologies like Next.js, NextAuth, and Lucid React.


🧩 Step 0: Create a Wireframe and Preplan the Database Model

Before diving into code, begin by designing your wireframe. This helps visualize your app’s structure β€” how pages connect, what sections appear on each screen, and where users perform key actions like creating or managing projects.

After wireframing, plan your database model. Identify the entities and their relationships early to avoid future refactoring.
At minimum, you’ll need tables or models for:

  • Users

  • Projects

  • Tasks

  • UserProject (to manage relationships like admin, user, etc.)


πŸ—ƒοΈ Step 1: Create the Database Schema

Define the database structure that supports your app’s functionality. Each model should represent one part of your system β€” projects, tasks, users, and their associations.

model Project {
  id          String  @id @default(cuid())
  projectName String
  image       String  @default("https://img.freepik.com/free-photo/project-management-planning-development-message-box-notification-graphic_53876-123902.jpg?semt=ais_hybrid&w=740")
  description String?

  task        Task[]
  userProject UserProject[]
}

enum Status {
  Todo
  In_Progress
  Done
}

model Task {
  id           String @id @default(cuid())

  deadline     DateTime?
  createdAt    DateTime  @default(now())
  updatedAt    DateTime  @updatedAt

  title        String
  description  String?

  status       Status @default(Todo)

  assignedTo   String?
  projectId    String

  project      Project @relation(fields: [projectId], references: [id])
  user         User?   @relation(fields: [assignedTo], references: [id])
}

enum Role {
  Admin
  User
}

model UserProject {
  id        String  @id @default(cuid())
  ProjectId String
  userId    String
  role      Role    @default(User)
  project   Project @relation(fields: [ProjectId], references: [id])
  user      User    @relation(fields: [userId], references: [id])
}

// Necessary for Next auth
model Account {
  id                       String  @id @default(cuid())
  userId                   String
  type                     String
  provider                 String
  providerAccountId        String
  refresh_token            String? // @db.Text
  access_token             String? // @db.Text
  expires_at               Int?
  token_type               String?
  scope                    String?
  id_token                 String? // @db.Text
  session_state            String?
  user                     User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  refresh_token_expires_in Int?

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]

  Task          Task[]
  userProject   UserProject[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

πŸ” Step 2: Set Up Authentication with NextAuth (Google & GitHub)

Add secure, user-friendly authentication using NextAuth.js. This enables login via Google and GitHub to simplify onboarding.

Steps:

  1. Add Google and GitHub Client IDs and secrets to your environment variables.
# Next Auth Google Provider
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
# Next Auth Github Provider
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""
  1. Update env.js to load them correctly.
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    AUTH_SECRET:
      process.env.NODE_ENV === "production"
        ? z.string()
        : z.string().optional(),
    AUTH_GOOGLE_ID: z.string(),
    AUTH_GOOGLE_SECRET: z.string(),
    AUTH_GITHUB_ID: z.string(),
    AUTH_GITHUB_SECRET: z.string(),
    DATABASE_URL: z.string().url(),
    NODE_ENV: z
      .enum(["development", "test", "production"])
      .default("development"),
  },

  client: {
    // NEXT_PUBLIC_CLIENTVAR: z.string(),
  },

  runtimeEnv: {
    AUTH_SECRET: process.env.AUTH_SECRET,
    AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
    AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
    AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
    AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
    DATABASE_URL: process.env.DATABASE_URL,
    NODE_ENV: process.env.NODE_ENV,
  },

  skipValidation: !!process.env.SKIP_ENV_VALIDATION,

  emptyStringAsUndefined: true,
});
  1. Add both providers in your NextAuth configuration.

  2. Set allowDangerousEmailAccountLinking to true to automatically link accounts with the same email.

  providers: [
    Google({
      allowDangerousEmailAccountLinking: true,
    }),
    GitHub({
      allowDangerousEmailAccountLinking: true,
    }),
  ]

⚠️ Important: As noted in the Auth.js security documentation, automatic account linking between arbitrary providers can be insecure. Enable this only if you understand the implications.


🧠 Step 3: Create Backend Routes

The backend handles your project’s logic and data flow. Create routes for every major feature your app needs.

Routes to implement:

  • Project Management

    • Create project

    • Edit project

    • Delete project

    • Get all projects of a user

    • Add member

  //routers/project.ts
  getAllProject: protectedProcedure.query(async ({ ctx }) => {
    const userProjects = await ctx.db.userProject.findMany({
      where: { userId: ctx.session.user.id },
    });
    const projects = await Promise.all(
      userProjects.map(async (up) => {
        return await ctx.db.project.findMany({ where: { id: up.ProjectId } });
      }),
    );
    return projects.flat();
  }),
  createProject: protectedProcedure
    .input(
      z.object({
        projectName: z.string(),
        image: z.string().optional(),
        description: z.string().optional(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      const userID = ctx.session.user.id;
      const project = await ctx.db.project.create({
        data: {
          projectName: input.projectName,
          image: input.image,
          description: input.description,
        },
      });
      return await ctx.db.userProject.create({
        data: {
          userId: userID,
          role: "Admin",
          ProjectId: project.id,
        },
      });
    }),
  addMembers: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
        userEmail: z.string(),
        role: z.string(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });
      const userAdd = await ctx.db.user.findFirst({
        where: {
          email: input.userEmail,
        },
      });
      if (!userAdd)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist",
        });

      await ctx.db.userProject.create({
        data: {
          userId: userAdd?.id,
          role: input.role as Role,
          ProjectId: input.projectId,
        },
      });
      return { success: true };
    }),

  editProject: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
        projectName: z.string().optional(),
        image: z.string().optional(),
        description: z.string().optional(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });
      await ctx.db.project.update({
        where: {
          id: input.projectId,
        },
        data: {
          projectName: input.projectName,
          image: input.image,
          description: input.description,
        },
      });
      return { success: true };
    }),
  deleteProject: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });
      await ctx.db.userProject.deleteMany({
        where: { ProjectId: input.projectId },
      });

      await ctx.db.project.delete({
        where: { id: input.projectId },
      });

      return { success: true };
    }),
  • Task Management

    • Create task

    • Edit task

    • Delete task

    • Get all tasks

//routers/task.ts
  getAllTask: protectedProcedure.query(async ({ ctx }) => {
    const userProjects = await ctx.db.userProject.findMany({
      where: {
        userId: ctx.session.user.id,
      },
    });
    const task = await Promise.all(
      userProjects.map(async (up) => {
        const tasks = await ctx.db.task.findMany({
          where: {
            projectId: up.ProjectId,
          },
        });
        return tasks;
      }),
    );
    return task.flat();
  }),

  createTask: protectedProcedure
    .input(
      z.object({
        deadline: z.date().optional(),
        title: z.string(),
        description: z.string().optional(),
        status: z.string().optional(),
        assignedTo: z.string().optional(),
        projectId: z.string(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      type Status = "Todo" | "In_Progress" | "Done" | undefined;

      return await ctx.db.task.create({
        data: {
          deadline: input.deadline,
          title: input.title,
          description: input.description,
          status: input.status as Status,
          projectId: input.projectId,
          assignedTo: input.assignedTo,
        },
      });
    }),
  editTask: protectedProcedure
    .input(
      z.object({
        taskId: z.string(),
        deadline: z.date().optional(),
        title: z.string().optional(),
        description: z.string().optional(),
        status: z.string().optional(),
        assignedTo: z.string().optional(),
        projectId: z.string().optional(),
      }),
    )
    .mutation(async ({ input, ctx }) => {
      type Status = "Todo" | "In_Progress" | "Done" | undefined;

      return await ctx.db.task.update({
        where: {
          id: input.taskId,
        },
        data: {
          deadline: input.deadline,
          title: input.title,
          description: input.description,
          status: input.status as Status,
          projectId: input.projectId,
          assignedTo: input.assignedTo,
        },
      });
    }),
  deleteTask: protectedProcedure
    .input(z.object({ taskId: z.string() }))
    .mutation(async ({ input, ctx }) => {
      await ctx.db.task.delete({ where: { id: input.taskId } });
      return { success: true };
    }),
  • User Management

    • Promote / demote member

    • Kick user from project

    • Get all userProject relations

    • Get all users by ID

  //routers/user.ts
  getUserByIds: protectedProcedure
    .input(z.array(z.string()))
    .query(async ({ ctx, input }) => {
      const users = await ctx.db.user.findMany({
        where: { id: { in: input } },
      });
      return users;
    }),
  getUserProjectRelation: protectedProcedure.query(async ({ ctx }) => {
    const userProjects = await ctx.db.userProject.findMany({
      where: { userId: ctx.session.user.id },
    });
    const projects = await Promise.all(
      userProjects.map(async (up) => {
        return await ctx.db.userProject.findMany({
          where: { ProjectId: up.ProjectId },
        });
      }),
    );

    return projects.flat();
  }),
  promoteUser: protectedProcedure
    .input(z.object({ projectId: z.string(), userEmail: z.string() }))
    .mutation(async ({ input, ctx }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });

      const userAdd = await ctx.db.user.findFirst({
        where: {
          email: input.userEmail,
        },
      });
      if (!userAdd)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist",
        });
      const userProject = await ctx.db.userProject.findFirst({
        where: {
          ProjectId: input.projectId,
          userId: userAdd.id,
        },
      });
      if (!userProject)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist in project",
        });
      await ctx.db.userProject.update({
        where: {
          id: userProject?.id,
        },
        data: {
          userId: userAdd.id,
          role: "Admin",
          ProjectId: input.projectId,
        },
      });
      return { success: true };
    }),
  kickUser: protectedProcedure
    .input(z.object({ projectId: z.string(), userEmail: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });

      const userAdd = await ctx.db.user.findFirst({
        where: {
          email: input.userEmail,
        },
      });
      if (!userAdd)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist",
        });
      const userProject = await ctx.db.userProject.findFirst({
        where: {
          ProjectId: input.projectId,
          userId: userAdd.id,
        },
      });
      if (!userProject)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist in project",
        });
      await ctx.db.userProject.delete({
        where: {
          id: userProject?.id,
        },
      });
      return { success: true };
    }),
  demoteUser: protectedProcedure
    .input(z.object({ projectId: z.string(), userEmail: z.string() }))
    .mutation(async ({ input, ctx }) => {
      const curentUser = await ctx.db.userProject.findFirst({
        where: {
          userId: ctx.session.user.id,
          ProjectId: input.projectId,
        },
      });
      if (curentUser?.role === "User")
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user must be admin",
        });

      const userAdd = await ctx.db.user.findFirst({
        where: {
          email: input.userEmail,
        },
      });
      if (!userAdd)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist",
        });
      const userProject = await ctx.db.userProject.findFirst({
        where: {
          ProjectId: input.projectId,
          userId: userAdd.id,
        },
      });
      if (!userProject)
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "user does not exist in project",
        });
      await ctx.db.userProject.update({
        where: {
          id: userProject?.id,
        },
        data: {
          userId: userAdd.id,
          role: "User",
          ProjectId: input.projectId,
        },
      });
      return { success: true };
    }),

πŸ–ΌοΈ Step 4: Add Logo and Metadata

Your app’s branding makes it memorable. Add a random logo from Logoipsum or generate your own. Then, update your metadata:

  • App title

  • Description

  • Favicon or logo icon

//app/layout.tsx;
export const metadata: Metadata = {
  title: "FlowTrack",
  description:
    "A simple project management tool to track your tasks and projects efficiently.",
  icons: [{ rel: "icon", url: "/logo.svg" }],
};

🏠 Step 5: Create the Landing Page

Design a clean and simple landing page that introduces your app

//app/page.tsx
import { auth } from "@/server/auth";
import { HydrateClient } from "@/trpc/server";
import Image from "next/image";
import Link from "next/link";

export default async function Home() {
  const session = await auth();
  return (
    <HydrateClient>
      <div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
        <Image
          src="/logo.svg"
          alt="Logo"
          width={128}
          height={128}
          className="mb-8"
        />
        <h1 className="mt-8 mb-4 text-4xl font-bold">Welcome to FlowTrack</h1>
        <p className="mb-8 text-lg text-gray-300">
          A simple project management tool to track your tasks and projects
          efficiently.
        </p>
        <Link
          href={session ? "/dashboard" : "/api/auth/signin"}
          className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
        >
          {session ? "dashboard" : "Sign in"}
        </Link>
      </div>
    </HydrateClient>
  );
}

🧰 Step 6: Install Lucid React

Lucid React provides modern, minimalistic UI components that integrate seamlessly with Next.js. Install it via npm or yarn and use it throughout your app for icons and UI elements.

npm install lucide-react

🧭 Step 7: Create the Sidebar Component

Build a reusable Sidebar under your _components directory. This component should handle navigation between key sections β€” Dashboard, Projects, Members, and Tasks.

"use client";
import { usePathname } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { Users, FolderOpen, Plus, ListTodo, Home } from "lucide-react";
import { api } from "@/trpc/react";

export const Sidebar = () => {
  const activeMenu = usePathname();

  const projects = api.project.getAllProject.useQuery().data;
  return (
    <nav className="flex h-screen w-64 flex-col border-r border-gray-200 bg-white">
      <Link href={"/"} className="border-b border-gray-200 p-6">
        <div className="flex items-center space-x-3">
          <Image src={"/logo.svg"} alt="flow logo" width={50} height={50} />

          <span className="text-lg font-semibold text-gray-800">FlowTrack</span>
        </div>
      </Link>
      <nav className="flex-1 p-4">
        <Link href={"/dashboard"}>
          <button
            className={`flex w-full items-center space-x-3 rounded-lg px-4 py-3 ${
              activeMenu === "/dashboard"
                ? "bg-blue-50 text-blue-600"
                : "text-gray-600 hover:bg-gray-50"
            }`}
          >
            <Home size={20} />
            <span className="font-medium">Home</span>
          </button>
        </Link>
        <Link href={"/task"}>
          <button
            className={`flex w-full items-center space-x-3 rounded-lg px-4 py-3 ${
              activeMenu === "/task"
                ? "bg-blue-50 text-blue-600"
                : "text-gray-600 hover:bg-gray-50"
            }`}
          >
            <ListTodo size={20} />
            <span className="font-medium">My task</span>
          </button>
        </Link>
        <Link href={"/members"}>
          <button
            className={`flex w-full items-center space-x-3 rounded-lg px-4 py-3 ${
              activeMenu === "/members"
                ? "bg-blue-50 text-blue-600"
                : "text-gray-600 hover:bg-gray-50"
            }`}
          >
            <Users size={20} />
            <span className="font-medium">Members</span>
          </button>
        </Link>
        <div className="mt-6">
          <Link
            href={"/project/add"}
            className="mb-2 flex items-center justify-between px-4 text-gray-400 hover:text-gray-600"
          >
            <span className="text-sm font-semibold text-gray-500 uppercase">
              project
            </span>
            <Plus size={18} />
          </Link>
        </div>
        {projects?.map((p) => {
          return (
            <Link href={`/project/${p.id}`} key={p.id}>
              <button
                className={`mt-1 flex w-full items-center space-x-3 rounded-lg px-4 py-3 ${
                  activeMenu === `/project/${p.id}`
                    ? "bg-blue-50 text-blue-600"
                    : "text-gray-600 hover:bg-gray-50"
                }`}
              >
                <FolderOpen size={20} />
                <span className="truncate font-medium">{p.projectName}</span>
              </button>
            </Link>
          );
        })}
      </nav>
    </nav>
  );
};

⏳ Step 8: Create a Fullscreen Loader

Add a fullscreen loader to improve user experience while data loads or authentication runs. Place it in _components for easy reuse across pages.

import { LoaderIcon } from "lucide-react";

interface FullscreenLoaderProps {
  label?: string;
}

export const FullscreenLoader = ({ label }: FullscreenLoaderProps) => {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-2">
      <LoaderIcon className="text-muted-foreground size-6 animate-spin" />
      {label && <p className="text-muted-foreground text-sm">{label}</p>}
    </div>
  );
};

πŸ“Š Step 9: Build the Dashboard

Your dashboard provides an instant summary to the users of your application.

Steps:

  1. Create the dashboard layout.
import { Sidebar } from "../_components/sidebar";

export default function DashboardLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <div>
      <div className="flex">
        <Sidebar />
        <div className="w-full p-8">{children}</div>
      </div>
    </div>
  );
}
  1. Add status cards to display progress summaries.
import type { AppRouter } from "@/server/api/root";
import { auth } from "@/server/auth";
import type { inferRouterOutputs } from "@trpc/server";

export const StatusCard = async (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
}) => {
  const session = await auth();
  const tasks = props.tasks;
  const assignedTasks = tasks.filter(
    (task) => task.assignedTo === session?.user.id
  );
  const completedTasks = assignedTasks.filter((task) => task.status === "Done");
  const overdueTasks = assignedTasks.filter((task) => {
    if (!task.deadline) return false;
    return (Date.now() - task.deadline.getTime()) / 1000 > 0;
  });
  return (
    <div className="mb-8 grid w-full grid-cols-5 gap-4">
      <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
        <div className="mb-1 text-sm text-gray-500">Total project</div>
        <div className="text-3xl font-bold text-gray-900">
          {props.projects.length}
        </div>
      </div>

      <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
        <div className="mb-1 text-sm text-gray-500">Total task</div>
        <div className="text-3xl font-bold text-gray-900">{tasks.length}</div>
      </div>

      <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
        <div className="mb-1 text-sm text-gray-500">Assigned Task</div>
        <div className="text-3xl font-bold text-gray-900">
          {assignedTasks.length}
        </div>
      </div>

      <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
        <div className="mb-1 text-sm text-gray-500">Completed task</div>
        <div className="text-3xl font-bold text-green-600">
          {completedTasks.length}
        </div>
      </div>

      <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
        <div className="mb-1 text-sm text-gray-500">Overdue</div>
        <div className="text-3xl font-bold text-red-600">
          {overdueTasks.length}
        </div>
      </div>
    </div>
  );
};
  1. Build sections for tasks assigned and projects assigned.
//tasks assigned
import type { AppRouter } from "@/server/api/root";
import { auth } from "@/server/auth";
import type { inferRouterOutputs } from "@trpc/server";
import { Calendar, ChevronRight, FolderOpen } from "lucide-react";
import Link from "next/link";

export const TaskAssigned = async (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
}) => {
  const session = await auth();
  const tasks = props.tasks.filter((t) => t.assignedTo === session?.user.id);
  return (
    <div className="rounded-xl border border-gray-200 bg-white shadow-sm">
      <div className="border-b border-gray-200 p-6">
        <h2 className="text-lg font-semibold text-gray-900">Task Assigned</h2>
      </div>
      <div className="p-6">
        <div className="space-y-3">
          {tasks.map((task) => (
            <div
              key={task.id}
              className="cursor-pointer rounded-lg bg-gray-50 p-4 transition-colors hover:bg-gray-100"
            >
              <div className="flex items-start justify-between">
                <div className="flex-1">
                  <h3 className="mb-1 font-medium text-gray-900">
                    {task.title}
                  </h3>
                  <div className="flex items-center text-sm text-gray-500">
                    <FolderOpen size={14} className="mr-1" />
                    <span>
                      {
                        props.projects.find((p) => p.id === task.projectId)
                          ?.projectName
                      }
                    </span>
                  </div>
                </div>
                {task.deadline ? (
                  <div
                    className={`flex items-center rounded px-2 py-1 text-xs ${
                      task.deadline.getTime() < Date.now()
                        ? "bg-red-100 text-red-700"
                        : "bg-blue-100 text-blue-700"
                    }`}
                  >
                    <Calendar size={12} className="mr-1" />
                    <span>{task.deadline.toDateString()}</span>
                  </div>
                ) : (
                  <></>
                )}
              </div>
            </div>
          ))}
        </div>
        <Link
          href="/task"
          className="mt-4 flex w-full items-center justify-center py-2 text-sm font-medium text-blue-600 hover:text-blue-700"
        >
          view all
          <ChevronRight size={16} className="ml-1" />
        </Link>
      </div>
    </div>
  );
};
//projects assigned
import type { AppRouter } from "@/server/api/root";
import type { inferRouterOutputs } from "@trpc/server";

export const ProjectAssigned = (props: {
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
}) => {
  return (
    <div className="rounded-xl border border-gray-200 bg-white shadow-sm">
      <div className="border-b border-gray-200 p-6">
        <h2 className="text-lg font-semibold text-gray-900">Projects</h2>
      </div>
      <div className="p-6">
        <div className="space-y-4">
          {props.projects.map((project) => (
            <div
              key={project.id}
              className="cursor-pointer rounded-lg bg-gray-50 p-4 transition-colors hover:bg-gray-100"
            >
              <div className="mb-3 flex items-center justify-between">
                <h3 className="font-medium text-gray-900">
                  {project.projectName}
                </h3>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};
  1. Create dashboard/page.tsx for the main view.
import { api } from "@/trpc/server";
import { StatusCard } from "./statsCard";
import { TaskAssigned } from "./taskAssigned";
import { ProjectAssigned } from "./projectAssigned";
import { redirect } from "next/navigation";

export default async function dashboard() {
  type TRPCErrorShape = {
    data?: {
      code?: string;
    };
    message?: string;
  };

  try {
    const tasks = await api.task.getAllTask();
    const projects = await api.project.getAllProject();

    return (
      <div className="flex h-fit w-full flex-wrap gap-4">
        <StatusCard tasks={tasks} projects={projects} />
        <div className="grid w-full grid-cols-2 gap-6">
          <TaskAssigned tasks={tasks} projects={projects} />
          <ProjectAssigned projects={projects} />
        </div>
      </div>
    );
  } catch (err: unknown) {
    const error = err as TRPCErrorShape;

    if (
      error.data?.code === "UNAUTHORIZED" ||
      error.message?.includes("UNAUTHORIZED")
    ) {
      return redirect("/");
    }

    throw err;
  }
}
  1. Add loading.tsx for smoother transitions.
import { FullscreenLoader } from "../_components/fullScreenLoader";

export default function loading() {
  return <FullscreenLoader label="Dashboard loading..." />;
}

βœ… Step 10: Create the Task Module

The task section manages creation, filtering, and visualization of tasks.

Steps:

  1. Create the task layout having the same layout as dashboard

  2. Create the page.tsx just call jsx element Mytask.

import { Mytask } from "./myTask";

export default async function Task() {
  return <Mytask />;
}
  1. In mytask.tsx, add:

    • View modes (table, Kanban, calendar)

    • Filters

    • Components like TaskTable, KanbanBoard, and Calendar

  2. Build AddTask component for creating and editing tasks.

  3. Allow AddTask to open prefilled data when editing existing tasks.

"use client";
import type { AppRouter } from "@/server/api/root";
import { api } from "@/trpc/react";
import type { Status } from "@prisma/client";
import type { inferRouterOutputs } from "@trpc/server";
import { ArrowUpDown, Calendar1, FolderOpen } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { FullscreenLoader } from "../_components/fullScreenLoader";

export const Mytask = () => {
  const router = useRouter();
  const [cuurentView, setViewMode] = useState("table");
  const [showNewTaskModal, setShowNewTaskModal] = useState(false);
  const [showEditTaskModal, setShowEditTaskModal] = useState(false);
  const [editTaskId, setEditTaskId] = useState("");

  const [filterStatus, setFilterStatus] = useState("all");
  const [filterAssignee, setFilterAssignee] = useState("all");
  const [filterProject, setFilterProject] = useState("all");

  const { data: userProjects, error } =
    api.user.getUserProjectRelation.useQuery();
  if (error?.data?.code === "UNAUTHORIZED") router.push("/");

  const userIds = userProjects?.map((userProject) => userProject.userId) ?? [];

  const uniqueAssignees = api.user.getUserByIds.useQuery(userIds).data ?? [];
  const projects = api.project.getAllProject.useQuery().data ?? [];

  const { data: tasks, isLoading } = api.task.getAllTask.useQuery();
  const filteredTasks =
    tasks?.filter((task) => {
      if (filterAssignee !== "all" && task.assignedTo !== filterAssignee)
        return false;
      if (filterStatus !== "all" && task.status !== filterStatus) return false;
      if (filterProject !== "all" && task.projectId !== filterProject)
        return false;
      return true;
    }) ?? [];

  return (
    <>
      {isLoading && <FullscreenLoader label="Task loading..." />}
      {!isLoading && (
        <div>
          <div className="mb-6">
            <h1 className="mb-1 text-2xl font-bold text-gray-900">My Task</h1>
            <p className="text-gray-500">All of your task can be seen here</p>
          </div>

          <div className="mb-6 flex items-center justify-between">
            <div className="flex space-x-2">
              <button
                onClick={() => setViewMode("table")}
                className={`rounded-lg px-4 py-2 font-medium transition-colors ${
                  cuurentView === "table"
                    ? "bg-gray-900 text-white"
                    : "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
                }`}
              >
                Table
              </button>
              <button
                onClick={() => setViewMode("kanban")}
                className={`rounded-lg px-4 py-2 font-medium transition-colors ${
                  cuurentView === "kanban"
                    ? "bg-gray-900 text-white"
                    : "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
                }`}
              >
                kanban
              </button>
              <button
                onClick={() => setViewMode("calendar")}
                className={`rounded-lg px-4 py-2 font-medium transition-colors ${
                  cuurentView === "calendar"
                    ? "bg-gray-900 text-white"
                    : "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
                }`}
              >
                calendar
              </button>
            </div>

            <button
              onClick={() => setShowNewTaskModal(true)}
              className="rounded-lg bg-gray-900 px-4 py-2 font-medium text-white transition-colors hover:bg-gray-800"
            >
              New Task
            </button>
          </div>
        </div>
      )}
      {/* Filters */}
      <div className="mb-6 flex space-x-3">
        <select
          value={filterStatus}
          onChange={(e) => setFilterStatus(e.target.value)}
          className="rounded-lg border border-gray-300 bg-white px-4 py-2 font-medium text-gray-700 hover:border-gray-400 focus:ring-2 focus:ring-blue-500 focus:outline-none"
        >
          <option value="all">All status</option>
          <option value="Todo">Pending</option>
          <option value="In_Progress">In Progress</option>
          <option value="Done">Completed</option>
        </select>

        <select
          value={filterAssignee}
          onChange={(e) => setFilterAssignee(e.target.value)}
          className="rounded-lg border border-gray-300 bg-white px-4 py-2 font-medium text-gray-700 hover:border-gray-400 focus:ring-2 focus:ring-blue-500 focus:outline-none"
        >
          <option value="all">All Assignee</option>
          {uniqueAssignees.map((assignee) => (
            <option key={assignee.id} value={assignee.id}>
              {assignee.name}
            </option>
          ))}
        </select>

        <select
          value={filterProject}
          onChange={(e) => setFilterProject(e.target.value)}
          className="rounded-lg border border-gray-300 bg-white px-4 py-2 font-medium text-gray-700 hover:border-gray-400 focus:ring-2 focus:ring-blue-500 focus:outline-none"
        >
          <option value="all">All projects</option>
          {projects.map((project) => (
            <option key={project.id} value={project.projectName}>
              {project.projectName}
            </option>
          ))}
        </select>
      </div>
      {cuurentView === "table" ? (
        <TaskTable
          tasks={filteredTasks}
          projects={projects}
          users={uniqueAssignees}
          setShowEditTaskModal={setShowEditTaskModal}
          setEditTaskId={setEditTaskId}
        />
      ) : cuurentView === "kanban" ? (
        <Kanban
          tasks={filteredTasks}
          projects={projects}
          users={uniqueAssignees}
          setShowEditTaskModal={setShowEditTaskModal}
          setEditTaskId={setEditTaskId}
        />
      ) : (
        <Calendar
          tasks={filteredTasks}
          projects={projects}
          users={uniqueAssignees}
          setShowEditTaskModal={setShowEditTaskModal}
          setEditTaskId={setEditTaskId}
        />
      )}
      {showNewTaskModal && (
        <NewTask
          tasks={filteredTasks}
          projects={projects}
          users={uniqueAssignees}
          isEditTask={false}
          setShowNewTaskModal={setShowNewTaskModal}
          editTaskId={editTaskId}
        />
      )}
      {showEditTaskModal && (
        <NewTask
          tasks={filteredTasks}
          projects={projects}
          users={uniqueAssignees}
          isEditTask={true}
          setShowNewTaskModal={setShowEditTaskModal}
          editTaskId={editTaskId}
        />
      )}
    </>
  );
};

const NewTask = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
  users: inferRouterOutputs<AppRouter>["user"]["getUserByIds"];
  setShowNewTaskModal: Dispatch<SetStateAction<boolean>>;
  isEditTask: boolean;
  editTaskId: string;
}) => {
  const utils = api.useUtils();
  const createTask = api.task.createTask.useMutation({
    onSuccess: () => utils.task.getAllTask.invalidate(),
  });
  const editTask = api.task.editTask.useMutation({
    onSuccess: () => utils.task.getAllTask.invalidate(),
  });
  const [newTask, setNewTask] = useState({
    title: "",
    deadline: new Date().toDateString(),
    status: "Todo",
    description: "",
    projectId: props.projects[0]?.id ?? "",
    assignedTo: props.users[0]?.id ?? "",
  });
  useEffect(() => {
    if (props.isEditTask) {
      const taskToEdit = props.tasks.find((t) => t.id === props.editTaskId);
      if (taskToEdit)
        setNewTask({
          title: taskToEdit.title,
          deadline:
            taskToEdit.deadline?.toDateString() ?? new Date().toDateString(),
          status: taskToEdit.status,
          description: taskToEdit.description ?? "",
          projectId: taskToEdit.projectId,
          assignedTo: taskToEdit.assignedTo ?? "",
        });
    } else {
      setNewTask({
        title: "",
        deadline: new Date().toDateString(),
        status: "Todo",
        description: "",
        projectId: props.projects[0]?.id ?? "",
        assignedTo: props.users[0]?.id ?? "",
      });
    }
  }, [props.projects, props.users, props.isEditTask]);

  const handleCancelTask = () => {
    props.setShowNewTaskModal(false);
    setNewTask({
      title: "",
      deadline: new Date().toDateString(),
      status: "Todo",
      description: "",
      projectId: props.projects[0]?.id ?? "",
      assignedTo: props.users[0]?.id ?? "",
    });
  };
  const handleSaveTask = () => {
    const task = { ...newTask, deadline: new Date(newTask.deadline) };
    if (props.isEditTask)
      editTask.mutate({ ...task, taskId: props.editTaskId });
    else createTask.mutate(task);
    handleCancelTask();
  };
  const isMutation = createTask.isPending ?? editTask.isPending;
  return (
    <div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
      <div className="m-4 w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
        {isMutation && (
          <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-white/70 backdrop-blur-sm">
            <div className="h-10 w-10 animate-spin rounded-full border-4 border-gray-400 border-t-gray-900"></div>
          </div>
        )}
        {props.isEditTask ? (
          <h2 className="mb-6 text-2xl font-bold text-gray-900">Edit Task</h2>
        ) : (
          <h2 className="mb-6 text-2xl font-bold text-gray-900">
            Create New Task
          </h2>
        )}

        <div className="space-y-4">
          {/* Task name, Due Date, and Status row */}
          <div className="grid grid-cols-3 gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Task name
              </label>
              <input
                type="text"
                value={newTask.title}
                onChange={(e) =>
                  setNewTask({ ...newTask, title: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
                placeholder="Enter task name"
              />
            </div>

            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Due Date
              </label>
              <input
                type="date"
                value={newTask.deadline}
                onChange={(e) =>
                  setNewTask({ ...newTask, deadline: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              />
            </div>

            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                status
              </label>
              <select
                value={newTask.status}
                onChange={(e) =>
                  setNewTask({ ...newTask, status: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              >
                <option value="Todo">Pending</option>
                <option value="In_Progress">In Progress</option>
                <option value="Done">Completed</option>
              </select>
            </div>
          </div>

          {/* Description */}
          <div>
            <label className="mb-2 block text-sm font-medium text-gray-700">
              Description:
            </label>
            <textarea
              value={newTask.description}
              onChange={(e) =>
                setNewTask({ ...newTask, description: e.target.value })
              }
              className="h-32 w-full resize-none rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              placeholder="Enter task description"
            />
          </div>

          {/* Project Name and Assignee Name row */}
          <div className="grid grid-cols-2 gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Project Name
              </label>
              <select
                value={newTask.projectId}
                onChange={(e) =>
                  setNewTask({ ...newTask, projectId: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              >
                {props.projects.map((p) => (
                  <option key={p.id} value={p.id}>
                    {p.projectName}
                  </option>
                ))}
              </select>
            </div>

            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Assignee name
              </label>
              <select
                value={newTask.assignedTo}
                onChange={(e) =>
                  setNewTask({ ...newTask, assignedTo: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              >
                {props.users.map((p) => (
                  <option key={p.id} value={p.id}>
                    {p.name}
                  </option>
                ))}
              </select>
            </div>
          </div>

          {/* Buttons */}
          <div className="grid grid-cols-2 gap-4 pt-4">
            <button
              onClick={handleCancelTask}
              className="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition-colors hover:bg-gray-50"
            >
              Cancel
            </button>
            <button
              onClick={handleSaveTask}
              className="w-full rounded-lg bg-gray-900 px-4 py-3 font-medium text-white transition-colors hover:bg-gray-800"
            >
              Save
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

const TaskTable = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
  users: inferRouterOutputs<AppRouter>["user"]["getUserByIds"];
  setShowEditTaskModal: Dispatch<SetStateAction<boolean>>;
  setEditTaskId: Dispatch<SetStateAction<string>>;
}) => {
  const filteredTasks = props.tasks;
  const toggleTaskComplete = api.task.editTask.useMutation();

  return (
    <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
      <table className="w-full">
        <thead className="border-b border-gray-200 bg-gray-50">
          <tr>
            <th className="w-12 px-4 py-3 text-left">
              <input type="checkbox" className="rounded border-gray-300" />
            </th>
            <th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
              Task Name
            </th>
            <th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
              Project
            </th>
            <th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
              Assignee
            </th>
            <th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
              <div className="flex cursor-pointer items-center hover:text-gray-900">
                Due Date
                <ArrowUpDown size={14} className="ml-1" />
              </div>
            </th>
            <th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
              status
            </th>
          </tr>
        </thead>
        <tbody className="divide-y divide-gray-200">
          {filteredTasks.map((task) => (
            <tr
              key={task.id}
              onClick={() => {
                props.setEditTaskId(task.id);
                props.setShowEditTaskModal(true);
              }}
              className="transition-colors hover:bg-gray-50"
            >
              <td className="px-4 py-4">
                <input
                  type="checkbox"
                  checked={task.status === "Done"}
                  onClick={(e) => e.stopPropagation()}
                  onChange={() => {
                    task.status =
                      task.status === "Done" ? "In_Progress" : "Done";
                    toggleTaskComplete.mutate({
                      status: task.status === "Done" ? "Done" : "In_Progress",
                      taskId: task.id,
                    });
                  }}
                  className="rounded border-gray-300"
                />
              </td>
              <td className="px-4 py-4">
                <span
                  className={`text-sm ${
                    task.status === "Done"
                      ? "text-gray-400 line-through"
                      : "text-gray-900"
                  }`}
                >
                  {task.title}
                </span>
              </td>
              <td className="px-4 py-4">
                <span className="text-sm text-gray-600">
                  {
                    props.projects.find(
                      (element) => element.id === task.projectId
                    )?.projectName
                  }
                </span>
              </td>
              <td className="px-4 py-4">
                <div className="flex items-center">
                  <div className="mr-2 flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-600 text-xs font-medium text-white">
                    {props.users.find((u) => u.id === task.assignedTo)?.name}
                  </div>
                  <span className="text-sm text-gray-600">
                    {props.users.find((u) => u.id === task.assignedTo)?.name}
                  </span>
                </div>
              </td>
              <td className="px-4 py-4">
                <span
                  className={`text-sm ${
                    task.deadline &&
                    (Date.now() - task.deadline.getTime()) / 1000 > 0 &&
                    task.status !== "Done"
                      ? "font-medium text-red-600"
                      : "text-gray-600"
                  }`}
                >
                  {task.deadline?.toDateString()}
                </span>
              </td>
              <td className="px-4 py-4">
                <span
                  className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${
                    task.status === "Done"
                      ? "bg-green-100 text-green-700"
                      : task.status === "In_Progress"
                      ? "bg-blue-100 text-blue-700"
                      : "bg-gray-100 text-gray-700"
                  }`}
                >
                  {task.status}
                </span>
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      {filteredTasks.length === 0 && (
        <div className="py-12 text-center text-gray-500">
          No tasks found matching the selected filters
        </div>
      )}
    </div>
  );
};

const Kanban = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
  users: inferRouterOutputs<AppRouter>["user"]["getUserByIds"];
  setShowEditTaskModal: Dispatch<SetStateAction<boolean>>;
  setEditTaskId: Dispatch<SetStateAction<string>>;
}) => {
  const [tasks, setTasks] = useState(props.tasks);
  const kanbanColumns = {
    "To Do": tasks.filter((t) => t.status === "Todo"),
    "In progress": tasks.filter((t) => t.status === "In_Progress"),
    Done: tasks.filter((t) => t.status === "Done"),
  };
  const [draggedTask, setDraggedTask] = useState<
    inferRouterOutputs<AppRouter>["task"]["getAllTask"][0] | null
  >(null);
  const utils = api.useUtils();
  const editTask = api.task.editTask.useMutation({
    onSuccess: () => utils.task.getAllTask.invalidate(),
  });

  const handleDrop = (newStatus: string) => {
    if (draggedTask) {
      const statusMap = {
        "To Do": "Todo",
        "In progress": "In_Progress",
        Done: "Done",
      };
      const newStat = statusMap[newStatus as keyof typeof statusMap];
      const newTasks = tasks.map((task) =>
        task.id === draggedTask.id
          ? {
              ...task,
              status: newStat as Status,
            }
          : task
      );
      setTasks(() => newTasks);
      editTask.mutate({
        taskId: draggedTask.id,
        status: newStat,
      });
      setDraggedTask(null);
    }
  };
  const isOverdue = (deadline: Date | null) => {
    return deadline !== null
      ? deadline.getTime() < new Date().getTime()
      : false;
  };
  const assignedTo = (assigned: string) => {
    return props.users.find((u) => u.id === assigned)?.name;
  };
  return (
    <div className="grid grid-cols-3 gap-6">
      {Object.entries(kanbanColumns).map(([columnName, columnTasks]) => (
        <div
          key={columnName}
          onDragOver={(e) => {
            e.preventDefault();
          }}
          onDrop={() => handleDrop(columnName)}
          className="min-h-96 rounded-xl bg-gray-50 p-4"
        >
          <h3 className="mb-4 flex items-center justify-between font-semibold text-gray-900">
            <span>{columnName}</span>
            <span className="rounded bg-white px-2 py-1 text-sm font-normal text-gray-500">
              {columnTasks.length}
            </span>
          </h3>
          <div className="space-y-3">
            {columnTasks.map((task) => (
              <div
                key={task.id}
                draggable
                onDragStart={() => setDraggedTask(task)}
                className={`cursor-move rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md ${
                  draggedTask?.id === task.id ? "opacity-50" : ""
                }`}
              >
                <h4 className="mb-2 font-medium text-gray-900">{task.title}</h4>
                <div className="mb-3 text-sm text-gray-600">
                  <div className="mb-1 flex items-center">
                    <FolderOpen size={14} className="mr-1" />
                    <span>
                      {
                        props.projects.find((p) => p.id === task.projectId)
                          ?.projectName
                      }
                    </span>
                  </div>
                  <div className="flex items-center">
                    <Calendar1 size={14} className="mr-1" />
                    <span
                      className={
                        isOverdue(task.deadline) && task.status !== "Done"
                          ? "font-medium text-red-600"
                          : ""
                      }
                    >
                      {task.deadline?.toDateString()}
                    </span>
                  </div>
                </div>
                <div className="flex items-center justify-between">
                  <div className="flex items-center">
                    <div className="flex h-6 w-6 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-600 text-xs font-medium text-white">
                      {props.users.find((u) => u.id === task.assignedTo)?.name}
                    </div>
                  </div>
                  {isOverdue(task.deadline) && task.status !== "Done" && (
                    <span className="text-xs font-medium text-red-600">
                      Overdue
                    </span>
                  )}
                </div>
              </div>
            ))}
            {columnTasks.length === 0 && (
              <div className="rounded-lg border-2 border-dashed border-gray-300 py-8 text-center text-sm text-gray-400">
                Drop tasks here
              </div>
            )}
          </div>
        </div>
      ))}
    </div>
  );
};
const Calendar = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
  projects: inferRouterOutputs<AppRouter>["project"]["getAllProject"];
  users: inferRouterOutputs<AppRouter>["user"]["getUserByIds"];
  setShowEditTaskModal: Dispatch<SetStateAction<boolean>>;
  setEditTaskId: Dispatch<SetStateAction<string>>;
}) => {
  const [tasks, setTasks] = useState(props.tasks);

  const [currentMonth, setCurrentMonth] = useState(new Date());
  const monthNames = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];

  const navigateMonth = (direction: 1 | -1) => {
    setCurrentMonth(
      new Date(
        currentMonth.getFullYear(),
        currentMonth.getMonth() + direction,
        1
      )
    );
  };
  const getDaysInMonth = (date: Date) => {
    const year = date.getFullYear();
    const month = date.getMonth();
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const daysInMonth = lastDay.getDate();
    const startingDayOfWeek = firstDay.getDay();

    return { daysInMonth, startingDayOfWeek, year, month };
  };
  const getTasksForDate = (date: Date) => {
    return (
      tasks?.filter((task) => {
        if (!task.deadline) return;
        const taskDate = new Date(task.deadline);
        return (
          taskDate.getFullYear() === date.getFullYear() &&
          taskDate.getMonth() === date.getMonth() &&
          taskDate.getDate() === date.getDate()
        );
      }) ?? []
    );
  };

  useEffect(() => {
    setTasks(props.tasks);
  }, [props.tasks]);
  return (
    <div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
      <div className="mb-6 flex items-center justify-between">
        <h2 className="text-xl font-bold text-gray-900">
          {monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
        </h2>
        <div className="flex space-x-2">
          <button
            onClick={() => navigateMonth(-1)}
            className="rounded border border-gray-300 px-3 py-1 hover:bg-gray-50"
          >
            &lt;
          </button>
          <button
            onClick={() => navigateMonth(1)}
            className="rounded border border-gray-300 px-3 py-1 hover:bg-gray-50"
          >
            &gt;
          </button>
        </div>
      </div>
      <div className="grid grid-cols-7 gap-2">
        {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
          <div
            key={day}
            className="py-2 text-center text-sm font-semibold text-gray-600"
          >
            {day}
          </div>
        ))}
        {(() => {
          const { daysInMonth, startingDayOfWeek, year, month } =
            getDaysInMonth(currentMonth);
          const days = [];

          for (let i = 0; i < startingDayOfWeek; i++) {
            days.push(
              <div
                key={`empty-${i}`}
                className="h-24 rounded-lg bg-gray-50"
              ></div>
            );
          }

          for (let day = 1; day <= daysInMonth; day++) {
            const date = new Date(year, month, day);
            const tasksForDay = getTasksForDate(date);
            const isToday = new Date().toDateString() === date.toDateString();

            days.push(
              <div
                key={day}
                className={`h-24 rounded-lg border p-2 transition-colors hover:bg-gray-50 ${
                  isToday
                    ? "border-blue-300 bg-blue-50"
                    : "border-gray-200 bg-white"
                }`}
              >
                <div
                  className={`mb-1 text-sm font-medium ${
                    isToday ? "text-blue-600" : "text-gray-700"
                  }`}
                >
                  {day}
                </div>
                <div className="space-y-1">
                  {tasksForDay.slice(0, 2).map((task) => (
                    <div
                      key={task.id}
                      className="truncate rounded bg-blue-100 px-2 py-1 text-xs text-blue-700"
                      title={task.title}
                    >
                      {task.title}
                    </div>
                  ))}
                  {tasksForDay.length > 2 && (
                    <div className="text-xs text-gray-500">
                      +{tasksForDay.length - 2} more
                    </div>
                  )}
                </div>
              </div>
            );
          }
          return days;
        })()}
      </div>
    </div>
  );
};

πŸ‘₯ Step 11: Build the Members Section

Handle all member-related features β€” viewing, adding, or managing users within projects.

Steps:

  1. Create member layout having the same layout as dashboard and page.tsx which just return the jsx element Members

  2. Add a Members component to display all team members.

  3. Include AddMember component to add new member to a project

"use client";
import { api } from "@/trpc/react";
import { User } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { FullscreenLoader } from "../_components/fullScreenLoader";

export const Members = () => {
  const router = useRouter();
  const utils = api.useUtils();
  const [showAddMemberModal, setShowAddMemberModal] = useState(false);
  const [searchTerm, setSearchTerm] = useState("");
  const promoteUser = api.user.promoteUser.useMutation();
  const kickUser = api.user.kickUser.useMutation({
    onSuccess: () => utils.user.invalidate(),
  });
  const { data: userProjects, error } =
    api.user.getUserProjectRelation.useQuery();
  if (error?.data?.code) router.push("/");
  const userIds = userProjects?.map((userProject) => userProject.userId) ?? [];
  const { data, isLoading } = api.user.getUserByIds.useQuery(userIds);
  const members = data ?? [];

  const handlePromoteUser = (projectId: string | undefined, userId: string) => {
    const userEmail = members.find((member) => member.id === userId)?.email;
    if (userEmail && projectId) promoteUser.mutate({ projectId, userEmail });
  };
  const handleKickUser = (projectId: string | undefined, userId: string) => {
    const userEmail = members.find((member) => member.id === userId)?.email;
    if (userEmail && projectId) kickUser.mutate({ projectId, userEmail });
  };
  const [debouncedSearch, setDebouncedSearch] = useState("");
  useEffect(() => {
    const handler = setTimeout(() => setDebouncedSearch(searchTerm), 500);
    return () => clearTimeout(handler);
  }, [searchTerm]);

  const filteredMembers = members.filter((member) => {
    const query = debouncedSearch.toLowerCase();
    return (
      member.name?.toLowerCase().includes(query) ??
      member.email?.toLowerCase().includes(query)
    );
  });
  return (
    <div>
      {isLoading && <FullscreenLoader label="Members loading..." />}
      <div className="mb-6">
        <h1 className="mb-1 text-2xl font-bold text-gray-900">Team Members</h1>
        <p className="text-gray-500">
          Manage your team members and permissions
        </p>
      </div>

      <div className="mb-6 flex items-center space-x-3">
        <input
          type="text"
          placeholder="Search members..."
          className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <button
          onClick={() => setShowAddMemberModal(true)}
          className="rounded-lg bg-gray-900 px-4 py-2 font-medium text-white transition-colors hover:bg-gray-800"
        >
          Add People
        </button>
      </div>

      <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
        <table className="w-full">
          <thead className="border-b border-gray-200 bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700">
                User
              </th>
              <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700">
                Action
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-200">
            {filteredMembers.map((member) => {
              const role = userProjects?.find(
                (up) => member.id === up.userId
              )?.role;
              const projectId = userProjects?.find(
                (up) => member.id === up.userId
              )?.ProjectId;
              return (
                <tr key={member.id} className="hover:bg-gray-50">
                  <td className="px-6 py-4">
                    <div>
                      <div className="font-medium text-gray-900">
                        {member.name}
                      </div>
                      <div className="text-sm text-gray-500">
                        {member.email} &lt;{role}&gt;
                      </div>
                    </div>
                  </td>
                  <td className="px-6 py-4">
                    <div className="flex items-center space-x-2">
                      <button className="flex h-8 w-8 items-center justify-center rounded bg-gray-100 hover:bg-gray-200">
                        <User size={16} className="text-gray-600" />
                      </button>
                      {role !== "Admin" && (
                        <>
                          <button
                            onClick={() =>
                              handlePromoteUser(projectId, member.id)
                            }
                            className="rounded border border-gray-300 px-3 py-1 text-sm hover:bg-gray-50"
                          >
                            promot user
                          </button>
                          <button
                            onClick={() => handleKickUser(projectId, member.id)}
                            className="rounded border border-gray-300 px-3 py-1 text-sm hover:bg-gray-50"
                          >
                            kick user
                          </button>
                        </>
                      )}
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      {showAddMemberModal && (
        <AddMemberButton setShowAddMemberModal={setShowAddMemberModal} />
      )}
    </div>
  );
};

const AddMemberButton = (props: {
  setShowAddMemberModal: Dispatch<SetStateAction<boolean>>;
}) => {
  const [isError, setIsError] = useState(false);
  const [error, setError] = useState("");
  const utils = api.useUtils();
  const projects = api.project.getAllProject.useQuery().data ?? [];
  const addMember = api.project.addMembers.useMutation({
    onSuccess: async () => {
      await utils.user.getUserByIds.invalidate();
      handleCancelMember();
    },
    onError: (error) => {
      if (error.message === "user does not exist") {
        setIsError(true);
        setError(error.message);
      }
    },
  });

  const [newMember, setNewMember] = useState({
    userEmail: "",
    projectId: projects[0]?.id ?? "",
    role: "User",
  });
  const handleCancelMember = () => {
    props.setShowAddMemberModal(false);
  };
  const handleAddMember = () => {
    addMember.mutate(newMember);
  };
  return (
    <div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
      <div className="m-4 w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
        <h2 className="mb-6 text-2xl font-bold text-gray-900">
          Add New Members
          {isError && (
            <div className="mt-2 text-sm font-medium text-red-700">{error}</div>
          )}
        </h2>
        <div className="space-y-4">
          {/* Task name, Due Date, and Status row */}
          <div className="gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Email
              </label>
              <input
                type="text"
                value={newMember.userEmail}
                onChange={(e) => {
                  setIsError(false);
                  setNewMember({ ...newMember, userEmail: e.target.value });
                }}
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
                placeholder="Enter task name"
              />
            </div>
          </div>

          {/* Description */}
          <div>
            <label className="mb-2 block text-sm font-medium text-gray-700">
              Project Name
            </label>
            <select
              value={newMember.projectId}
              onChange={(e) =>
                setNewMember({ ...newMember, projectId: e.target.value })
              }
              className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
            >
              {projects.map((p) => (
                <option key={p.id} value={p.id}>
                  {p.projectName}
                </option>
              ))}
            </select>
          </div>

          {/* Project Name and Assignee Name row */}
          <div className="gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Role
              </label>
              <select
                value={"User"}
                onChange={(e) =>
                  setNewMember({ ...newMember, role: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              >
                <option value={"User"}>User</option>
                <option value={"Admin"}>Admin</option>
              </select>
            </div>
          </div>

          {/* Buttons */}
          <div className="grid grid-cols-2 gap-4 pt-4">
            <button
              onClick={handleCancelMember}
              className="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition-colors hover:bg-gray-50"
            >
              Cancel
            </button>
            <button
              onClick={handleAddMember}
              className="w-full rounded-lg bg-gray-900 px-4 py-3 font-medium text-white transition-colors hover:bg-gray-800"
            >
              Save
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

🧱 Step 12: Create the /project/add Page

Implement the ability to create new projects dynamically.

Steps:

  1. Add page.tsx which just return AddProject jsx element under /project/add.

  2. Create AddProject component for input fields and logic.

"use client";
import { api } from "@/trpc/react";
import { useRouter } from "next/navigation";
import { useState } from "react";

export const AddProject = () => {
  const router = useRouter();
  const [newProject, setNewProject] = useState({
    projectName: "",
    description: "",
  });
  const utils = api.useUtils();
  const addProject = api.project.createProject.useMutation({
    onSuccess: () => utils.project.getAllProject.invalidate(),
    onError: (error) => {
      if (error.data?.code === "UNAUTHORIZED") router.push("/");
    },
  });

  const handleCancelAddProject = () => {
    router.push("/dashboard");
  };

  const handleAddProject = () => {
    addProject.mutate(newProject);
    handleCancelAddProject();
  };
  return (
    <div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
      <div className="m-4 w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
        <h2 className="mb-6 text-2xl font-bold text-gray-900">Add Project</h2>

        <div className="space-y-4">
          {/* Task name, Due Date, and Status row */}
          <div className="gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Project Name:
              </label>
              <input
                type="text"
                value={newProject.projectName}
                onChange={(e) =>
                  setNewProject({ ...newProject, projectName: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
                placeholder="Enter task name"
              />
            </div>
          </div>

          {/* Description */}
          <div>
            <label className="mb-2 block text-sm font-medium text-gray-700">
              Description:
            </label>
            <textarea
              value={newProject.description}
              onChange={(e) =>
                setNewProject({ ...newProject, description: e.target.value })
              }
              className="h-32 w-full resize-none rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              placeholder="Enter task description"
            />
          </div>

          {/* Buttons */}
          <div className="grid grid-cols-2 gap-4 pt-4">
            <button
              onClick={handleCancelAddProject}
              className="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition-colors hover:bg-gray-50"
            >
              Cancel
            </button>
            <button
              onClick={handleAddProject}
              className="w-full rounded-lg bg-gray-900 px-4 py-3 font-medium text-white transition-colors hover:bg-gray-800"
            >
              Save
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

🧭 Step 13: Create the /project/[projectId] Dynamic Route

This route handles displaying and editing individual projects.

Steps:

  1. Create layout.tsx having the same layout as dashboard and page.tsx which just return jsx element Project.

  2. Add components:

    • Project

    • ProjectStatusCard

    • StatusOverview

    • TeamOverload

    • EditProject

"use client";

import type { AppRouter } from "@/server/api/root";
import { api } from "@/trpc/react";
import type { inferRouterOutputs } from "@trpc/server";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";

export const Project = () => {
  const [editProject, setEditProject] = useState(false);
  const projectId = usePathname().split("/")[2];
  const router = useRouter();
  const deleteProject = api.project.deleteProject.useMutation({
    onSuccess: () => router.push("/dashboard"),
  });
  const project = api.project.getAllProject
    .useQuery()
    .data?.find((p) => p.id === projectId);
  const { data, error } = api.task.getAllTask.useQuery();
  if (error?.data?.code === "UNAUTHORIZED") router.push("/");
  const task = data?.filter((t) => t.projectId === projectId) ?? [];
  return (
    <div>
      <div className="mb-6 flex items-center justify-between">
        <h2 className="border-b-2 border-transparent bg-transparent px-2 text-2xl font-bold text-gray-900 outline-none focus:border-blue-500">
          {project?.projectName.replace(/^./, (c) => c.toUpperCase())}
        </h2>
        <div className="flex space-x-2">
          <button
            onClick={() => {
              console.log(projectId);
              if (projectId) deleteProject.mutate({ projectId: projectId });
            }}
            className="rounded-lg border border-gray-300 px-4 py-2 font-medium text-gray-700 hover:bg-gray-50"
          >
            Delete
          </button>
          <button
            onClick={() => setEditProject(true)}
            className="rounded-lg bg-gray-900 px-4 py-2 font-medium text-white hover:bg-gray-800"
          >
            Edit
          </button>
        </div>
      </div>
      <ProjectStatusCard project={project} tasks={task} />
      <div className="grid grid-cols-2 gap-6">
        <StatusOverview tasks={task} />
        <TeamWorkLoad tasks={task} />
      </div>
      {editProject && <EditProject project={project} />}
    </div>
  );
};

const EditProject = (props: {
  project:
    | inferRouterOutputs<AppRouter>["project"]["getAllProject"][0]
    | undefined;
}) => {
  const [newProject, setNewProject] = useState({
    projectName: props.project?.projectName ?? "",
    description: props.project?.description ?? "",
  });
  const utils = api.useUtils();
  const addProject = api.project.editProject.useMutation({
    onSuccess: () => utils.project.getAllProject.invalidate(),
  });
  const router = useRouter();

  const handleCancelAddProject = () => {
    router.push("/dashboard");
  };

  const handleAddProject = () => {
    addProject.mutate({
      ...newProject,
      projectId: props.project ? props.project.id : "",
    });
    handleCancelAddProject();
  };
  return (
    <div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
      <div className="m-4 w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl">
        <h2 className="mb-6 text-2xl font-bold text-gray-900">Add Project</h2>

        <div className="space-y-4">
          {/* Task name, Due Date, and Status row */}
          <div className="gap-4">
            <div>
              <label className="mb-2 block text-sm font-medium text-gray-700">
                Project Name:
              </label>
              <input
                type="text"
                value={newProject.projectName}
                onChange={(e) =>
                  setNewProject({ ...newProject, projectName: e.target.value })
                }
                className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
                placeholder="Enter task name"
              />
            </div>
          </div>

          {/* Description */}
          <div>
            <label className="mb-2 block text-sm font-medium text-gray-700">
              Description:
            </label>
            <textarea
              value={newProject.description}
              onChange={(e) =>
                setNewProject({ ...newProject, description: e.target.value })
              }
              className="h-32 w-full resize-none rounded-lg border border-gray-300 px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
              placeholder="Enter task description"
            />
          </div>

          {/* Buttons */}
          <div className="grid grid-cols-2 gap-4 pt-4">
            <button
              onClick={handleCancelAddProject}
              className="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition-colors hover:bg-gray-50"
            >
              Cancel
            </button>
            <button
              onClick={handleAddProject}
              className="w-full rounded-lg bg-gray-900 px-4 py-3 font-medium text-white transition-colors hover:bg-gray-800"
            >
              Save
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

const ProjectStatusCard = (props: {
  project:
    | inferRouterOutputs<AppRouter>["project"]["getAllProject"][0]
    | undefined;
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"] | undefined;
}) => {
  const task = props.tasks ?? [];
  const now = new Date();
  const sevenDaysAgo = new Date(now);
  sevenDaysAgo.setDate(now.getDate() - 7);
  const completedTask = task.filter(
    (t) => t.status === "Done" && new Date(t.updatedAt) >= sevenDaysAgo
  );
  const created = task.filter((t) => t.createdAt >= sevenDaysAgo);
  const dueTasks = task.filter((t) =>
    t.deadline ? t.deadline >= sevenDaysAgo && t.deadline <= now : false
  );
  return (
    <div className="mb-6 grid grid-cols-3 gap-4">
      <div className="rounded-xl border border-gray-200 bg-white p-4">
        <div className="text-2xl font-bold text-gray-900">
          {completedTask.length}
        </div>
        <div className="text-xs text-gray-500">
          completed
          <br />
          in the last 7 day
        </div>
      </div>
      <div className="rounded-xl border border-gray-200 bg-white p-4">
        <div className="text-2xl font-bold text-gray-900">{created.length}</div>
        <div className="text-xs text-gray-500">
          created
          <br />
          in the last 7 day
        </div>
      </div>
      <div className="rounded-xl border border-gray-200 bg-white p-4">
        <div className="text-2xl font-bold text-gray-900">
          {dueTasks.length}
        </div>
        <div className="text-xs text-gray-500">
          due
          <br />
          in the last 7 day
        </div>
      </div>
    </div>
  );
};
const StatusOverview = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"] | undefined;
}) => {
  return (
    <div className="rounded-xl border border-gray-200 bg-white p-6">
      <h3 className="mb-2 font-semibold text-gray-900">Status overview</h3>
      <p className="mb-6 text-xs text-gray-500">
        Get a snapshot of the status of your work items. View all work items.
      </p>
      <div className="flex items-center justify-center">
        {(() => {
          const stats = {
            pending:
              props.tasks?.filter((t) => t.status === "Todo").length ?? 0,
            total: props.tasks?.length ?? 0,
            inProgress:
              props.tasks?.filter((t) => t.status === "In_Progress").length ??
              0,
            done: props.tasks?.filter((t) => t.status === "Done").length ?? 0,
          };
          const total = stats.total;
          const pendingAngle = (stats.pending / total) * 360;
          const inProgressAngle = (stats.inProgress / total) * 360;
          const doneAngle = (stats.done / total) * 360;

          return (
            <div className="flex items-center gap-8">
              <svg width="180" height="180" viewBox="0 0 180 180">
                <circle
                  cx="90"
                  cy="90"
                  r="70"
                  fill="none"
                  stroke="#e5e7eb"
                  strokeWidth="40"
                />
                {stats.pending > 0 && (
                  <circle
                    cx="90"
                    cy="90"
                    r="70"
                    fill="none"
                    stroke="#9ca3af"
                    strokeWidth="40"
                    strokeDasharray={`${(pendingAngle / 360) * 439.6} 439.6`}
                    transform="rotate(-90 90 90)"
                  />
                )}
                {stats.inProgress > 0 && (
                  <circle
                    cx="90"
                    cy="90"
                    r="70"
                    fill="none"
                    stroke="#3b82f6"
                    strokeWidth="40"
                    strokeDasharray={`${(inProgressAngle / 360) * 439.6} 439.6`}
                    strokeDashoffset={`-${(pendingAngle / 360) * 439.6}`}
                    transform="rotate(-90 90 90)"
                  />
                )}
                {stats.done > 0 && (
                  <circle
                    cx="90"
                    cy="90"
                    r="70"
                    fill="none"
                    stroke="#10b981"
                    strokeWidth="40"
                    strokeDasharray={`${(doneAngle / 360) * 439.6} 439.6`}
                    strokeDashoffset={`-${
                      ((pendingAngle + inProgressAngle) / 360) * 439.6
                    }`}
                    transform="rotate(-90 90 90)"
                  />
                )}
              </svg>
              <div className="space-y-2 text-sm">
                <div className="flex items-center gap-2">
                  <div className="h-4 w-4 rounded bg-gray-400"></div>
                  <span>To Do: {stats.pending}</span>
                </div>
                <div className="flex items-center gap-2">
                  <div className="h-4 w-4 rounded bg-blue-500"></div>
                  <span>In Progress: {stats.inProgress}</span>
                </div>
                <div className="flex items-center gap-2">
                  <div className="h-4 w-4 rounded bg-green-500"></div>
                  <span>Done: {stats.done}</span>
                </div>
              </div>
            </div>
          );
        })()}
      </div>
    </div>
  );
};
const TeamWorkLoad = (props: {
  tasks: inferRouterOutputs<AppRouter>["task"]["getAllTask"];
}) => {
  const getTeamWorkload = () => {
    const workloadMap: Record<string, number> = {};
    const assignees: string[] = [];

    props.tasks?.forEach((task) => {
      const assignee = task.assignedTo;
      if (assignee) {
        workloadMap[assignee] = (workloadMap[assignee] ?? 0) + 1;
        if (workloadMap[assignee] === 1) assignees.push(assignee);
      }
    });

    const total = props.tasks?.length ?? 1;
    const user = api.user.getUserByIds.useQuery(assignees).data ?? [];
    return Object.entries(workloadMap).map(([id, count]) => ({
      name: user.find((u) => u.id === id)?.name,
      count,
      percentage: Math.round((count / total) * 100),
    }));
  };
  return (
    <div className="rounded-xl border border-gray-200 bg-white p-6">
      <h3 className="mb-2 font-semibold text-gray-900">Team workload</h3>
      <p className="mb-6 text-xs text-gray-500">
        Monitor the capacity of your team.
      </p>
      <div className="space-y-4">
        <div className="flex items-center justify-between border-b pb-2 text-sm font-medium text-gray-700">
          <span>Assignee</span>
          <span>Work distribution</span>
        </div>
        {(() => {
          const workload = getTeamWorkload();
          return workload.length > 0 ? (
            workload.map((person, idx) => (
              <div
                key={idx}
                className="flex items-center justify-between gap-4"
              >
                <span className="w-32 text-sm text-gray-600">
                  {person.name}
                </span>
                <div className="flex flex-1 items-center gap-2">
                  <div className="h-6 flex-1 overflow-hidden rounded-full bg-gray-200">
                    <div
                      className="flex h-full items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600 text-xs font-medium text-white"
                      style={{ width: `${person.percentage}%` }}
                    >
                      {person.percentage > 15 && `${person.percentage}%`}
                    </div>
                  </div>
                  {person.percentage <= 15 && (
                    <span className="text-xs text-gray-600">
                      {person.percentage}%
                    </span>
                  )}
                </div>
              </div>
            ))
          ) : (
            <div className="py-4 text-center text-sm text-gray-400">
              No tasks assigned yet
            </div>
          );
        })()}
      </div>
    </div>
  );
};

Each project page should summarize progress, show members, and allow project updates.


🏁 Conclusion

Congratulations! πŸŽ‰ By following these steps, you now have a complete project management system featuring:

  • Secure login via Google and GitHub (NextAuth)

  • Full CRUD backend for projects, tasks, and members

  • Responsive dashboard and reusable components

  • Clean UI built with Lucid React

This system can easily be extended with features like notifications,or real-time updates using WebSockets.


πŸ—‚οΈ Project Folder Structure

.
β”œβ”€β”€ eslint.config.js
β”œβ”€β”€ next.config.js
β”œβ”€β”€ next-env.d.ts
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ postcss.config.js
β”œβ”€β”€ prettier.config.js
β”œβ”€β”€ prisma
β”‚   └── schema.prisma
β”œβ”€β”€ public
β”‚   β”œβ”€β”€ image.jpg
β”‚   └── logo.svg
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ app
β”‚   β”‚   β”œβ”€β”€ api
β”‚   β”‚   β”‚   β”œβ”€β”€ auth
β”‚   β”‚   β”‚   β”‚   └── [...nextauth]
β”‚   β”‚   β”‚   β”‚       └── route.ts
β”‚   β”‚   β”‚   └── trpc
β”‚   β”‚   β”‚       └── [trpc]
β”‚   β”‚   β”‚           └── route.ts
β”‚   β”‚   β”œβ”€β”€ _components
β”‚   β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ fullScreenLoader.tsx
β”‚   β”‚   β”‚   └── sidebar.tsx
β”‚   β”‚   β”œβ”€β”€ dashboard
β”‚   β”‚   β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ loading.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ projectAssigned.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ statsCard.tsx
β”‚   β”‚   β”‚   └── taskAssigned.tsx
β”‚   β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”‚   β”œβ”€β”€ members
β”‚   β”‚   β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ members.tsx
β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”œβ”€β”€ page.tsx
β”‚   β”‚   β”œβ”€β”€ project
β”‚   β”‚   β”‚   β”œβ”€β”€ add
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ addProject.tsx
β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ loading.tsx
β”‚   β”‚   β”‚   └── [projectId]
β”‚   β”‚   β”‚       β”œβ”€β”€ dataProject.tsx
β”‚   β”‚   β”‚       β”œβ”€β”€ layout.tsx
β”‚   β”‚   β”‚       └── page.tsx
β”‚   β”‚   └── task
β”‚   β”‚       β”œβ”€β”€ layout.tsx
β”‚   β”‚       β”œβ”€β”€ myTask.tsx
β”‚   β”‚       └── page.tsx
β”‚   β”œβ”€β”€ env.js
β”‚   β”œβ”€β”€ server
β”‚   β”‚   β”œβ”€β”€ api
β”‚   β”‚   β”‚   β”œβ”€β”€ root.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ routers
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ project.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ task.ts
β”‚   β”‚   β”‚   β”‚   └── user.ts
β”‚   β”‚   β”‚   └── trpc.ts
β”‚   β”‚   β”œβ”€β”€ auth
β”‚   β”‚   β”‚   β”œβ”€β”€ config.ts
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   └── db.ts
β”‚   β”œβ”€β”€ styles
β”‚   β”‚   └── globals.css
β”‚   └── trpc
β”‚       β”œβ”€β”€ query-client.ts
β”‚       β”œβ”€β”€ react.tsx
β”‚       └── server.ts
└── tsconfig.json

23 directories, 50 files

Written by shubham narnolia β€” full-stack developer passionate about building efficient web systems.