chore styles and cpns
This commit is contained in:
@@ -57,14 +57,7 @@ export default async function DashboardPage() {
|
||||
icon="globeLock"
|
||||
/>
|
||||
</div>
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
}}
|
||||
action="/api/record"
|
||||
/>
|
||||
<LiveLog admin={false} />
|
||||
<UserUrlsList
|
||||
user={{
|
||||
id: user.id,
|
||||
@@ -74,7 +67,14 @@ export default async function DashboardPage() {
|
||||
}}
|
||||
action="/api/url"
|
||||
/>
|
||||
<LiveLog admin={false} />
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
}}
|
||||
action="/api/record"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function LiveLog({ admin }: { admin: boolean }) {
|
||||
Live Log
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time updates of short URL visits.
|
||||
Real-time logs of short link visits.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -164,9 +164,7 @@ export default function LiveLog({ admin }: { admin: boolean }) {
|
||||
variant={"outline"}
|
||||
size="sm"
|
||||
className={`ml-auto gap-2 bg-primary-foreground transition-colors hover:border-blue-600 hover:text-blue-600 ${
|
||||
isLive
|
||||
? "animate-pulse border-dashed border-blue-600 text-blue-500"
|
||||
: ""
|
||||
isLive ? "border-dashed border-blue-600 text-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
<Icons.CirclePlay className="h-4 w-4" /> {isLive ? "Stop" : "Live"}
|
||||
|
||||
+121
-291
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ForwardEmail } from "@prisma/client";
|
||||
import { toast } from "sonner";
|
||||
@@ -9,11 +8,19 @@ import useSWR from "swr";
|
||||
|
||||
import { cn, fetcher, htmlToText, timeAgo } from "@/lib/utils";
|
||||
|
||||
import BlurImage from "../shared/blur-image";
|
||||
import { Icons } from "../shared/icons";
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import { Badge } from "../ui/badge";
|
||||
// import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { Switch } from "../ui/switch";
|
||||
import {
|
||||
@@ -24,28 +31,7 @@ import {
|
||||
} from "../ui/tooltip";
|
||||
import EmailDetail from "./EmailDetail";
|
||||
import Loader from "./Loader";
|
||||
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
import { BlurImg } from "../shared/blur-image";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "../ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
|
||||
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
|
||||
import { SendEmailModal } from "./SendEmailModal";
|
||||
|
||||
interface EmailListProps {
|
||||
emailAddress: string | null;
|
||||
@@ -66,11 +52,7 @@ export default function EmailList({
|
||||
const [isAutoRefresh, setIsAutoRefresh] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [showSendDrawer, setShowSendDrawer] = useState(false);
|
||||
const [sendForm, setSendForm] = useState({ to: "", subject: "", html: "" });
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
|
||||
|
||||
const [showMutiCheckBox, setShowMutiCheckBox] = useState(false);
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<{
|
||||
@@ -83,11 +65,10 @@ export default function EmailList({
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: isAutoRefresh ? 5000 : 0,
|
||||
dedupingInterval: 2000, // 避免短时间内重复请求
|
||||
dedupingInterval: 2000,
|
||||
},
|
||||
);
|
||||
|
||||
// 切换email address时,清空选中的email
|
||||
useEffect(() => {
|
||||
if (emailAddress && selectedEmailId) {
|
||||
const emailExists = data?.list.some(
|
||||
@@ -99,68 +80,6 @@ export default function EmailList({
|
||||
}
|
||||
}, [emailAddress, data, selectedEmailId]);
|
||||
|
||||
if (!emailAddress) {
|
||||
return (
|
||||
<div className="grids flex flex-1 animate-fade-in flex-col items-center justify-center p-4 text-center text-neutral-600 dark:text-neutral-400">
|
||||
<BlurImg
|
||||
className="size-40"
|
||||
src="/_static/landing/mailbox.svg"
|
||||
height={200}
|
||||
width={200}
|
||||
/>
|
||||
|
||||
<h2 className="my-2 text-lg font-semibold">
|
||||
No Email Address Selected
|
||||
</h2>
|
||||
|
||||
<p className="max-w-md text-sm">
|
||||
Please select an email address from the list to view your inbox. Once
|
||||
selected, your emails will appear here automatically.
|
||||
</p>
|
||||
|
||||
<ul className="mt-3 list-disc text-left">
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#how-it-works"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to use email to send or receive emails?
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#expiration"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Will my email or inbox expire?
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#limit"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
What is the limit? It's free?
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 delay-100 dark:bg-neutral-600" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 delay-200 dark:bg-neutral-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 处理单封邮件标记为已读
|
||||
const handleMarkAsRead = async (emailId: string) => {
|
||||
try {
|
||||
await fetch("/api/email/read", {
|
||||
@@ -173,38 +92,32 @@ export default function EmailList({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理批量标记为已读
|
||||
const handleMarkSelectedAsRead = async () => {
|
||||
if (selectedEmails.length === 0) {
|
||||
toast.error("Please select at least one email");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/email/read", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ emailIds: selectedEmails }),
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/email/read", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ emailIds: selectedEmails }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSelectedEmails([]);
|
||||
mutate();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
toast.error(errorData.error || "Failed to mark emails as read");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Error marking emails as read");
|
||||
if (response.ok) {
|
||||
setSelectedEmails([]);
|
||||
mutate();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
toast.error(errorData.error || "Failed to mark emails as read");
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Error marking emails as read");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理邮件选择
|
||||
const handleSelectEmail = (emailId: string) => {
|
||||
console.log(emailId);
|
||||
|
||||
setSelectedEmails((prev) =>
|
||||
prev.includes(emailId)
|
||||
? prev.filter((id) => id !== emailId)
|
||||
@@ -224,47 +137,6 @@ export default function EmailList({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSendEmailModal = () => {
|
||||
setShowSendDrawer(true);
|
||||
setSendForm({ to: "", subject: "", html: "" });
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
if (!emailAddress) {
|
||||
toast.error("No email address selected");
|
||||
return;
|
||||
}
|
||||
if (!sendForm.to || !sendForm.subject || !sendForm.html) {
|
||||
toast.error("Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/email/send", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
from: emailAddress,
|
||||
to: sendForm.to,
|
||||
subject: sendForm.subject,
|
||||
html: sendForm.html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Email sent successfully");
|
||||
setShowSendDrawer(false);
|
||||
} else {
|
||||
toast.error("Failed to send email", {
|
||||
description: await response.text(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Error sending email");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEmailSelection = (emailId: string | null) => {
|
||||
if (emailId) {
|
||||
const selectedEmail = data?.list?.find((email) => email.id === emailId);
|
||||
@@ -275,35 +147,23 @@ export default function EmailList({
|
||||
onSelectEmail(emailId);
|
||||
};
|
||||
|
||||
if (!emailAddress) {
|
||||
return EmptyInboxSection();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("grids flex flex-1 flex-col", className)}>
|
||||
<div className="flex items-center gap-2 bg-neutral-200/40 p-2 text-base font-semibold text-neutral-600 backdrop-blur dark:bg-neutral-800 dark:text-neutral-50">
|
||||
<Icons.inbox size={20} />
|
||||
<span>INBOX</span>
|
||||
{data && data.total > 0 && (
|
||||
<Badge
|
||||
className="bg-neutral-200 px-2 py-0.5 text-xs dark:text-zinc-900"
|
||||
variant={"secondary"}
|
||||
>
|
||||
{data.total}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={"sm"}
|
||||
onClick={() => handleOpenSendEmailModal()}
|
||||
>
|
||||
<Icons.send size={17} className={cn("")} />
|
||||
</Button>
|
||||
|
||||
<SendEmailModal emailAddress={emailAddress} onSuccess={mutate} />
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger>
|
||||
<Switch
|
||||
className="mt-1 data-[state=checked]:bg-blue-500 data-[state=unchecked]:bg-neutral-300 dark:data-[state=unchecked]:bg-neutral-200"
|
||||
onCheckedChange={(value) => handleSetAutoRefresh(value)}
|
||||
onCheckedChange={handleSetAutoRefresh}
|
||||
defaultChecked={isAutoRefresh}
|
||||
aria-label="Auto refresh"
|
||||
/>
|
||||
@@ -311,10 +171,9 @@ export default function EmailList({
|
||||
<TooltipContent side="bottom">Auto refresh</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size={"sm"}
|
||||
size="sm"
|
||||
onClick={handleManualRefresh}
|
||||
disabled={isRefreshing || isLoading || isAutoRefresh}
|
||||
>
|
||||
@@ -328,53 +187,46 @@ export default function EmailList({
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
showMutiCheckBox ? "bg-primary text-primary-foreground" : "",
|
||||
)}
|
||||
variant="outline"
|
||||
size={"sm"}
|
||||
size="sm"
|
||||
onClick={() => setShowMutiCheckBox(!showMutiCheckBox)}
|
||||
>
|
||||
<Icons.listChecks className="size-4" />
|
||||
</Button>
|
||||
{selectedEmails.length > 0 && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex w-full items-center gap-1"
|
||||
>
|
||||
<span className="text-sm">more</span>
|
||||
<Icons.chevronDown className="mt-0.5 size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex w-full items-center gap-1"
|
||||
disabled={isPending}
|
||||
onClick={handleMarkSelectedAsRead}
|
||||
className="w-full"
|
||||
>
|
||||
<span className="text-sm">more</span>
|
||||
<Icons.chevronDown className="mt-0.5 size-4" />
|
||||
<span className="text-xs">Mask as read</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleMarkSelectedAsRead}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="text-xs">Mask as read</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
// onClick={handleMarkSelectedAsRead}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="text-xs">Delete selected</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button variant="ghost" size="sm" className="w-full">
|
||||
<span className="text-xs">Delete selected</span>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,7 +306,6 @@ export default function EmailList({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && Math.ceil(data.total / pageSize) > 1 && (
|
||||
<PaginationWrapper
|
||||
className="mx-2 my-1 justify-center"
|
||||
@@ -463,83 +314,62 @@ export default function EmailList({
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 发送邮件 Modal */}
|
||||
<Drawer open={showSendDrawer} onOpenChange={setShowSendDrawer}>
|
||||
<DrawerContent className="fixed bottom-0 right-0 top-0 w-full rounded-none sm:max-w-xl">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="flex items-center gap-1">
|
||||
Send Email{" "}
|
||||
<Icons.help className="size-5 text-neutral-600 hover:text-neutral-400" />
|
||||
</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost" className="absolute right-4 top-4">
|
||||
<Icons.close className="h-4 w-4" />
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<div className="scrollbar-hidden h-[calc(100vh)] space-y-4 overflow-y-auto p-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
From
|
||||
</label>
|
||||
<Input value={emailAddress || ""} disabled className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
To
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.to}
|
||||
onChange={(e) =>
|
||||
setSendForm({ ...sendForm, to: e.target.value })
|
||||
}
|
||||
placeholder="recipient@example.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Subject
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.subject}
|
||||
onChange={(e) =>
|
||||
setSendForm({ ...sendForm, subject: e.target.value })
|
||||
}
|
||||
placeholder="Enter subject"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Content
|
||||
</label>
|
||||
<ReactQuill
|
||||
value={sendForm.html}
|
||||
onChange={(value) => setSendForm({ ...sendForm, html: value })}
|
||||
className="mt-1 h-40 rounded-lg"
|
||||
theme="snow"
|
||||
placeholder="Enter your message"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
<Button
|
||||
onClick={handleSendEmail}
|
||||
disabled={isPending}
|
||||
variant={"default"}
|
||||
>
|
||||
{isPending ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyInboxSection() {
|
||||
return (
|
||||
<div className="grids flex flex-1 animate-fade-in flex-col items-center justify-center p-4 text-center text-neutral-600 dark:text-neutral-400">
|
||||
<BlurImage
|
||||
className="size-40"
|
||||
src="/_static/landing/mailbox.svg"
|
||||
height={200}
|
||||
width={200}
|
||||
alt="Inbox"
|
||||
/>
|
||||
<h2 className="my-2 text-lg font-semibold">No Email Address Selected</h2>
|
||||
<p className="max-w-md text-sm">
|
||||
Please select an email address from the list to view your inbox. Once
|
||||
selected, your emails will appear here automatically.
|
||||
</p>
|
||||
<ul className="mt-3 list-disc text-left">
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#how-it-works"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to use email to send or receive emails?
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#expiration"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Will my email or inbox expire?
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="text-blue-500 underline"
|
||||
href="/docs/emails#limit"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
What is the limit? It's free?
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-6 flex gap-2">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 delay-100 dark:bg-neutral-600" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-neutral-300 delay-200 dark:bg-neutral-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "../ui/select";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { SendEmailModal } from "./SendEmailModal";
|
||||
|
||||
interface EmailSidebarProps {
|
||||
user: User;
|
||||
@@ -394,7 +395,7 @@ export default function EmailSidebar({
|
||||
key={email.id}
|
||||
onClick={() => onSelectEmail(email.emailAddress)}
|
||||
className={cn(
|
||||
`border-gray-5 m-1 cursor-pointer bg-neutral-50 p-2 transition-colors hover:bg-neutral-100 dark:border-zinc-700 dark:bg-neutral-800 dark:hover:bg-neutral-900`,
|
||||
`border-gray-5 group m-1 cursor-pointer bg-neutral-50 p-2 transition-colors hover:bg-neutral-100 dark:border-zinc-700 dark:bg-neutral-800 dark:hover:bg-neutral-900`,
|
||||
selectedEmailAddress === email.emailAddress
|
||||
? "bg-gray-100 dark:bg-neutral-900"
|
||||
: "",
|
||||
@@ -419,25 +420,32 @@ export default function EmailSidebar({
|
||||
</span>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<CopyButton
|
||||
value={`${email.emailAddress}`}
|
||||
className={cn(
|
||||
"ml-auto size-5 rounded border p-1",
|
||||
"duration-250 transition-all group-hover:opacity-100",
|
||||
)}
|
||||
title="Copy email address"
|
||||
<SendEmailModal
|
||||
emailAddress={selectedEmailAddress}
|
||||
onSuccess={mutate}
|
||||
triggerButton={
|
||||
<Icons.send className="hidden size-5 rounded border p-1 text-primary hover:bg-neutral-200 group-hover:ml-auto group-hover:inline" />
|
||||
}
|
||||
/>
|
||||
<PenLine
|
||||
className="size-5 rounded border p-1 text-primary"
|
||||
className="hidden size-5 rounded border p-1 text-primary hover:bg-neutral-200 group-hover:ml-auto group-hover:inline"
|
||||
onClick={() => handleOpenEditEmail(email)}
|
||||
/>
|
||||
<Icons.trash
|
||||
className="size-5 rounded border p-1 text-primary"
|
||||
className="hidden size-5 rounded border p-1 text-primary hover:bg-neutral-200 group-hover:inline"
|
||||
onClick={() => {
|
||||
setEmailToDelete(email.id);
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
/>
|
||||
<CopyButton
|
||||
value={`${email.emailAddress}`}
|
||||
className={cn(
|
||||
"size-5 rounded border p-1",
|
||||
"duration-250 transition-all hover:bg-neutral-200",
|
||||
)}
|
||||
title="Copy email address"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Icons } from "../shared/icons";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "../ui/drawer";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
|
||||
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
|
||||
|
||||
interface SendEmailModalProps {
|
||||
className?: string;
|
||||
emailAddress: string | null;
|
||||
triggerButton?: React.ReactNode; // 自定义触发按钮
|
||||
onSuccess?: () => void; // 发送成功后的回调
|
||||
}
|
||||
|
||||
export function SendEmailModal({
|
||||
className,
|
||||
emailAddress,
|
||||
triggerButton,
|
||||
onSuccess,
|
||||
}: SendEmailModalProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [sendForm, setSendForm] = useState({ to: "", subject: "", html: "" });
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
if (!emailAddress) {
|
||||
toast.error("No email address selected");
|
||||
return;
|
||||
}
|
||||
if (!sendForm.to || !sendForm.subject || !sendForm.html) {
|
||||
toast.error("Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/email/send", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
from: emailAddress,
|
||||
to: sendForm.to,
|
||||
subject: sendForm.subject,
|
||||
html: sendForm.html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Email sent successfully");
|
||||
setIsOpen(false);
|
||||
setSendForm({ to: "", subject: "", html: "" });
|
||||
onSuccess?.();
|
||||
} else {
|
||||
toast.error("Failed to send email", {
|
||||
description: await response.text(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Error sending email");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{triggerButton ? (
|
||||
<div onClick={() => setIsOpen(true)}>{triggerButton}</div>
|
||||
) : (
|
||||
<Button
|
||||
className={className}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Icons.send size={17} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DrawerContent className="fixed bottom-0 right-0 top-0 w-full rounded-none sm:max-w-xl">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="flex items-center gap-1">
|
||||
Send Email{" "}
|
||||
<Icons.help className="size-5 text-neutral-600 hover:text-neutral-400" />
|
||||
</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost" className="absolute right-4 top-4">
|
||||
<Icons.close className="h-4 w-4" />
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<div className="scrollbar-hidden h-[calc(100vh)] space-y-4 overflow-y-auto p-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
From
|
||||
</label>
|
||||
<Input value={emailAddress || ""} disabled className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
To
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.to}
|
||||
onChange={(e) =>
|
||||
setSendForm({ ...sendForm, to: e.target.value })
|
||||
}
|
||||
placeholder="recipient@example.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Subject
|
||||
</label>
|
||||
<Input
|
||||
value={sendForm.subject}
|
||||
onChange={(e) =>
|
||||
setSendForm({ ...sendForm, subject: e.target.value })
|
||||
}
|
||||
placeholder="Enter subject"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Content
|
||||
</label>
|
||||
<ReactQuill
|
||||
value={sendForm.html}
|
||||
onChange={(value) => setSendForm({ ...sendForm, html: value })}
|
||||
className="mt-1 h-40 rounded-lg"
|
||||
theme="snow"
|
||||
placeholder="Enter your message"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
<Button
|
||||
onClick={handleSendEmail}
|
||||
disabled={isPending}
|
||||
variant="default"
|
||||
>
|
||||
{isPending ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -9,9 +9,9 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
title: "MENU",
|
||||
items: [
|
||||
{ href: "/dashboard", icon: "dashboard", title: "Dashboard" },
|
||||
{ href: "/dashboard/records", icon: "globeLock", title: "DNS Records" },
|
||||
{ href: "/dashboard/urls", icon: "link", title: "Short Urls" },
|
||||
{ href: "/emails", icon: "mail", title: "Emails" },
|
||||
{ href: "/dashboard/records", icon: "globeLock", title: "DNS Records" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,6 +3,14 @@ export const EXPIRATION_ENUMS = [
|
||||
value: "-1",
|
||||
label: "Never",
|
||||
},
|
||||
{
|
||||
value: "10", // 10s
|
||||
label: "10s",
|
||||
},
|
||||
{
|
||||
value: "60", // 1 min
|
||||
label: "60s",
|
||||
},
|
||||
{
|
||||
value: "600", // 10 min
|
||||
label: "10min",
|
||||
@@ -200,6 +208,64 @@ export const reservedAddressSuffix = [
|
||||
"system",
|
||||
"noreply",
|
||||
"no-reply",
|
||||
"info",
|
||||
"contact",
|
||||
"help",
|
||||
"hello",
|
||||
"hi",
|
||||
"inquiries",
|
||||
"feedback",
|
||||
"suggestions",
|
||||
"service",
|
||||
"customerservice",
|
||||
"supportteam",
|
||||
"care",
|
||||
"assistance",
|
||||
"complaints",
|
||||
"sales",
|
||||
"marketing",
|
||||
"business",
|
||||
"partnerships",
|
||||
"advertising",
|
||||
"promo",
|
||||
"deals",
|
||||
"accounts",
|
||||
"payment",
|
||||
"finance",
|
||||
"invoicing",
|
||||
"refunds",
|
||||
"subscriptions",
|
||||
"webmaster",
|
||||
"postmaster",
|
||||
"hostmaster",
|
||||
"tech",
|
||||
"it",
|
||||
"ops",
|
||||
"dev",
|
||||
"developer",
|
||||
"engineering",
|
||||
"privacy",
|
||||
"abuse",
|
||||
"legal",
|
||||
"compliance",
|
||||
"trust",
|
||||
"fraud",
|
||||
"report",
|
||||
"news",
|
||||
"updates",
|
||||
"alerts",
|
||||
"notifications",
|
||||
"welcome",
|
||||
"verify",
|
||||
"confirmation",
|
||||
"team",
|
||||
"staff",
|
||||
"hr",
|
||||
"jobs",
|
||||
"careers",
|
||||
"press",
|
||||
"media",
|
||||
"events",
|
||||
];
|
||||
|
||||
export const LOGS_LIMITEs_ENUMS = [
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user