chore: better url list
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,4 +40,6 @@ next-env.d.ts
|
|||||||
/.react-email/
|
/.react-email/
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
.contentlayer
|
.contentlayer
|
||||||
|
|
||||||
|
public/sw.js.map
|
||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user