Files
wr.do/app/(protected)/admin/users/user-list.tsx
2025-11-01 17:01:22 +08:00

330 lines
13 KiB
TypeScript

"use client";
import { useState } from "react";
import { User } from "@prisma/client";
import { PenLine } from "lucide-react";
import { useTranslations } from "next-intl";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { FormType, UserForm } from "@/components/forms/user-form";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { Icons } from "@/components/shared/icons";
import { PaginationWrapper } from "@/components/shared/pagination";
import { TimeAgoIntl } from "@/components/shared/time-ago";
export interface UrlListProps {
user: Pick<User, "id" | "name">;
}
function TableColumnSekleton({ className }: { className?: string }) {
return (
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-8">
<TableCell className="col-span-1">
<Skeleton className="h-5 w-20" />
</TableCell>
<TableCell className="col-span-1 sm:col-span-2">
<Skeleton className="h-5 w-20" />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Skeleton className="h-5 w-16" />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Skeleton className="h-5 w-16" />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Skeleton className="h-5 w-16" />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Skeleton className="h-5 w-16" />
</TableCell>
<TableCell className="col-span-1 flex justify-center">
<Skeleton className="h-5 w-16" />
</TableCell>
</TableRow>
);
}
export default function UsersList({ user }: UrlListProps) {
const { isMobile } = useMediaQuery();
const [formType, setFormType] = useState<FormType>("add");
const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
const [searchParams, setSearchParams] = useState({
email: "",
userName: "",
});
const t = useTranslations("List");
const { mutate } = useSWRConfig();
const { data, isLoading } = useSWR<{ total: number; list: User[] }>(
`/api/user/admin?page=${currentPage}&size=${pageSize}&email=${searchParams.email}&userName=${searchParams.userName}`,
fetcher,
{
revalidateOnFocus: false,
},
);
const handleRefresh = () => {
mutate(
`/api/user/admin?page=${currentPage}&size=${pageSize}&email=${searchParams.email}&userName=${searchParams.userName}`,
undefined,
);
};
return (
<>
<Card className="xl:col-span-2">
<CardHeader className="flex flex-row items-center">
<CardDescription className="text-balance text-lg font-bold">
<span>{t("Total Users")}:</span>{" "}
<span className="font-bold">{data && data.total}</span>
</CardDescription>
<div className="ml-auto flex items-center justify-end gap-3">
<Button
variant={"outline"}
onClick={() => handleRefresh()}
disabled={isLoading}
>
{isLoading ? (
<Icons.refreshCw className="size-4 animate-spin" />
) : (
<Icons.refreshCw className="size-4" />
)}
</Button>
<Button
className="flex shrink-0 gap-1"
variant="default"
onClick={() => {
setcurrentEditUser(null);
setShowForm(false);
setFormType("add");
setShowForm(!isShowForm);
}}
>
<Icons.add className="size-4" />
<span className="hidden sm:inline">{t("Add User")}</span>
</Button>
</div>
</CardHeader>
<CardContent>
<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="Search by email..."
value={searchParams.email}
onChange={(e) => {
setSearchParams({
...searchParams,
email: e.target.value,
});
}}
/>
{searchParams.email && (
<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, email: "" })
}
variant={"ghost"}
>
<Icons.close className="size-3" />
</Button>
)}
</div>
<div className="relative w-full">
<Input
className="h-8 text-xs md:text-xs"
placeholder="Search by user name..."
value={searchParams.userName}
onChange={(e) => {
setSearchParams({
...searchParams,
userName: e.target.value,
});
}}
/>
{searchParams.userName && (
<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"}
>
<Icons.close className="size-3" />
</Button>
)}
</div>
</div>
<Table>
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-8">
<TableHead className="col-span-1 flex items-center font-bold">
{t("Name")}
</TableHead>
<TableHead className="col-span-1 flex items-center font-bold sm:col-span-2">
{t("Email")}
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
{t("Role")}
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
{t("Plan")}
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
{t("Status")}
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
{t("Join")}
</TableHead>
<TableHead className="col-span-1 flex items-center justify-center font-bold">
{t("Actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<>
<TableColumnSekleton />
<TableColumnSekleton />
<TableColumnSekleton />
<TableColumnSekleton />
<TableColumnSekleton />
</>
) : data && data.list && data.list.length ? (
data.list.map((user) => (
<TableRow
key={user.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-8"
>
<TableCell className="col-span-1 truncate">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="truncate">
{user.name || "Anonymous"}
</TooltipTrigger>
<TooltipContent>
{user.name || "Anonymous"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell className="col-span-1 flex items-center gap-1 truncate sm:col-span-2">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="truncate">
{user.email}
</TooltipTrigger>
<TooltipContent>{user.email}</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Badge className="text-xs" variant="outline">
{t(user.role)}
</Badge>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Badge className="text-xs" variant="outline">
{user.team}
</Badge>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<Switch defaultChecked={user.active === 1} disabled />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<TimeAgoIntl date={user.updatedAt as Date} />
</TableCell>
<TableCell className="col-span-1 flex justify-center">
<Button
className="text-sm hover:bg-slate-100"
size="sm"
variant={"outline"}
onClick={() => {
setcurrentEditUser(user);
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
}}
>
<p className="text-nowrap">{t("Edit")}</p>
<PenLine className="ml-1 size-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<EmptyPlaceholder className="shadow-none">
<EmptyPlaceholder.Icon name="users" />
<EmptyPlaceholder.Title>No users</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
Here don&apos;t have any user yet.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
)}
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
layout={isMobile ? "right" : "split"}
total={data.total}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
pageSize={pageSize}
setPageSize={setPageSize}
/>
)}
</Table>
</CardContent>
</Card>
<Modal
className="md:max-w-2xl"
showModal={isShowForm}
setShowModal={setShowForm}
>
<UserForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type={formType}
initData={currentEditUser}
onRefresh={handleRefresh}
/>
</Modal>
</>
);
}