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

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ next-env.d.ts
.vscode
.contentlayer
public/sw.js.map

View File

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

View File

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

View File

@@ -1,13 +1,8 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() {
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">
<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 { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import UserUrlsList from "../../dashboard/urls/url-list";
@@ -18,12 +17,6 @@ export default async function DashboardPage() {
return (
<>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
link="/docs/short-urls"
linkText="short urls"
/>
<UserUrlsList
user={{
id: user.id,

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,8 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() {
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">
<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 { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import UserUrlsList from "./url-list";
@@ -18,12 +17,6 @@ export default async function DashboardPage() {
return (
<>
<DashboardHeader
heading="Manage Short URLs"
text="List and manage short urls"
link="/docs/short-urls"
linkText="short urls"
/>
<UserUrlsList
user={{
id: user.id,

View File

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

View File

@@ -14,5 +14,5 @@ export default async function SentEmailPage() {
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 [isAutoRefresh, setIsAutoRefresh] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [pageSize, setPageSize] = useState(15);
const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
const [showMutiCheckBox, setShowMutiCheckBox] = useState(false);

View File

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

View File

@@ -1,12 +1,11 @@
"use client";
import { useCallback, useState } from "react";
import { UserSendEmail } from "@prisma/client";
import { User, UserSendEmail } from "@prisma/client";
import { useTranslations } from "next-intl";
import useSWR from "swr";
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";
@@ -19,9 +18,9 @@ import {
} from "../ui/collapsible";
import { Switch } from "../ui/switch";
export default function SendsEmailList({}: {}) {
export default function SendsEmailList({ user }: { user: User }) {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [pageSize, setPageSize] = useState(15);
const [isAdminModel, setAdminModel] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -72,28 +71,30 @@ export default function SendsEmailList({}: {}) {
</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>
{user.role === "ADMIN" && (
<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>
<Switch
className="scale-90"
checked={isAdminModel}
onCheckedChange={(v) => setAdminModel(v)}
/>
</div>
)}
</div>
<div className="mb-4 flex items-center justify-between gap-4">
<Input

View File

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

View File

@@ -233,7 +233,10 @@
"Email Service Configs": "Email Service Configs",
"Email Provider": "Email Provider",
"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": {
"Dashboard": "Dashboard",

View File

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

File diff suppressed because one or more lines are too long