-
-
TXT
-
{`unsend._domainkey.${domainQuery.data?.name}`}
-
{`p=${domainQuery.data?.publicKey}`}
-
- {domainQuery.data?.dkimStatus?.toLowerCase()}
-
-
-
-
TXT
-
{`send.${domainQuery.data?.name}`}
-
{`"v=spf1 include:amazonses.com ~all"`}
-
- {domainQuery.data?.spfDetails?.toLowerCase()}
-
-
-
-
MX
-
{`send.${domainQuery.data?.name}`}
-
{`feedback-smtp.${domainQuery.data?.region}.amazonses.com`}
-
- {domainQuery.data?.spfDetails?.toLowerCase()}
-
+
+
+
+ {/*
+
{domainQuery.data?.name}
+ */}
+
+
+
+
+ Domains
+
+
+
+
+
+ {domainQuery.data?.name}
+
+
+
+
+
+
+
+ {domainQuery.data ? (
+
+ ) : null}
- >
+
+
+
DNS records
+
+
+
+ Type
+ Value
+ Status
+ TTL
+ Priority
+ Status
+
+
+
+
+ MX
+
+
+
+
+
+ {/*
+ {`feedback-smtp.${domainQuery.data?.region}.amazonses.com`}
+
*/}
+
+ Auto
+ 10
+
+
+
+
+
+ TXT
+
+
+
+
+
+
+ Auto
+
+
+
+
+
+
+ TXT
+
+
+
+
+
+
+ Auto
+
+
+
+
+
+
+ TXT
+
+
+
+
+
+
+ Auto
+
+
+
+
+
+
+
+
+ {domainQuery.data ? (
+
+ ) : null}
+
)}
);
}
+
+const InputWithCopyButton: React.FC<{ value: string; className?: string }> = ({
+ value,
+ className,
+}) => {
+ const [isCopied, setIsCopied] = React.useState(false);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(value);
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 2000); // Reset isCopied to false after 2 seconds
+ } catch (err) {
+ console.error("Failed to copy: ", err);
+ }
+ };
+
+ return (
+
+
{value}
+
+ {isCopied ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
+ const updateDomain = api.domain.updateDomain.useMutation();
+ const utils = api.useUtils();
+
+ const [clickTracking, setClickTracking] = React.useState(
+ domain.clickTracking
+ );
+ const [openTracking, setOpenTracking] = React.useState(domain.openTracking);
+
+ function handleClickTrackingChange() {
+ setClickTracking(!clickTracking);
+ updateDomain.mutate(
+ { id: domain.id, clickTracking: !clickTracking },
+ {
+ onSuccess: () => {
+ utils.domain.domains.invalidate();
+ },
+ }
+ );
+ }
+
+ function handleOpenTrackingChange() {
+ setOpenTracking(!openTracking);
+ updateDomain.mutate(
+ { id: domain.id, openTracking: !openTracking },
+ {
+ onSuccess: () => {
+ utils.domain.domains.invalidate();
+ },
+ }
+ );
+ }
+ return (
+
+
Settings
+
+
Click tracking
+
+ Track any links in your emails content.{" "}
+
+
+
+
+
+
Open tracking
+
+ Unsend adds a tracking pixel to every email you send. This allows you
+ to see how many people open your emails. This will affect the delivery
+ rate of your emails.
+
+
+
+
+
+
Danger
+
+
+ Deleting a domain will remove all of its DNS records and stop sending
+ emails.
+
+
+
+
+ );
+};
+
+const DnsVerificationStatus: React.FC<{ status: string }> = ({ status }) => {
+ let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
+ switch (status) {
+ case DomainStatus.NOT_STARTED:
+ badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
+ break;
+ case DomainStatus.SUCCESS:
+ badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
+ break;
+ case DomainStatus.FAILED:
+ badgeColor = "bg-red-500/10 text-red-600 border-red-500/20";
+ break;
+ case DomainStatus.TEMPORARY_FAILURE:
+ case DomainStatus.PENDING:
+ badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10";
+ break;
+ default:
+ badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
+ }
+
+ return (
+
+
+ {status.split("_").join(" ").toLowerCase()}
+
+
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx
new file mode 100644
index 0000000..24e6006
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { Button } from "@unsend/ui/src/button";
+import { Input } from "@unsend/ui/src/input";
+import { Label } from "@unsend/ui/src/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@unsend/ui/src/dialog";
+import { api } from "~/trpc/react";
+import React, { useState } from "react";
+import { Domain } from "@prisma/client";
+import { useRouter } from "next/navigation";
+import { toast } from "@unsend/ui/src/toaster";
+import { Send, SendHorizonal } from "lucide-react";
+
+export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
+ const [open, setOpen] = useState(false);
+ const [domainName, setDomainName] = useState("");
+ const deleteDomainMutation = api.domain.deleteDomain.useMutation();
+
+ const utils = api.useUtils();
+
+ const router = useRouter();
+
+ function handleSave() {
+ deleteDomainMutation.mutate(
+ {
+ id: domain.id,
+ },
+ {
+ onSuccess: () => {
+ utils.domain.domains.invalidate();
+ setOpen(false);
+ toast.success(`Domain ${domain.name} deleted`);
+ router.replace("/domains");
+ },
+ }
+ );
+ }
+
+ return (
+
(_open !== open ? setOpen(_open) : null)}
+ >
+
+
+
+ Send test email
+
+
+
+
+ Send test email
+
+
+
+ );
+};
+
+export default SendTestMail;
diff --git a/apps/web/src/app/(dashboard)/domains/domain-badge.tsx b/apps/web/src/app/(dashboard)/domains/domain-badge.tsx
new file mode 100644
index 0000000..78e3e67
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/domains/domain-badge.tsx
@@ -0,0 +1,34 @@
+import { DomainStatus } from "@prisma/client";
+
+export const DomainStatusBadge: React.FC<{ status: DomainStatus }> = ({
+ status,
+}) => {
+ let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
+ switch (status) {
+ case DomainStatus.NOT_STARTED:
+ badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
+ break;
+ case DomainStatus.SUCCESS:
+ badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
+ break;
+ case DomainStatus.FAILED:
+ badgeColor = "bg-red-500/10 text-red-600 border-red-500/20";
+ break;
+ case DomainStatus.TEMPORARY_FAILURE:
+ case DomainStatus.PENDING:
+ badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10";
+ break;
+ default:
+ badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
+ }
+
+ return (
+
+
+ {status === "SUCCESS" ? "Verified" : status.toLowerCase()}
+
+
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/domains/domain-list.tsx b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
index 3552199..f2cc993 100644
--- a/apps/web/src/app/(dashboard)/domains/domain-list.tsx
+++ b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
@@ -6,6 +6,8 @@ import Link from "next/link";
import { Switch } from "@unsend/ui/src/switch";
import { api } from "~/trpc/react";
import React from "react";
+import { StatusIndicator } from "./status-indicator";
+import { DomainStatusBadge } from "./domain-badge";
export default function DomainsList() {
const domainsQuery = api.domain.domains.useQuery();
@@ -72,6 +74,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
Created at
@@ -112,55 +115,3 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
);
};
-
-const DomainStatusBadge: React.FC<{ status: DomainStatus }> = ({ status }) => {
- let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
- switch (status) {
- case DomainStatus.NOT_STARTED:
- badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
- break;
- case DomainStatus.SUCCESS:
- badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
- break;
- case DomainStatus.FAILED:
- badgeColor = "bg-red-500/10 text-red-800 border-red-600/10";
- break;
- case DomainStatus.TEMPORARY_FAILURE:
- case DomainStatus.PENDING:
- badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10";
- break;
- default:
- badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
- }
-
- return (
-
- {status === "SUCCESS" ? "Verified" : status.toLowerCase()}
-
- );
-};
-
-const StatusIndicator: React.FC<{ status: DomainStatus }> = ({ status }) => {
- let badgeColor = "bg-gray-400"; // Default color
- switch (status) {
- case DomainStatus.NOT_STARTED:
- badgeColor = "bg-gray-400";
- break;
- case DomainStatus.SUCCESS:
- badgeColor = "bg-emerald-500";
- break;
- case DomainStatus.FAILED:
- badgeColor = "bg-red-500";
- break;
- case DomainStatus.TEMPORARY_FAILURE:
- case DomainStatus.PENDING:
- badgeColor = "bg-yellow-500";
- break;
- default:
- badgeColor = "bg-gray-400";
- }
-
- return
;
-};
diff --git a/apps/web/src/app/(dashboard)/domains/status-indicator.tsx b/apps/web/src/app/(dashboard)/domains/status-indicator.tsx
new file mode 100644
index 0000000..3aa8353
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/domains/status-indicator.tsx
@@ -0,0 +1,26 @@
+import { DomainStatus } from "@prisma/client";
+
+export const StatusIndicator: React.FC<{ status: DomainStatus }> = ({
+ status,
+}) => {
+ let badgeColor = "bg-gray-400"; // Default color
+ switch (status) {
+ case DomainStatus.NOT_STARTED:
+ badgeColor = "bg-gray-400";
+ break;
+ case DomainStatus.SUCCESS:
+ badgeColor = "bg-emerald-500";
+ break;
+ case DomainStatus.FAILED:
+ badgeColor = "bg-red-500";
+ break;
+ case DomainStatus.TEMPORARY_FAILURE:
+ case DomainStatus.PENDING:
+ badgeColor = "bg-yellow-500";
+ break;
+ default:
+ badgeColor = "bg-gray-400";
+ }
+
+ return
;
+};
diff --git a/apps/web/src/app/(dashboard)/nav-button.tsx b/apps/web/src/app/(dashboard)/nav-button.tsx
index 938a1bc..3855092 100644
--- a/apps/web/src/app/(dashboard)/nav-button.tsx
+++ b/apps/web/src/app/(dashboard)/nav-button.tsx
@@ -10,7 +10,7 @@ export const NavButton: React.FC<{
}> = ({ href, children }) => {
const pathname = usePathname();
- const isActive = pathname === href;
+ const isActive = pathname?.startsWith(href);
return (
+
{children}
diff --git a/apps/web/src/server/api/routers/domain.ts b/apps/web/src/server/api/routers/domain.ts
index b1f73d9..9abdd28 100644
--- a/apps/web/src/server/api/routers/domain.ts
+++ b/apps/web/src/server/api/routers/domain.ts
@@ -9,6 +9,7 @@ import {
import { db } from "~/server/db";
import {
createDomain,
+ deleteDomain,
getDomain,
updateDomain,
} from "~/server/service/domain-service";
@@ -53,4 +54,11 @@ export const domainRouter = createTRPCRouter({
openTracking: input.openTracking,
});
}),
+
+ deleteDomain: teamProcedure
+ .input(z.object({ id: z.number() }))
+ .mutation(async ({ ctx, input }) => {
+ await deleteDomain(input.id);
+ return { success: true };
+ }),
});
diff --git a/apps/web/src/server/service/domain-service.ts b/apps/web/src/server/service/domain-service.ts
index 852cf89..ead67c5 100644
--- a/apps/web/src/server/service/domain-service.ts
+++ b/apps/web/src/server/service/domain-service.ts
@@ -1,15 +1,21 @@
-import { addDomain, getDomainIdentity } from "~/server/ses";
+import dns from "dns";
+import util from "util";
+import * as tldts from "tldts";
+import * as ses from "~/server/ses";
import { db } from "~/server/db";
+const dnsResolveTxt = util.promisify(dns.resolveTxt);
+
export async function createDomain(teamId: number, name: string) {
- console.log("Creating domain:", name);
- const publicKey = await addDomain(name);
+ const subdomain = tldts.getSubdomain(name);
+ const publicKey = await ses.addDomain(name);
const domain = await db.domain.create({
data: {
name,
publicKey,
teamId,
+ subdomain,
},
});
@@ -28,7 +34,10 @@ export async function getDomain(id: number) {
}
if (domain.status !== "SUCCESS") {
- const domainIdentity = await getDomainIdentity(domain.name, domain.region);
+ const domainIdentity = await ses.getDomainIdentity(
+ domain.name,
+ domain.region
+ );
const dkimStatus = domainIdentity.DkimAttributes?.Status;
const spfDetails = domainIdentity.MailFromAttributes?.MailFromDomainStatus;
@@ -36,13 +45,17 @@ export async function getDomain(id: number) {
const verificationStatus = domainIdentity.VerificationStatus;
const lastCheckedTime =
domainIdentity.VerificationInfo?.LastCheckedTimestamp;
+ const _dmarcRecord = await getDmarcRecord(domain.name);
+ const dmarcRecord = _dmarcRecord?.[0]?.[0];
console.log(domainIdentity);
+ console.log(dmarcRecord);
if (
domain.dkimStatus !== dkimStatus ||
domain.spfDetails !== spfDetails ||
- domain.status !== verificationStatus
+ domain.status !== verificationStatus ||
+ domain.dmarcAdded !== (dmarcRecord ? true : false)
) {
domain = await db.domain.update({
where: {
@@ -52,16 +65,18 @@ export async function getDomain(id: number) {
dkimStatus,
spfDetails,
status: verificationStatus ?? "NOT_STARTED",
+ dmarcAdded: dmarcRecord ? true : false,
},
});
}
return {
...domain,
- dkimStatus,
- spfDetails,
- verificationError,
+ dkimStatus: dkimStatus?.toString() ?? null,
+ spfDetails: spfDetails?.toString() ?? null,
+ verificationError: verificationError?.toString() ?? null,
lastCheckedTime,
+ dmarcAdded: dmarcRecord ? true : false,
};
}
@@ -77,3 +92,33 @@ export async function updateDomain(
data,
});
}
+
+export async function deleteDomain(id: number) {
+ const domain = await db.domain.findUnique({
+ where: { id },
+ });
+
+ if (!domain) {
+ throw new Error("Domain not found");
+ }
+
+ const deleted = await ses.deleteDomain(domain.name, domain.region);
+
+ if (!deleted) {
+ throw new Error("Error in deleting domain");
+ }
+
+ return db.domain.delete({
+ where: { id },
+ });
+}
+
+async function getDmarcRecord(domain: string) {
+ try {
+ const dmarcRecord = await dnsResolveTxt(`_dmarc.${domain}`);
+ return dmarcRecord;
+ } catch (error) {
+ console.error("Error fetching DMARC record:", error);
+ return null; // or handle error as appropriate
+ }
+}
diff --git a/apps/web/src/server/ses.ts b/apps/web/src/server/ses.ts
index b0aac9c..8b9e6de 100644
--- a/apps/web/src/server/ses.ts
+++ b/apps/web/src/server/ses.ts
@@ -66,7 +66,7 @@ export async function addDomain(domain: string, region = "us-east-1") {
const emailIdentityCommand = new PutEmailIdentityMailFromAttributesCommand({
EmailIdentity: domain,
- MailFromDomain: `send.${domain}`,
+ MailFromDomain: `mail.${domain}`,
});
const emailIdentityResponse = await sesClient.send(emailIdentityCommand);
@@ -75,12 +75,23 @@ export async function addDomain(domain: string, region = "us-east-1") {
response.$metadata.httpStatusCode !== 200 ||
emailIdentityResponse.$metadata.httpStatusCode !== 200
) {
- throw new Error("Failed to create email identity");
+ console.log(response);
+ console.log(emailIdentityResponse);
+ throw new Error("Failed to create domain identity");
}
return publicKey;
}
+export async function deleteDomain(domain: string, region = "us-east-1") {
+ const sesClient = getSesClient(region);
+ const command = new DeleteEmailIdentityCommand({
+ EmailIdentity: domain,
+ });
+ const response = await sesClient.send(command);
+ return response.$metadata.httpStatusCode === 200;
+}
+
export async function getDomainIdentity(domain: string, region = "us-east-1") {
const sesClient = getSesClient(region);
const command = new GetEmailIdentityCommand({
@@ -165,7 +176,6 @@ export async function addWebhookConfiguration(
ConfigurationSetName: configName, // required
EventDestinationName: "unsend_destination", // required
EventDestination: {
- // EventDestinationDefinition
Enabled: true,
MatchingEventTypes: eventTypes,
SnsDestination: {
diff --git a/packages/ui/index.ts b/packages/ui/index.ts
index cb0ff5c..816b2a0 100644
--- a/packages/ui/index.ts
+++ b/packages/ui/index.ts
@@ -1 +1,3 @@
-export {};
+import { cn } from "./lib/utils";
+
+export { cn };
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 82beaa7..89d9776 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -38,6 +38,7 @@
"lucide-react": "^0.359.0",
"next-themes": "^0.3.0",
"pnpm": "^8.15.5",
+ "sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7"
}
diff --git a/packages/ui/src/breadcrumb.tsx b/packages/ui/src/breadcrumb.tsx
new file mode 100644
index 0000000..755e8e9
--- /dev/null
+++ b/packages/ui/src/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "../lib/utils";
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) =>
);
+Breadcrumb.displayName = "Breadcrumb";
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = "BreadcrumbList";
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = "BreadcrumbItem";
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = "BreadcrumbLink";
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = "BreadcrumbPage";
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+
svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx
index e169daf..6ff34b6 100644
--- a/packages/ui/src/button.tsx
+++ b/packages/ui/src/button.tsx
@@ -17,12 +17,13 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
+ icon: "bg-transparent hover:bg-transparent hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
- default: "h-10 px-4 py-2",
- sm: "h-9 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
+ default: "h-9 px-4 ",
+ sm: "h-8 rounded-md px-3",
+ lg: "h-10 rounded-md px-8",
icon: "h-10 w-10",
},
},
diff --git a/packages/ui/src/input.tsx b/packages/ui/src/input.tsx
index 7e22ae6..01ad3bd 100644
--- a/packages/ui/src/input.tsx
+++ b/packages/ui/src/input.tsx
@@ -11,7 +11,7 @@ const Input = React.forwardRef
(
;
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme();
+
+ return (
+
+ );
+};
+
+export { Toaster, toast };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 467fc2f..931d84a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -160,6 +160,9 @@ importers:
superjson:
specifier: ^2.2.1
version: 2.2.1
+ tldts:
+ specifier: ^6.1.16
+ version: 6.1.16
zod:
specifier: ^3.22.4
version: 3.22.4
@@ -293,6 +296,9 @@ importers:
pnpm:
specifier: ^8.15.5
version: 8.15.5
+ sonner:
+ specifier: ^1.4.41
+ version: 1.4.41(react-dom@18.2.0)(react@18.2.0)
tailwind-merge:
specifier: ^2.2.2
version: 2.2.2
@@ -4133,7 +4139,7 @@ packages:
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@@ -6323,6 +6329,16 @@ packages:
engines: {node: '>=12'}
dev: true
+ /sonner@1.4.41(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/sort-object-keys@1.1.3:
resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==}
dev: true
@@ -6591,6 +6607,17 @@ packages:
dependencies:
any-promise: 1.3.0
+ /tldts-core@6.1.16:
+ resolution: {integrity: sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==}
+ dev: false
+
+ /tldts@6.1.16:
+ resolution: {integrity: sha512-X6VrQzW4RymhI1kBRvrWzYlRLXTftZpi7/s/9ZlDILA04yM2lNX7mBvkzDib9L4uSymHt8mBbeaielZMdsAkfQ==}
+ hasBin: true
+ dependencies:
+ tldts-core: 6.1.16
+ dev: false
+
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}