diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
new file mode 100644
index 000000000..038ddcb6a
--- /dev/null
+++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
@@ -0,0 +1,172 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { api } from "@/utils/api";
+import { Copy, Loader2 } from "lucide-react";
+import { useRouter } from "next/router";
+import { useState } from "react";
+import { toast } from "sonner";
+
+export type Services = {
+ appName: string;
+ serverId?: string | null;
+ name: string;
+ type:
+ | "mariadb"
+ | "application"
+ | "postgres"
+ | "mysql"
+ | "mongo"
+ | "redis"
+ | "compose";
+ description?: string | null;
+ id: string;
+ createdAt: string;
+ status?: "idle" | "running" | "done" | "error";
+};
+
+interface DuplicateProjectProps {
+ projectId: string;
+ services: Services[];
+ selectedServiceIds: string[];
+}
+
+export const DuplicateProject = ({
+ projectId,
+ services,
+ selectedServiceIds,
+}: DuplicateProjectProps) => {
+ const [open, setOpen] = useState(false);
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const utils = api.useUtils();
+ const router = useRouter();
+
+ const selectedServices = services.filter((service) =>
+ selectedServiceIds.includes(service.id),
+ );
+
+ const { mutateAsync: duplicateProject, isLoading } =
+ api.project.duplicate.useMutation({
+ onSuccess: async (newProject) => {
+ await utils.project.all.invalidate();
+ toast.success("Project duplicated successfully");
+ setOpen(false);
+ router.push(`/dashboard/project/${newProject.projectId}`);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ });
+
+ const handleDuplicate = async () => {
+ if (!name) {
+ toast.error("Project name is required");
+ return;
+ }
+
+ await duplicateProject({
+ sourceProjectId: projectId,
+ name,
+ description,
+ includeServices: true,
+ selectedServices: selectedServices.map((service) => ({
+ id: service.id,
+ type: service.type,
+ })),
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx
index d6fc9dcbf..e3cfce16d 100644
--- a/apps/dokploy/pages/dashboard/project/[projectId].tsx
+++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx
@@ -92,6 +92,7 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
+import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = {
appName: string;
@@ -553,7 +554,7 @@ const Project = (
{data?.description}
- {(auth?.role === "owner" || auth?.canCreateServices) && (
+
@@ -569,7 +570,7 @@ const Project = (
className="w-[200px] space-y-2"
align="end"
>
-
+
Actions
@@ -593,7 +594,7 @@ const Project = (
- )}
+
{isLoading ? (
@@ -670,20 +671,27 @@ const Project = (
{(auth?.role === "owner" ||
auth?.canDeleteServices) && (
-
-
-
+
+
+
+ >
)}