feat(domain): add duplicate feature

This commit is contained in:
oiov
2025-06-23 10:54:12 +08:00
parent cdee24b5c9
commit dcb8479017
7 changed files with 180 additions and 24 deletions
+110 -20
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useTransition } from "react";
import Link from "next/link";
import { User } from "@prisma/client";
import { PenLine, RefreshCwIcon } from "lucide-react";
@@ -13,6 +13,13 @@ import { fetcher } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { Skeleton } from "@/components/ui/skeleton";
@@ -67,8 +74,10 @@ function TableColumnSekleton() {
export default function DomainList({ user, action }: DomainListProps) {
const { isMobile } = useMediaQuery();
const [isPending, startTransition] = useTransition();
const t = useTranslations("List");
const [isShowForm, setShowForm] = useState(false);
const [isShowDuplicateForm, setShowDuplicateForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
const [currentEditDomain, setCurrentEditDomain] =
useState<DomainFormData | null>(null);
@@ -123,6 +132,26 @@ export default function DomainList({ user, action }: DomainListProps) {
}
};
const handleDuplicate = () => {
startTransition(async () => {
const response = await fetch(`${action}/duplicate`, {
method: "POST",
body: JSON.stringify({
domain: currentEditDomain?.domain_name,
}),
});
if (!response.ok || response.status !== 200) {
toast.error("Duplicate Failed!", {
description: await response.text(),
});
} else {
toast.success(`Duplicate successfully!`);
setShowDuplicateForm(false);
handleRefresh();
}
});
};
return (
<>
<Card className="xl:col-span-2">
@@ -290,27 +319,52 @@ export default function DomainList({ user, action }: DomainListProps) {
<TimeAgoIntl date={domain.updatedAt as Date} />
</TableCell>
<TableCell className="col-span-1 flex items-center gap-1">
<Button
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground sm:px-1.5"
size="sm"
variant={"outline"}
onClick={() => {
setCurrentEditDomain(domain);
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
}}
>
<p className="hidden text-nowrap sm:block">
{t("Edit")}
</p>
<PenLine className="mx-0.5 size-4 sm:ml-1 sm:size-3" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="size-[25px] p-1.5"
size="sm"
variant="ghost"
>
<Icons.moreVertical className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild>
<Button
className="flex w-full items-center gap-2 text-nowrap"
size="sm"
variant="ghost"
onClick={() => {
setCurrentEditDomain(domain);
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
}}
>
{/* <PenLine className="mx-0.5 size-4" /> */}
{t("Edit")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
className="flex w-full items-center gap-2"
size="sm"
variant="ghost"
onClick={() => {
setCurrentEditDomain(domain);
setShowDuplicateForm(false);
setShowDuplicateForm(!isShowDuplicateForm);
}}
>
{t("Duplicate")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
{/* {isShowDomainInfo && selectedDomain?.id === domain.id && (
<DomainInfo domain={domain} />
)} */}
</div>
))
) : (
@@ -355,6 +409,42 @@ export default function DomainList({ user, action }: DomainListProps) {
onRefresh={handleRefresh}
/>
</Modal>
<Modal
showModal={isShowDuplicateForm}
setShowModal={setShowDuplicateForm}
>
<div className="flex flex-col items-start border-b p-4 pt-8 sm:px-16">
<h2 className="mb-2 text-lg font-bold">
{t("Confirm duplicate domain")} ?
</h2>
<p>
{t(
"This will duplicate all configuration information for the {domain} domain, and create a new domain",
{ domain: currentEditDomain?.domain_name || "" },
)}
.
</p>
<div className="mt-6 flex w-full items-center justify-between gap-2">
<Button
type="reset"
variant="destructive"
className="w-[100px] px-0"
onClick={() => setShowDuplicateForm(false)}
>
{t("Cancel")}
</Button>
<Button
className="w-full text-nowrap"
disabled={isPending}
onClick={() => handleDuplicate()}
>
{t("Duplicate")}
</Button>
</div>
</div>
</Modal>
</>
);
}
+1 -1
View File
@@ -265,7 +265,7 @@ export default function UsersList({ user }: UrlListProps) {
setShowForm(!isShowForm);
}}
>
<p>{t("Edit")}</p>
<p className="text-nowrap">{t("Edit")}</p>
<PenLine className="ml-1 size-4" />
</Button>
</TableCell>
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest } from "next/server";
import { createDomain, getDomainByName } from "@/lib/dto/domains";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
export async function POST(req: NextRequest) {
try {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
if (user.role !== "ADMIN") {
return Response.json("Unauthorized", { status: 401 });
}
const { domain } = await req.json();
if (!domain) {
return Response.json("Domain name is required", { status: 400 });
}
const target_domain = await getDomainByName(domain);
if (!target_domain) {
return Response.json("Domain not found", { status: 404 });
}
const newDomain = await createDomain({
domain_name: target_domain.domain_name + "-copy",
enable_short_link: !!target_domain.enable_short_link,
enable_email: !!target_domain.enable_email,
enable_dns: !!target_domain.enable_dns,
cf_zone_id: target_domain.cf_zone_id,
cf_api_key: target_domain.cf_api_key,
cf_email: target_domain.cf_email,
cf_record_types: target_domain.cf_record_types,
cf_api_key_encrypted: false,
resend_api_key: target_domain.resend_api_key,
max_short_links: target_domain.max_short_links,
max_email_forwards: target_domain.max_email_forwards,
max_dns_records: target_domain.max_dns_records,
min_url_length: target_domain.min_url_length,
min_email_length: target_domain.min_email_length,
min_record_length: target_domain.min_record_length,
active: true,
});
if (!newDomain) {
return Response.json("Failed to create domain", { status: 400 });
}
return Response.json("Success", { status: 200 });
} catch (error) {
console.error("[Error]", error);
return Response.json(error.message || "Server error", { status: 500 });
}
}
+6
View File
@@ -112,6 +112,12 @@ export async function getDomainsByFeatureClient(feature: string) {
}
}
export async function getDomainByName(domain_name: string) {
return await prisma.domain.findUnique({
where: { domain_name },
});
}
export async function checkDomainIsConfiguratedResend(domain_name: string) {
try {
const domain = await prisma.domain.findUnique({
+4 -1
View File
@@ -181,7 +181,10 @@
"USER": "USER",
"Total Users": "Total Users",
"Edit User": "Edit User",
"Login Password": "Password"
"Login Password": "Password",
"Duplicate": "Duplicate",
"Confirm duplicate domain": "Confirm duplicate domain",
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "This will duplicate all configuration information for the {domain} domain, and create a new domain"
},
"Components": {
"Dashboard": "Dashboard",
+4 -1
View File
@@ -181,7 +181,10 @@
"USER": "用户",
"Total Users": "用户总数",
"Edit User": "编辑用户",
"Login Password": "用户密码"
"Login Password": "用户密码",
"Duplicate": "复制",
"Confirm duplicate domain": "确认复制域名",
"This will duplicate all configuration information for the {domain} domain, and create a new domain": "这将复制 {domain} 域名的所有配置信息,并创建一个新域名"
},
"Components": {
"Dashboard": "用户面板",
+1 -1
View File
File diff suppressed because one or more lines are too long