chore: better url list

This commit is contained in:
oiov
2025-11-01 17:01:22 +08:00
parent e935adf4b6
commit 2ddafd6aec
19 changed files with 182 additions and 163 deletions

4
.gitignore vendored
View File

@@ -40,4 +40,6 @@ next-env.d.ts
/.react-email/ /.react-email/
.vscode .vscode
.contentlayer .contentlayer
public/sw.js.map

View File

@@ -81,7 +81,7 @@ export default function DomainList({ user, action }: DomainListProps) {
const [currentEditDomain, setCurrentEditDomain] = const [currentEditDomain, setCurrentEditDomain] =
useState<DomainFormData | null>(null); useState<DomainFormData | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
slug: "", slug: "",
target: "", target: "",

View File

@@ -73,7 +73,7 @@ export default function PlanList({ user, action }: PlanListProps) {
const [currentEditPlan, setCurrentEditPlan] = const [currentEditPlan, setCurrentEditPlan] =
useState<PlanQuotaFormData | null>(null); useState<PlanQuotaFormData | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
slug: "", slug: "",
target: "", target: "",

View File

@@ -1,13 +1,8 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() { export default function DashboardUrlsLoading() {
return ( return (
<> <>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
/>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
<Skeleton className="h-[102px] w-full rounded-lg" /> <Skeleton className="h-[102px] w-full rounded-lg" />
<Skeleton className="h-[102px] w-full rounded-lg" /> <Skeleton className="h-[102px] w-full rounded-lg" />

View File

@@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session"; import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils"; import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import UserUrlsList from "../../dashboard/urls/url-list"; import UserUrlsList from "../../dashboard/urls/url-list";
@@ -18,12 +17,6 @@ export default async function DashboardPage() {
return ( return (
<> <>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
link="/docs/short-urls"
linkText="short urls"
/>
<UserUrlsList <UserUrlsList
user={{ user={{
id: user.id, id: user.id,

View File

@@ -78,7 +78,7 @@ export default function UsersList({ user }: UrlListProps) {
const [isShowForm, setShowForm] = useState(false); const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null); const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
email: "", email: "",
userName: "", userName: "",

View File

@@ -88,7 +88,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
const [currentEditRecord, setCurrentEditRecord] = const [currentEditRecord, setCurrentEditRecord] =
useState<UserRecordFormData | null>(null); useState<UserRecordFormData | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const isAdmin = action.includes("/admin"); const isAdmin = action.includes("/admin");
const t = useTranslations("List"); const t = useTranslations("List");
@@ -154,7 +154,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
<div className="grid gap-2"> <div className="grid gap-2">
<CardTitle>{t("Subdomain List")}</CardTitle> <CardTitle>{t("Subdomain List")}</CardTitle>
<CardDescription className="hidden text-balance sm:block"> <CardDescription className="hidden text-balance sm:block">
{t("Before using please read the")}{" "} {t("Before using please read the")}
<Link <Link
target="_blank" target="_blank"
className="font-semibold text-yellow-600 after:content-['↗'] hover:underline" className="font-semibold text-yellow-600 after:content-['↗'] hover:underline"
@@ -162,14 +162,14 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
> >
{t("legitimacy review")} {t("legitimacy review")}
</Link> </Link>
. {t("See")}{" "} . {t("See")}
<Link <Link
target="_blank" target="_blank"
className="text-blue-500 hover:underline" className="text-blue-500 hover:underline"
href="/docs/examples/vercel" href="/docs/examples/vercel"
> >
{t("examples")} {t("examples")}
</Link>{" "} </Link>
{t("for more usage")}. {t("for more usage")}.
</CardDescription> </CardDescription>
</div> </div>

View File

@@ -157,7 +157,10 @@ export const UrlExporter: React.FC<{
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="flex items-center gap-2" variant={"outline"}> <Button
className="flex items-center gap-2 text-nowrap"
variant={"outline"}
>
{t("Export")} {t("Export")}
<Icons.chevronDown className="size-4" /> <Icons.chevronDown className="size-4" />
</Button> </Button>

View File

@@ -1,13 +1,8 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() { export default function DashboardUrlsLoading() {
return ( return (
<> <>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
/>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
<Skeleton className="h-[102px] w-full rounded-lg" /> <Skeleton className="h-[102px] w-full rounded-lg" />
<Skeleton className="h-[102px] w-full rounded-lg" /> <Skeleton className="h-[102px] w-full rounded-lg" />

View File

@@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session"; import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils"; import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import UserUrlsList from "./url-list"; import UserUrlsList from "./url-list";
@@ -18,12 +17,6 @@ export default async function DashboardPage() {
return ( return (
<> <>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
link="/docs/short-urls"
linkText="short urls"
/>
<UserUrlsList <UserUrlsList
user={{ user={{
id: user.id, id: user.id,

View File

@@ -30,6 +30,13 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal"; import { Modal } from "@/components/ui/modal";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -113,7 +120,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
null, null,
); );
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [isShowStats, setShowStats] = useState(false); const [isShowStats, setShowStats] = useState(false);
const [isShowQrcode, setShowQrcode] = useState(false); const [isShowQrcode, setShowQrcode] = useState(false);
const [selectedUrl, setSelectedUrl] = useState<ShortUrlFormData | null>(null); const [selectedUrl, setSelectedUrl] = useState<ShortUrlFormData | null>(null);
@@ -197,80 +204,101 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</EmptyPlaceholder> </EmptyPlaceholder>
); );
const rendeSeachInputs = () => ( const renderSearchInputs = () => {
<div className="mb-2 flex-row items-center gap-2 space-y-2 sm:flex sm:space-y-0"> const [searchType, setSearchType] = useState<
<div className="relative w-full"> "slug" | "target" | "userName"
<Input >("slug");
className="h-8 text-xs md:text-xs"
placeholder={t("Search by slug") + "..."}
value={searchParams.slug}
onChange={(e) => {
setSearchParams({
...searchParams,
slug: e.target.value,
});
}}
/>
{searchParams.slug && (
<Button
className="absolute right-2 top-1/2 h-6 -translate-y-1/2 rounded-full px-1 text-gray-500 hover:text-gray-700"
onClick={() => setSearchParams({ ...searchParams, slug: "" })}
variant={"ghost"}
>
<Icons.close className="size-3" />
</Button>
)}
</div>
<div className="relative w-full"> const getCurrentSearchValue = () => {
<Input switch (searchType) {
className="h-8 text-xs md:text-xs" case "slug":
placeholder={t("Search by target") + "..."} return searchParams.slug;
value={searchParams.target} case "target":
onChange={(e) => { return searchParams.target;
setSearchParams({ case "userName":
...searchParams, return searchParams.userName;
target: e.target.value, default:
}); return "";
}} }
/> };
{searchParams.target && (
<Button
className="absolute right-2 top-1/2 h-6 -translate-y-1/2 rounded-full px-1 text-gray-500 hover:text-gray-700"
onClick={() => setSearchParams({ ...searchParams, target: "" })}
variant={"ghost"}
>
<Icons.close className="size-3" />
</Button>
)}
</div>
{user.role === "ADMIN" && ( const handleSearchChange = (value: string) => {
<div className="relative w-full"> setSearchParams({
...searchParams,
slug: searchType === "slug" ? value : "",
target: searchType === "target" ? value : "",
userName: searchType === "userName" ? value : "",
});
};
const handleClearSearch = () => {
handleSearchChange("");
};
const getPlaceholder = () => {
switch (searchType) {
case "slug":
return t("Search by slug") + "...";
case "target":
return t("Search by target") + "...";
case "userName":
return t("Search by username") + "...";
default:
return t("Search") + "...";
}
};
const searchOptions = [
{ value: "slug", label: t("Link Slug") },
{ value: "target", label: t("Link Target") },
...(user.role === "ADMIN"
? [{ value: "userName", label: t("Username") }]
: []),
];
const currentSearchValue = getCurrentSearchValue();
return (
<div className="ml-auto flex items-center">
<Select
value={searchType}
onValueChange={(value: typeof searchType) => setSearchType(value)}
>
<SelectTrigger className="h-10 w-[85px] rounded-r-none bg-muted text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{searchOptions.map((option) => (
<SelectItem
key={option.value}
value={option.value}
className="text-sm"
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative flex-1">
<Input <Input
className="h-8 text-xs md:text-xs" className="h-10 rounded-l-none border-l-0 pr-8 text-sm"
placeholder={t("Search by username") + "..."} placeholder={getPlaceholder()}
value={searchParams.userName} value={currentSearchValue}
onChange={(e) => { onChange={(e) => handleSearchChange(e.target.value)}
setSearchParams({
...searchParams,
userName: e.target.value,
});
}}
/> />
{searchParams.userName && ( {currentSearchValue && (
<Button <Button
className="absolute right-2 top-1/2 h-6 -translate-y-1/2 rounded-full px-1 text-gray-500 hover:text-gray-700" className="absolute right-2 top-1/2 h-6 -translate-y-1/2 rounded-full px-1 text-gray-500 hover:text-gray-700"
onClick={() => setSearchParams({ ...searchParams, userName: "" })} onClick={handleClearSearch}
variant={"ghost"} variant="ghost"
> >
<Icons.close className="size-3" /> <Icons.close className="size-3" />
</Button> </Button>
)} )}
</div> </div>
)} </div>
</div> );
); };
const rendeClicks = (short: ShortUrlFormData) => ( const rendeClicks = (short: ShortUrlFormData) => (
<> <>
@@ -650,13 +678,16 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
return ( return (
<> <>
<Tabs <Tabs
className={cn("rounded-lg", pathname === "/dashboard" && "border p-6")} className={cn(
"space-y-3 rounded-lg",
pathname === "/dashboard" && "border p-6",
)}
value={currentView} value={currentView}
> >
{/* Tabs */} {/* Tabs */}
<div className="mb-4 flex items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
{pathname === "/dashboard" && ( {pathname === "/dashboard" && (
<h2 className="mr-3 text-lg font-semibold">{t("Short URLs")}</h2> <h2 className="mr-auto text-lg font-semibold">{t("Short URLs")}</h2>
)} )}
<TabsList> <TabsList>
<TabsTrigger onClick={() => setCurrentView("List")} value="List"> <TabsTrigger onClick={() => setCurrentView("List")} value="List">
@@ -678,8 +709,8 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</TabsTrigger> </TabsTrigger>
)} )}
</TabsList> </TabsList>
{/* <p>Total: {data?.total || 0}</p> */} <div className="flex items-center justify-end gap-3">
<div className="ml-auto flex items-center justify-end gap-3"> {renderSearchInputs()}
<UrlExporter data={data?.list || []} /> <UrlExporter data={data?.list || []} />
<Button <Button
variant={"outline"} variant={"outline"}
@@ -710,14 +741,12 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div> </div>
</div> </div>
<TabsContent className="space-y-3" value="List"> {pathname !== "/dashboard" && <UrlStatus action={action} />}
{pathname !== "/dashboard" && <UrlStatus action={action} />}
{rendeSeachInputs()} <TabsContent className="mt-0 space-y-3" value="List">
{rendeList()} {rendeList()}
</TabsContent> </TabsContent>
<TabsContent className="space-y-3" value="Grid"> <TabsContent className="mt-0 space-y-3" value="Grid">
{pathname !== "/dashboard" && <UrlStatus action={action} />}
{rendeSeachInputs()}
{rendeGrid()} {rendeGrid()}
</TabsContent> </TabsContent>
{selectedUrl?.id && ( {selectedUrl?.id && (

View File

@@ -14,5 +14,5 @@ export default async function SentEmailPage() {
if (!user?.id) redirect("/login"); if (!user?.id) redirect("/login");
return <SendsEmailList />; return <SendsEmailList user={user as any} />;
} }

View File

@@ -54,7 +54,7 @@ export default function EmailList({
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [isAutoRefresh, setIsAutoRefresh] = useState(false); const [isAutoRefresh, setIsAutoRefresh] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [selectedEmails, setSelectedEmails] = useState<string[]>([]); const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
const [showMutiCheckBox, setShowMutiCheckBox] = useState(false); const [showMutiCheckBox, setShowMutiCheckBox] = useState(false);

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useMemo, useState, useTransition } from "react"; import { useEffect, useState, useTransition } from "react";
import { User, UserEmail } from "@prisma/client"; import { User, UserEmail } from "@prisma/client";
import randomName from "@scaleway/random-name"; import randomName from "@scaleway/random-name";
import { import {
@@ -45,7 +45,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "../ui/tooltip"; } from "../ui/tooltip";
import { SendEmailModal } from "./SendEmailModal"; import { SendEmailModal } from "./SendEmailModal";
import SendsEmailList from "./SendsEmailList";
interface EmailSidebarProps { interface EmailSidebarProps {
user: User; user: User;
@@ -83,7 +82,7 @@ export default function EmailSidebar({
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [onlyUnread, setOnlyUnread] = useState(false); const [onlyUnread, setOnlyUnread] = useState(false);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const { data, isLoading, error, mutate } = useSWR<{ const { data, isLoading, error, mutate } = useSWR<{
list: UserEmailList[]; list: UserEmailList[];
@@ -338,6 +337,7 @@ export default function EmailSidebar({
"bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700": "bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-gray-700":
onlyUnread, onlyUnread,
}, },
{ "col-span-2": user.role !== "ADMIN" },
)} )}
onClick={() => { onClick={() => {
setOnlyUnread(!onlyUnread); setOnlyUnread(!onlyUnread);
@@ -363,28 +363,30 @@ export default function EmailSidebar({
</div> </div>
{/* Admin Mode */} {/* Admin Mode */}
<div {user.role === "ADMIN" && (
onClick={() => setAdminModel(!isAdminModel)} <div
className={cn( onClick={() => setAdminModel(!isAdminModel)}
"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", 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, "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" /> <div className="flex items-center gap-1">
<p className="line-clamp-1 text-start font-medium"> <Icons.lock className="size-3" />
{t("Admin Mode")} <p className="line-clamp-1 text-start font-medium">
</p> {t("Admin Mode")}
</p>
</div>
<Switch
className="scale-90"
checked={isAdminModel}
onCheckedChange={(v) => setAdminModel(v)}
/>
</div> </div>
<Switch )}
className="scale-90"
checked={isAdminModel}
onCheckedChange={(v) => setAdminModel(v)}
/>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,12 +1,11 @@
"use client"; "use client";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { UserSendEmail } from "@prisma/client"; import { User, UserSendEmail } from "@prisma/client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useSWR from "swr"; import useSWR from "swr";
import { cn, fetcher, formatDate, htmlToText, nFormatter } 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 { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@@ -19,9 +18,9 @@ import {
} from "../ui/collapsible"; } from "../ui/collapsible";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
export default function SendsEmailList({}: {}) { export default function SendsEmailList({ user }: { user: User }) {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(15);
const [isAdminModel, setAdminModel] = useState(false); const [isAdminModel, setAdminModel] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -72,28 +71,30 @@ export default function SendsEmailList({}: {}) {
</div> </div>
{/* Admin Mode */} {/* Admin Mode */}
<div {user.role === "ADMIN" && (
onClick={() => setAdminModel(!isAdminModel)} <div
className={cn( onClick={() => setAdminModel(!isAdminModel)}
"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", 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, "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" /> <div className="flex items-center gap-1">
<p className="line-clamp-1 text-start font-medium"> <Icons.lock className="size-3" />
{t("Admin Mode")} <p className="line-clamp-1 text-start font-medium">
</p> {t("Admin Mode")}
</p>
</div>
<Switch
className="scale-90"
checked={isAdminModel}
onCheckedChange={(v) => setAdminModel(v)}
/>
</div> </div>
<Switch )}
className="scale-90"
checked={isAdminModel}
onCheckedChange={(v) => setAdminModel(v)}
/>
</div>
</div> </div>
<div className="mb-4 flex items-center justify-between gap-4"> <div className="mb-4 flex items-center justify-between gap-4">
<Input <Input

View File

@@ -40,7 +40,7 @@ export function PaginationWrapper({
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
}) { }) {
// Page size options // Page size options
const pageSizeOptions = [10, 20, 50, 100]; const pageSizeOptions = [10, 15, 20, 50, 100];
// Calculate total pages based on pageSize // Calculate total pages based on pageSize
const totalPages = Math.ceil(total / pageSize); const totalPages = Math.ceil(total / pageSize);

View File

@@ -233,7 +233,10 @@
"Email Service Configs": "Email Service Configs", "Email Service Configs": "Email Service Configs",
"Email Provider": "Email Provider", "Email Provider": "Email Provider",
"Resend API Key": "Resend API Key", "Resend API Key": "Resend API Key",
"Brevo API Key": "Brevo API Key" "Brevo API Key": "Brevo API Key",
"Link Slug": "Slug",
"Link Target": "Target",
"Username": "Username"
}, },
"Components": { "Components": {
"Dashboard": "Dashboard", "Dashboard": "Dashboard",

View File

@@ -233,7 +233,10 @@
"Email Service Configs": "邮件服务商", "Email Service Configs": "邮件服务商",
"Email Provider": "邮件提供商", "Email Provider": "邮件提供商",
"Resend API Key": "Resend 密钥", "Resend API Key": "Resend 密钥",
"Brevo API Key": "Brevo 密钥" "Brevo API Key": "Brevo 密钥",
"Link Slug": "后缀",
"Link Target": "目标链接",
"Username": "用户名"
}, },
"Components": { "Components": {
"Dashboard": "用户面板", "Dashboard": "用户面板",

File diff suppressed because one or more lines are too long