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 ( + +
+ setIsOpen(true))} + > + ( + + + + + + + )} + /> + { + form.handleSubmit(({ name }) => { + handleChangeName(name); + })(); + }} + /> + + + +
+ ); +} 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, + ); }, });