chore: split email module
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
|
||||
9
app/emails/sent/loading.tsx
Normal file
9
app/emails/sent/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="h-full w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
app/emails/sent/page.tsx
Normal file
18
app/emails/sent/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
import SendsEmailList from "@/components/email/SendsEmailList";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Emails",
|
||||
description: "List and manage emails.",
|
||||
});
|
||||
|
||||
export default async function SentEmailPage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (!user?.id) redirect("/login");
|
||||
|
||||
return <SendsEmailList />;
|
||||
}
|
||||
@@ -83,8 +83,6 @@ export default function EmailSidebar({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [onlyUnread, setOnlyUnread] = useState(false);
|
||||
|
||||
const [showSendsModal, setShowSendsModal] = useState(false);
|
||||
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const { data, isLoading, error, mutate } = useSWR<{
|
||||
@@ -98,14 +96,6 @@ export default function EmailSidebar({
|
||||
{ dedupingInterval: 5000 },
|
||||
);
|
||||
|
||||
const { data: sendEmails } = useSWR<number>(
|
||||
`/api/email/send?all=${isAdminModel}`,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: emailDomains, isLoading: isLoadingDomains } = useSWR<
|
||||
{ domain_name: string; min_email_length: number }[]
|
||||
>("/api/domain?feature=email", fetcher, {
|
||||
@@ -372,39 +362,31 @@ export default function EmailSidebar({
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* Sent Emails */}
|
||||
{/* Admin Mode */}
|
||||
<div
|
||||
onClick={() => setShowSendsModal(!showSendsModal)}
|
||||
onClick={() => setAdminModel(!isAdminModel)}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-col items-center gap-1 rounded-md bg-neutral-100 px-1 pb-1 pt-2 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700",
|
||||
{
|
||||
"bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700":
|
||||
showSendsModal,
|
||||
isAdminModel,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.send className="size-3" />
|
||||
<Icons.lock className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
{t("Sent Emails")}
|
||||
{t("Admin Mode")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{nFormatter(sendEmails || 0)}
|
||||
</p>
|
||||
<Switch
|
||||
className="scale-90"
|
||||
checked={isAdminModel}
|
||||
onCheckedChange={(v) => setAdminModel(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCollapsed && user.role === "ADMIN" && (
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
{t("Admin Mode")}:{" "}
|
||||
<Switch
|
||||
defaultChecked={isAdminModel}
|
||||
onCheckedChange={(v) => setAdminModel(v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="scrollbar-hidden flex-1 overflow-y-scroll">
|
||||
@@ -569,16 +551,6 @@ export default function EmailSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSendsModal && (
|
||||
<Modal
|
||||
className="md:max-w-2xl"
|
||||
showModal={showSendsModal}
|
||||
setShowModal={setShowSendsModal}
|
||||
>
|
||||
<SendsEmailList isAdminModel={isAdminModel} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 创建\编辑邮箱的 Modal */}
|
||||
{showEmailModal && (
|
||||
<Modal showModal={showEmailModal} setShowModal={setShowEmailModal}>
|
||||
|
||||
@@ -5,30 +5,37 @@ import { UserSendEmail } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { fetcher, formatDate, htmlToText } from "@/lib/utils";
|
||||
import { cn, fetcher, formatDate, htmlToText, nFormatter } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { Icons } from "../shared/icons";
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
export default function SendsEmailList({
|
||||
isAdminModel,
|
||||
}: {
|
||||
isAdminModel: boolean;
|
||||
}) {
|
||||
export default function SendsEmailList({}: {}) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [isAdminModel, setAdminModel] = useState(false);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const t = useTranslations("Email");
|
||||
|
||||
const { data: sendEmails } = useSWR<number>(
|
||||
`/api/email/send?all=${isAdminModel}`,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, isLoading, error } = useSWR<{
|
||||
list: UserSendEmail[];
|
||||
total: number;
|
||||
@@ -50,87 +57,126 @@ export default function SendsEmailList({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-4xl border-none">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("Sent Emails")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-2 flex items-center justify-between gap-4">
|
||||
<Input
|
||||
placeholder={t("Search by send to email")}
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
className="w-full bg-neutral-50"
|
||||
<div className="h-[calc(100vh-60px)] w-full overflow-auto p-4 xl:p-8">
|
||||
<div className="mb-4 grid grid-cols-2 gap-2 rounded-lg text-xs text-neutral-700 dark:bg-neutral-900 dark:text-neutral-400">
|
||||
<div className="flex flex-col items-center gap-1 rounded-md bg-neutral-100 px-1 pb-1 pt-2 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.send className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
{t("Sent Emails")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{nFormatter(sendEmails ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Admin Mode */}
|
||||
<div
|
||||
onClick={() => setAdminModel(!isAdminModel)}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-col items-center gap-1 rounded-md bg-neutral-100 px-1 pb-1 pt-2 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700",
|
||||
{
|
||||
"bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700":
|
||||
isAdminModel,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icons.lock className="size-3" />
|
||||
<p className="line-clamp-1 text-start font-medium">
|
||||
{t("Admin Mode")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
className="scale-90"
|
||||
checked={isAdminModel}
|
||||
onCheckedChange={(v) => setAdminModel(v)}
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between gap-4">
|
||||
<Input
|
||||
placeholder={t("Search by send to email")}
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
className="w-full bg-neutral-50"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Search by from email")}
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
className="w-full bg-neutral-50"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="space-y-1.5">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-red-500">
|
||||
{t("Failed to load emails")}. {t("Please try again")}.
|
||||
</div>
|
||||
) : !data || data.list.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
{t("No emails found")}.
|
||||
</div>
|
||||
) : (
|
||||
<div className="scrollbar-hidden overflow-y-auto">
|
||||
<div className="space-y-1.5">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-red-500">
|
||||
{t("Failed to load emails")}. {t("Please try again")}.
|
||||
</div>
|
||||
) : !data || data.list.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
{t("No emails found")}.
|
||||
</div>
|
||||
) : (
|
||||
<div className="scrollbar-hidden max-h-[50vh] overflow-y-auto">
|
||||
<div className="space-y-1.5">
|
||||
{data.list.map((email) => (
|
||||
<Collapsible
|
||||
className="w-full rounded-lg border bg-white transition-all duration-200 hover:bg-gray-50"
|
||||
key={email.id}
|
||||
>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="grids flex items-center justify-between rounded-t-lg bg-neutral-300/70 px-2 py-1.5">
|
||||
<span className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
{email.from}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-800 dark:text-neutral-300">
|
||||
{formatDate(email.createdAt as any)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 gap-3 p-2 sm:grid-cols-2">
|
||||
<div className="text-start">
|
||||
<div className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
<strong>Send To:</strong> {email.to}
|
||||
</div>
|
||||
<p className="line-clamp-1 truncate text-xs font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<strong>Subject:</strong>{" "}
|
||||
{email.subject || "No subject"}
|
||||
</p>
|
||||
{data.list.map((email) => (
|
||||
<Collapsible
|
||||
className="w-full rounded-lg border bg-primary-foreground transition-all duration-200 hover:bg-neutral-100 dark:hover:bg-neutral-500"
|
||||
key={email.id}
|
||||
>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="grids flex items-center justify-between rounded-t-lg bg-neutral-300/70 px-2 py-1.5">
|
||||
<span className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
{email.from}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-800 dark:text-neutral-300">
|
||||
{formatDate(email.createdAt as any)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 gap-3 p-2 sm:grid-cols-2">
|
||||
<div className="text-start">
|
||||
<div className="truncate text-xs font-semibold text-neutral-600 dark:text-neutral-200">
|
||||
<strong>Send To:</strong> {email.to}
|
||||
</div>
|
||||
<p className="line-clamp-2 break-all text-start text-xs text-neutral-500">
|
||||
{htmlToText(email.html || "")}
|
||||
<p className="line-clamp-1 truncate text-xs font-semibold text-neutral-600 dark:text-neutral-400">
|
||||
<strong>Subject:</strong>{" "}
|
||||
{email.subject || "No subject"}
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="animate-fade-in break-all border-t border-dashed p-2 text-sm text-neutral-500">
|
||||
<p className="line-clamp-2 break-all text-start text-xs text-neutral-500">
|
||||
{htmlToText(email.html || "")}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
{data && totalPages > 1 && (
|
||||
<PaginationWrapper
|
||||
className="m-0 mt-6"
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
layout="split"
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="animate-fade-in break-all border-t border-dashed p-2 text-sm text-neutral-500 dark:text-neutral-100">
|
||||
{htmlToText(email.html || "")}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{data && totalPages > 1 && (
|
||||
<PaginationWrapper
|
||||
className="m-0 mt-6"
|
||||
total={data.total}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
layout="split"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
}, [path, links]);
|
||||
|
||||
const renderNavItem = (item: NavItem, isNested = false) => {
|
||||
const Icon = item.icon ? Icons[item.icon] : () => null;
|
||||
const Icon = Icons[item.icon ?? "arrowLeft"];
|
||||
const hasSubItems = item.items && item.items.length > 0;
|
||||
const isOpen = openCollapsibles.has(item.title);
|
||||
|
||||
@@ -96,13 +96,14 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
>
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"flex w-full items-center rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"text-muted-foreground hover:text-accent-foreground",
|
||||
item.disabled &&
|
||||
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
|
||||
item.icon ? "gap-3" : "pl-6",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
<Icon className={item.icon ? "size-5" : "hidden"} />
|
||||
{t(item.title)}
|
||||
<Icons.chevronDown
|
||||
className={cn(
|
||||
@@ -167,16 +168,16 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
|
||||
key={`link-${item.title}`}
|
||||
href={item.disabled ? "#" : item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"flex items-center rounded-md text-sm hover:bg-muted",
|
||||
path === item.href
|
||||
? "bg-muted"
|
||||
: "text-muted-foreground hover:text-accent-foreground",
|
||||
item.disabled &&
|
||||
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
|
||||
isNested && "pl-6",
|
||||
isNested ? "py-1" : "gap-3 p-2 font-medium",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
<Icon className={item.icon ? "size-5" : "opacity-0"} />
|
||||
{t(item.title)}
|
||||
{item.badge && (
|
||||
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||
@@ -350,7 +351,7 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
}, [path, links]);
|
||||
|
||||
const renderMobileNavItem = (item: NavItem, isNested = false) => {
|
||||
const Icon = item.icon ? Icons[item.icon] : () => null;
|
||||
const Icon = Icons[item.icon ?? "arrowLeft"];
|
||||
const hasSubItems = item.items && item.items.length > 0;
|
||||
const isOpen = openCollapsibles.has(item.title);
|
||||
|
||||
@@ -364,13 +365,14 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
>
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"flex w-full items-center rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"text-muted-foreground hover:text-accent-foreground",
|
||||
item.disabled &&
|
||||
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
|
||||
item.icon ? "gap-3" : "pl-6",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
<Icon className={item.icon ? "size-5" : "hidden"} />
|
||||
{t(item.title)}
|
||||
<Icons.chevronDown
|
||||
className={cn(
|
||||
@@ -399,16 +401,16 @@ export function MobileSheetSidebar({ links }: DashboardSidebarProps) {
|
||||
}}
|
||||
href={item.disabled ? "#" : item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-md p-2 text-sm font-medium hover:bg-muted",
|
||||
"flex items-center rounded-md text-sm hover:bg-muted",
|
||||
path === item.href
|
||||
? "bg-muted"
|
||||
: "text-muted-foreground hover:text-accent-foreground",
|
||||
item.disabled &&
|
||||
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
|
||||
isNested && "pl-6",
|
||||
isNested ? "py-1.5" : "gap-3 p-2 font-medium",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
<Icon className={item.icon ? "size-5" : "opacity-0"} />
|
||||
{t(item.title)}
|
||||
{item.badge && (
|
||||
<Badge className="ml-auto flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||
|
||||
@@ -27,7 +27,7 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
title: "Emails",
|
||||
items: [
|
||||
{ href: "/emails", title: "Inbox" },
|
||||
{ href: "/emails/sent", title: "Sent", disabled: true },
|
||||
{ href: "/emails/sent", title: "Sent" },
|
||||
{ href: "/emails/trash", title: "Trash", disabled: true },
|
||||
{ href: "/emails/api", title: "API", disabled: true },
|
||||
],
|
||||
@@ -90,7 +90,12 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
title: "Admin Panel",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
|
||||
{
|
||||
href: "/admin/users",
|
||||
icon: "users",
|
||||
title: "Users",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/resources",
|
||||
icon: "boxes",
|
||||
@@ -98,16 +103,46 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
items: [
|
||||
{
|
||||
href: "/admin/users",
|
||||
title: "Users",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls",
|
||||
// icon: "link",
|
||||
href: "",
|
||||
title: "URLs",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
items: [
|
||||
{
|
||||
href: "/admin/urls",
|
||||
title: "List",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls/analytics",
|
||||
title: "Analytics",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls/logs",
|
||||
title: "Ip Logs",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// href: "/admin/emails",
|
||||
// // icon: "globe",
|
||||
// title: "Emails",
|
||||
// authorizeOnly: UserRole.ADMIN,
|
||||
// items: [
|
||||
// {
|
||||
// href: "/admin/emails/sent",
|
||||
// title: "Sent",
|
||||
// authorizeOnly: UserRole.ADMIN,
|
||||
// },
|
||||
// {
|
||||
// href: "/admin/emails/trash",
|
||||
// title: "Trash",
|
||||
// authorizeOnly: UserRole.ADMIN,
|
||||
// disabled: true,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
href: "/admin/records",
|
||||
// icon: "globe",
|
||||
@@ -120,16 +155,6 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
title: "Cloud Storage Manage",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls/analytics",
|
||||
title: "Analytics",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls/logs",
|
||||
title: "Ip Logs",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -605,7 +605,8 @@
|
||||
"Storage": "Storage",
|
||||
"Resources": "Resources",
|
||||
"App Configs": "App Settings",
|
||||
"Plans": "Plan Settings"
|
||||
"Plans": "Plan Settings",
|
||||
"List": "List"
|
||||
},
|
||||
"Email": {
|
||||
"Search emails": "Search emails",
|
||||
@@ -660,7 +661,8 @@
|
||||
"Sending": "Sending",
|
||||
"Reply-To": "Reply-To",
|
||||
"Attachments": "Attachments",
|
||||
"Date": "Date"
|
||||
"Date": "Date",
|
||||
"Search by from email": "Search by from email"
|
||||
},
|
||||
"Scrape": {
|
||||
"Playground": "Playground",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"Spam": "垃圾邮件",
|
||||
"Trash": "废纸篓",
|
||||
"API": "API",
|
||||
"Links": "我的链接",
|
||||
"Links": "链接管理",
|
||||
"Analytics": "访客统计",
|
||||
"Ip Logs": "实时日志",
|
||||
"APIs": "APIs",
|
||||
@@ -604,7 +604,8 @@
|
||||
"Storage": "存储桶",
|
||||
"Resources": "资源管理",
|
||||
"App Configs": "全局配置",
|
||||
"Plans": "配额设置"
|
||||
"Plans": "配额设置",
|
||||
"List": "用户链接"
|
||||
},
|
||||
"Email": {
|
||||
"Search emails": "搜索邮箱...",
|
||||
@@ -659,7 +660,8 @@
|
||||
"Sending": "发送中...",
|
||||
"Reply-To": "回复",
|
||||
"Attachments": "附件",
|
||||
"Date": "日期"
|
||||
"Date": "日期",
|
||||
"Search by from email": "搜索发件人邮箱"
|
||||
},
|
||||
"Scrape": {
|
||||
"Playground": "在线体验",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user