diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/components/ProjectSidebar.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/components/ProjectSidebar.tsx
new file mode 100644
index 0000000..7c76368
--- /dev/null
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/components/ProjectSidebar.tsx
@@ -0,0 +1,42 @@
+"use client";
+import NavigationSidebar, { ItemGroup } from "@/src/components/NavigationSidebar";
+import { useProject } from "@/src/hooks/useProject";
+import { useTeam } from "@/src/hooks/useTeam";
+import { House, Settings } from "lucide-react";
+
+export default function ProjectSidebar() {
+ const { data: team, isLoading: isTeamLoading } = useTeam();
+ const { data: project, isLoading: isProjectLoading } = useProject();
+
+ const SidebarItems: ItemGroup[] = [
+ {
+ groupTitle: "Workspace",
+ items: [
+ {
+ title: "Overview",
+ Icon: House,
+ url: "",
+ },
+ ],
+ },
+ {
+ groupTitle: "Manage",
+ items: [
+ {
+ title: "Project Settings",
+ Icon: Settings,
+ url: "/settings",
+ },
+ ],
+ },
+ ];
+
+ const dynamicItems = SidebarItems.map((group) => ({
+ ...group,
+ items: group.items.map((item) => ({
+ ...item,
+ url: `/dashboard/${team?.slug}/projects/${project?.slug}${item.url}`,
+ })),
+ }));
+ return ;
+}
diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/layout.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/layout.tsx
new file mode 100644
index 0000000..2271133
--- /dev/null
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/layout.tsx
@@ -0,0 +1,10 @@
+import ProjectSidebar from "./components/ProjectSidebar";
+
+export default function Layout({ children }: React.PropsWithChildren) {
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/page.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/page.tsx
index ceb00d9..1bcd300 100644
--- a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/page.tsx
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/page.tsx
@@ -8,8 +8,8 @@ export default function Page() {
return (
-
{project?.name}
-
{project?.slug}
+
Name: {project?.name}
+
Slug: {project?.slug}
);
}
diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectDangerZone.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectDangerZone.tsx
new file mode 100644
index 0000000..fbeaf85
--- /dev/null
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectDangerZone.tsx
@@ -0,0 +1,102 @@
+"use client";
+import CallbackDialog from "@/src/components/CallbackDialog";
+import { Button } from "@/src/components/ui/button";
+import { Card, CardHeader, CardTitle, CardContent } from "@/src/components/ui/card";
+import {
+ Item,
+ ItemActions,
+ ItemContent,
+ ItemDescription,
+ ItemMedia,
+ ItemTitle,
+} from "@/src/components/ui/item";
+import { Separator } from "@/src/components/ui/separator";
+import { Skeleton } from "@/src/components/ui/skeleton";
+import { useProject, useProjectMutations } from "@/src/hooks/useProject";
+import { useTeam } from "@/src/hooks/useTeam";
+import { hasPermission } from "@/src/lib/utils/team-utils";
+import { Trash } from "lucide-react";
+import { useRouter } from "next/navigation";
+import React from "react";
+import { toast } from "sonner";
+
+const CardComponent = ({ children }: React.PropsWithChildren) => {
+ return (
+
+
+ DANGER ZONE
+
+
+
+ -
+
+
+
+
+ Delete Project
+
+ Your project will be permanently deleted including all of its data. This action is
+ irreversible.
+
+
+ {children}
+
+
+
+ );
+};
+
+export default function ProjectDangerZone() {
+ const router = useRouter();
+ const { data: team, isLoading: isTeamLoading } = useTeam();
+ const { data: project, isLoading: isProjectLoading } = useProject();
+
+ const { deleteProject } = useProjectMutations();
+
+ const handleProjectDeletion = async () => {
+ const id = toast.loading("Deleting project...");
+ deleteProject
+ .mutateAsync({ teamId: team!.id, projectId: project!.id })
+ .then(() => {
+ toast.success("Successfully deleted project", { id });
+ router.replace(`/dashboard/${team?.slug}`);
+ })
+ .catch((error) => {
+ if (error instanceof Error) {
+ toast.error(error.message, { id });
+ } else {
+ toast.error("An unexpected error happened while deleting project", {
+ id,
+ });
+ }
+ });
+ };
+
+ if (isTeamLoading || isProjectLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Delete Project
+
+ }
+ />
+
+ );
+}
diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectName.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectName.tsx
new file mode 100644
index 0000000..6eaedc0
--- /dev/null
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/ProjectName.tsx
@@ -0,0 +1,125 @@
+"use client";
+import CallbackDialog from "@/src/components/CallbackDialog";
+import { Button } from "@/src/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/src/components/ui/card";
+import { Form, FormControl, FormField, FormItem, FormMessage } from "@/src/components/ui/form";
+import { Input } from "@/src/components/ui/input";
+import { Separator } from "@/src/components/ui/separator";
+import { Skeleton } from "@/src/components/ui/skeleton";
+import { useProject, useProjectMutations } from "@/src/hooks/useProject";
+import { useTeam } from "@/src/hooks/useTeam";
+import { RenameProjectSchema } from "@/src/lib/types/project-types";
+import { hasPermission } from "@/src/lib/utils/team-utils";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { toast } from "sonner";
+
+const CardComponent = ({ children }: React.PropsWithChildren) => (
+
+
+ Project Name
+
+ This is the name of your project displayed across the dashboard.
+
+
+
+ {children}
+
+);
+
+export default function ProjectName() {
+ const router = useRouter();
+ const { data: team, isLoading: isTeamLoading } = useTeam();
+ const { data: project, isLoading: isProjectLoading } = useProject();
+ const [open, setIsOpen] = useState(false);
+ const { renameProject } = useProjectMutations();
+
+ const formSchema = RenameProjectSchema.refine((values) => values.name !== project?.name, {
+ error: "New project name must be different than your current project name",
+ });
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: "",
+ },
+ });
+
+ const handleChangeName = (name: string) => {
+ const id = toast.loading("Updating project name...");
+ renameProject
+ .mutateAsync({ teamId: team!.id, projectId: project!.id, newName: name })
+ .then(async (newProject) => {
+ toast.success("Successfully updated project name!", {
+ id,
+ });
+ router.replace(`/dashboard/${team?.slug}/projects/${newProject?.slug}/settings`);
+ })
+ .catch((error) => {
+ if (error instanceof Error) {
+ toast.error(error.message, { id });
+ } else {
+ toast.error("An unexpected error happened while updating project name", {
+ id,
+ });
+ }
+ });
+ };
+
+ if (isTeamLoading || isProjectLoading) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const disabled = !hasPermission(team?.role, "RenameProject");
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/page.tsx b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/page.tsx
new file mode 100644
index 0000000..5a02ebf
--- /dev/null
+++ b/src/app/dashboard/[teamSlug]/projects/[projectSlug]/settings/page.tsx
@@ -0,0 +1,12 @@
+import ProjectDangerZone from "./ProjectDangerZone";
+import ProjectName from "./ProjectName";
+
+export default function Page() {
+ return (
+ <>
+ Project Settings
+
+
+ >
+ );
+}
diff --git a/src/hooks/useProject.ts b/src/hooks/useProject.ts
index a5042b2..bb7ce7c 100644
--- a/src/hooks/useProject.ts
+++ b/src/hooks/useProject.ts
@@ -66,6 +66,11 @@ export function useProjectMutations() {
return [...prevData, project];
},
);
+
+ queryClient.setQueryData(
+ ["project", variables.teamId, project.id],
+ () => project,
+ );
},
});
@@ -77,14 +82,18 @@ export function useProjectMutations() {
teamId: string;
projectId: string;
}) => ProjectController.delete(teamId, projectId),
- onSuccess: (project, variables) => {
+ onSuccess: (oldProject, variables) => {
queryClient.setQueryData(
["projects", variables.teamId],
(prevData) => {
if (!prevData) return prevData;
- return prevData.filter((p) => p.id !== project.id);
+ return prevData.filter((p) => p.id !== oldProject.id);
},
);
+
+ queryClient.removeQueries({
+ queryKey: ["project", variables.teamId, oldProject.slug],
+ });
},
});
@@ -106,6 +115,11 @@ export function useProjectMutations() {
return prevData.map((p) => (p.id === project.id ? project : p));
},
);
+
+ queryClient.setQueryData(
+ ["project", variables.teamId, project.id],
+ () => project,
+ );
},
});