feat: add admin managements

This commit is contained in:
oiov
2024-07-31 11:29:19 +08:00
parent 9f2436359e
commit 6a1efda158
38 changed files with 1188 additions and 322 deletions
-34
View File
@@ -1,34 +0,0 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { DashboardHeader } from "@/components/dashboard/header";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
export const metadata = constructMetadata({
title: "Orders  WR.DO",
description: "Check and manage your latest orders.",
});
export default async function OrdersPage() {
// const user = await getCurrentUser();
// if (!user || user.role !== "ADMIN") redirect("/login");
return (
<>
<DashboardHeader
heading="Orders"
text="Check and manage your latest orders."
/>
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="package" />
<EmptyPlaceholder.Title>No orders listed</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
You don&apos;t have any orders yet. Start ordering a product.
</EmptyPlaceholder.Description>
<Button>Buy Products</Button>
</EmptyPlaceholder>
</>
);
}
+29 -7
View File
@@ -1,7 +1,12 @@
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
import { getUserShortUrlCount } from "@/lib/dto/short-urls";
import { getAllUsersCount } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardInfoCard } from "@/components/dashboard/dashboard-info-card";
import { DashboardHeader } from "@/components/dashboard/header";
import InfoCard from "@/components/dashboard/info-card";
import TransactionsList from "@/components/dashboard/transactions-list";
@@ -13,7 +18,11 @@ export const metadata = constructMetadata({
export default async function AdminPage() {
const user = await getCurrentUser();
if (!user || user.role !== "ADMIN") redirect("/login");
if (!user || !user.id || user.role !== "ADMIN") redirect("/login");
const user_count = await getAllUsersCount();
const record_count = await getUserRecordCount(user.id, 1, "ADMIN");
const url_count = await getUserShortUrlCount(user.id, 1, "ADMIN");
return (
<>
@@ -22,14 +31,27 @@ export default async function AdminPage() {
text="Access only for users with ADMIN role."
/>
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<InfoCard />
<InfoCard />
<InfoCard />
<InfoCard />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 xl:grid-cols-3">
<DashboardInfoCard
userId={user.id}
title="Users"
count={user_count}
link="/admin/users"
/>
<DashboardInfoCard
userId={user.id}
title="DNS Records"
count={record_count}
link="/admin/records"
/>
<DashboardInfoCard
userId={user.id}
title="Short URLs"
count={url_count}
link="/admin/urls"
/>
</div>
<TransactionsList />
<TransactionsList />
</div>
</>
);
@@ -1,13 +1,10 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function OrdersLoading() {
export default function DashboardRecordsLoading() {
return (
<>
<DashboardHeader
heading="Orders"
text="Check and manage your latest orders."
/>
<DashboardHeader heading="DNS Records" text="" />
<Skeleton className="size-full rounded-lg" />
</>
);
+33
View File
@@ -0,0 +1,33 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import UserRecordsList from "../../dashboard/records/record-list";
export const metadata = constructMetadata({
title: "DNS Records - WR.DO",
description: "List and manage records.",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<DashboardHeader
heading="Manage&nbsp;&nbsp;DNS&nbsp;&nbsp;Records"
text="List and manage records."
link="/docs/dns-records"
linkText="DNS Records."
/>
<UserRecordsList
user={{ id: user.id, name: user.name || "" }}
action="/api/record/admin"
/>
</>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function DashboardUrlsLoading() {
return (
<>
<DashboardHeader heading="Short Urls" text="" />
<Skeleton className="size-full rounded-lg" />
</>
);
}
+33
View File
@@ -0,0 +1,33 @@
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";
export const metadata = constructMetadata({
title: "Short URLs - WR.DO",
description: "List and manage records.",
});
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user?.id) redirect("/login");
return (
<>
<DashboardHeader
heading="Manage&nbsp;Short&nbsp;URLs"
text="List and manage short urls."
link="/docs/short-urls"
linkText="Short urls."
/>
<UserUrlsList
user={{ id: user.id, name: user.name || "" }}
action="/api/url/admin"
/>
</>
);
}
+68 -76
View File
@@ -2,8 +2,9 @@
import { useState } from "react";
import Link from "next/link";
import { Pagination } from "@nextui-org/pagination";
import { User } from "@prisma/client";
import { PenLine, RefreshCwIcon } from "lucide-react";
import { ArrowLeft, ArrowRight, PenLine, RefreshCwIcon } from "lucide-react";
import useSWR, { useSWRConfig } from "swr";
import { siteConfig } from "@/config/site";
@@ -18,6 +19,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { PaginationWrapper } from "@/components/ui/pagination";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
@@ -73,10 +75,12 @@ export default function UsersList({ user }: UrlListProps) {
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const { mutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<User[]>(
"/api/user/admin",
const { data, error, isLoading } = useSWR<{ total: number; list: User[] }>(
`/api/user/admin?page=${currentPage}&size=${pageSize}`,
fetcher,
{
revalidateOnFocus: false,
@@ -84,21 +88,19 @@ export default function UsersList({ user }: UrlListProps) {
);
const handleRefresh = () => {
mutate("/api/user/admin", undefined);
mutate(`/api/user/admin?page=${currentPage}&size=${pageSize}`, undefined);
};
return (
<>
<Card className="xl:col-span-2">
<CardHeader className="flex flex-row items-center">
<div className="grid gap-2">
<CardDescription className="text-balance text-lg font-bold">
<span>Total Users:</span>{" "}
<span className="font-bold">
{data && <CountUpFn count={data.length} />}
</span>
</CardDescription>
</div>
<CardDescription className="text-balance text-lg font-bold">
<span>Total Users:</span>{" "}
<span className="font-bold">
{data && <CountUpFn count={data.total} />}
</span>
</CardDescription>
<div className="ml-auto flex items-center justify-end gap-3">
<Button
variant={"outline"}
@@ -111,17 +113,6 @@ export default function UsersList({ user }: UrlListProps) {
<RefreshCwIcon className="size-4" />
)}
</Button>
{/* <Button
className="w-[120px] shrink-0 gap-1"
variant="default"
onClick={() => {
setcurrentEditUser(null);
setShowForm(false);
setShowForm(!isShowForm);
}}
>
Add user
</Button> */}
</div>
</CardHeader>
<CardContent>
@@ -165,59 +156,53 @@ export default function UsersList({ user }: UrlListProps) {
<TableColumnSekleton />
<TableColumnSekleton />
</>
) : data && data.length > 0 ? (
data
.sort(
(a, b) =>
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime(),
)
.map((user) => (
<TableRow
key={user.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
>
<TableCell className="col-span-1">
{user.name || "Anonymous"}
</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">
{user.role}
</Badge>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<StatusDot status={user.active} />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
{timeAgo(user.createdAt || "")}
</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);
setShowForm(!isShowForm);
}}
>
<p>Edit</p>
<PenLine className="ml-1 size-4" />
</Button>
</TableCell>
</TableRow>
))
) : data && data.list ? (
data.list.map((user) => (
<TableRow
key={user.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
>
<TableCell className="col-span-1">
{user.name || "Anonymous"}
</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">
{user.role}
</Badge>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<StatusDot status={user.active} />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
{timeAgo(user.createdAt || "")}
</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);
setShowForm(!isShowForm);
}}
>
<p>Edit</p>
<PenLine className="ml-1 size-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="user" />
@@ -239,6 +224,13 @@ export default function UsersList({ user }: UrlListProps) {
</EmptyPlaceholder>
)}
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
total={Math.ceil(data.total / pageSize)}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
)}
</Table>
</CardContent>
</Card>
-74
View File
@@ -1,74 +0,0 @@
import Link from "next/link";
import { GlobeLock, Link as LinkIcon } from "lucide-react";
import { siteConfig } from "@/config/site";
import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
import { getUserShortUrlCount } from "@/lib/dto/short-urls";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import CountUp from "@/components/dashboard/count-up";
export async function DNSInfoCard({ userId }: { userId: string }) {
const count = await getUserRecordCount(userId);
return (
<Card className="grids group bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
<Link
className="font-semibold text-slate-500 duration-500 group-hover:text-blue-500 group-hover:underline"
href="/dashboard/records"
>
DNS Records
</Link>
</CardTitle>
<GlobeLock className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{[-1, undefined].includes(count) ? (
<Skeleton className="h-5 w-20" />
) : (
<div className="flex items-end gap-2 text-2xl font-bold">
<CountUp count={count} />
<span className="align-top text-base text-slate-500">
/ {siteConfig.freeQuota.record}
</span>
</div>
)}
<p className="text-xs text-muted-foreground">total</p>
</CardContent>
</Card>
);
}
export async function UrlsInfoCard({ userId }: { userId: string }) {
const count = await getUserShortUrlCount(userId);
return (
<Card className="grids group bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
<Link
className="font-semibold text-slate-500 duration-500 group-hover:text-blue-500 group-hover:underline"
href="/dashboard/urls"
>
Short URLs
</Link>
</CardTitle>
<LinkIcon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{[-1, undefined].includes(count) ? (
<Skeleton className="h-5 w-20" />
) : (
<div className="flex items-end gap-2 text-2xl font-bold">
<CountUp count={count} />
<span className="align-top text-base text-slate-500">
/ {siteConfig.freeQuota.url}
</span>
</div>
)}
<p className="text-xs text-muted-foreground">total</p>
</CardContent>
</Card>
);
}
+33 -21
View File
@@ -1,12 +1,17 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
import { getUserShortUrlCount } from "@/lib/dto/short-urls";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import InfoCard from "@/components/dashboard/info-card";
import { DNSInfoCard, UrlsInfoCard } from "./info-card";
import {
DashboardInfoCard,
HeroCard,
} from "../../../components/dashboard/dashboard-info-card";
import UserRecordsList from "./records/record-list";
import UserUrlsList from "./urls/url-list";
@@ -20,6 +25,9 @@ export default async function DashboardPage() {
if (!user?.id) redirect("/login");
const record_count = await getUserRecordCount(user.id);
const url_count = await getUserShortUrlCount(user.id);
return (
<>
<DashboardHeader
@@ -28,26 +36,30 @@ export default async function DashboardPage() {
/>
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 xl:grid-cols-3">
<div className="group relative mb-4 h-full w-full shrink-0 origin-left overflow-hidden rounded-lg border bg-gray-50/70 p-4 text-left duration-500 before:absolute before:right-1 before:top-1 before:z-[2] before:h-12 before:w-12 before:rounded-full before:bg-violet-500 before:blur-lg before:duration-500 after:absolute after:right-8 after:top-3 after:z-[2] after:h-20 after:w-20 after:rounded-full after:bg-rose-300 after:blur-lg after:duration-500 hover:border-cyan-600 hover:decoration-2 hover:duration-500 hover:before:-bottom-8 hover:before:right-12 hover:before:blur hover:before:[box-shadow:_20px_20px_20px_30px_#a21caf] hover:after:-right-8 group-hover:before:duration-500 group-hover:after:duration-500 dark:bg-primary-foreground md:max-w-[350px]">
<h1 className="mb-7 text-lg font-bold duration-500 group-hover:text-blue-500">
Free Records & URLs
</h1>
<p className="text-sm">
Learn more from{" "}
<Link
className="text-blue-500 hover:underline"
href="/docs"
target="_blank"
>
documents.
</Link>{" "}
</p>
</div>
<DNSInfoCard userId={user.id} />
<UrlsInfoCard userId={user.id} />
<HeroCard />
<DashboardInfoCard
userId={user.id}
title="DNS Records"
count={record_count}
total={siteConfig.freeQuota.record}
link="/dashboard/records"
/>
<DashboardInfoCard
userId={user.id}
title="Short URLs"
count={url_count}
total={siteConfig.freeQuota.url}
link="/dashboard/urls"
/>
</div>
<UserRecordsList user={{ id: user.id, name: user.name || "" }} />
<UserUrlsList user={{ id: user.id, name: user.name || "" }} />
<UserRecordsList
user={{ id: user.id, name: user.name || "" }}
action="/api/record"
/>
<UserUrlsList
user={{ id: user.id, name: user.name || "" }}
action="/api/url"
/>
</div>
</>
);
+4 -1
View File
@@ -24,7 +24,10 @@ export default async function DashboardPage() {
link="/docs/dns-records"
linkText="DNS Records."
/>
<UserRecordsList user={{ id: user.id, name: user.name || "" }} />
<UserRecordsList
user={{ id: user.id, name: user.name || "" }}
action="/api/record"
/>
</>
);
}
@@ -3,12 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import { User } from "@prisma/client";
import {
ArrowUpRight,
DotSquareIcon,
PenLine,
RefreshCwIcon,
} from "lucide-react";
import { PenLine, RefreshCwIcon } from "lucide-react";
import useSWR, { useSWRConfig } from "swr";
import { TTL_ENUMS } from "@/lib/cloudflare";
@@ -23,6 +18,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { PaginationWrapper } from "@/components/ui/pagination";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
@@ -38,15 +34,17 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import CountUpFn from "@/components/dashboard/count-up";
import StatusDot from "@/components/dashboard/status-dot";
import { FormType, RecordForm } from "@/components/forms/record-form";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
export interface RecordListProps {
user: Pick<User, "id" | "name">;
action: string;
}
function TableColumnSekleton({ className }: { className?: string }) {
function TableColumnSekleton() {
return (
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-7">
<TableCell className="col-span-1">
@@ -71,35 +69,48 @@ function TableColumnSekleton({ className }: { className?: string }) {
);
}
export default function UserRecordsList({ user }: RecordListProps) {
export default function UserRecordsList({ user, action }: RecordListProps) {
const [isShowForm, setShowForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
const [currentEditRecord, setCurrentEditRecord] =
useState<UserRecordFormData | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const { mutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<UserRecordFormData[]>(
"/api/record",
fetcher,
{
revalidateOnFocus: false,
},
);
console.log("base", action);
const { data, error, isLoading } = useSWR<{
total: number;
list: UserRecordFormData[];
}>(`${action}?page=${currentPage}&size=${pageSize}`, fetcher, {
revalidateOnFocus: false,
});
const handleRefresh = () => {
mutate("/api/record", undefined);
mutate(`${action}?page=${currentPage}&size=${pageSize}`, undefined);
};
return (
<>
<Card className="xl:col-span-2">
<CardHeader className="flex flex-row items-center">
<div className="grid gap-2">
<CardTitle>Records</CardTitle>
<CardDescription className="text-balance">
All DNS Records
{action.includes("/admin") ? (
<CardDescription className="text-balance text-lg font-bold">
<span>Total Records:</span>{" "}
<span className="font-bold">
{data && <CountUpFn count={data.total ?? 0} />}
</span>
</CardDescription>
</div>
) : (
<div className="grid gap-2">
<CardTitle>DNS Records</CardTitle>
<CardDescription className="text-balance">
Your DNS Records
</CardDescription>
</div>
)}
<div className="ml-auto flex items-center justify-end gap-3">
<Button
variant={"outline"}
@@ -134,6 +145,7 @@ export default function UserRecordsList({ user }: RecordListProps) {
setShowForm={setShowForm}
type={formType}
initData={currentEditRecord}
action={action}
onRefresh={handleRefresh}
/>
)}
@@ -167,8 +179,8 @@ export default function UserRecordsList({ user }: RecordListProps) {
<TableColumnSekleton />
<TableColumnSekleton />
</>
) : data && data.length > 0 ? (
data.map((record) => (
) : data && data.list ? (
data.list.map((record) => (
<TableRow
key={record.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
@@ -179,9 +191,16 @@ export default function UserRecordsList({ user }: RecordListProps) {
</Badge>
</TableCell>
<TableCell className="col-span-1">
{record.name.endsWith(".wr.do")
? record.name.slice(0, -6)
: record.name}
<Link
className="text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
href={"https://" + record.name}
target="_blank"
prefetch={false}
>
{record.name.endsWith(".wr.do")
? record.name.slice(0, -6)
: record.name}
</Link>
</TableCell>
<TableCell className="col-span-2 hidden truncate text-nowrap sm:inline-block">
<TooltipProvider>
@@ -240,6 +259,13 @@ export default function UserRecordsList({ user }: RecordListProps) {
</EmptyPlaceholder>
)}
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
total={Math.ceil(data.total / pageSize)}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
)}
</Table>
</CardContent>
</Card>
+4 -1
View File
@@ -24,7 +24,10 @@ export default async function DashboardPage() {
link="/docs/short-urls"
linkText="Short urls."
/>
<UserUrlsList user={{ id: user.id, name: user.name || "" }} />
<UserUrlsList
user={{ id: user.id, name: user.name || "" }}
action="/api/url"
/>
</>
);
}
+37 -17
View File
@@ -18,6 +18,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { PaginationWrapper } from "@/components/ui/pagination";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
@@ -27,6 +28,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import CountUpFn from "@/components/dashboard/count-up";
import StatusDot from "@/components/dashboard/status-dot";
import { FormType } from "@/components/forms/record-form";
import { UrlForm } from "@/components/forms/url-form";
@@ -35,6 +37,7 @@ import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
export interface UrlListProps {
user: Pick<User, "id" | "name">;
action: string;
}
function TableColumnSekleton({ className }: { className?: string }) {
@@ -59,37 +62,47 @@ function TableColumnSekleton({ className }: { className?: string }) {
);
}
export default function UserUrlsList({ user }: UrlListProps) {
export default function UserUrlsList({ user, action }: UrlListProps) {
const { isMobile } = useMediaQuery();
const [isShowForm, setShowForm] = useState(false);
const [formType, setFormType] = useState<FormType>("add");
const [currentEditUrl, setCurrentEditUrl] = useState<ShortUrlFormData | null>(
null,
);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const { mutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<ShortUrlFormData[]>(
"/api/url",
fetcher,
{
revalidateOnFocus: false,
},
);
const { data, error, isLoading } = useSWR<{
total: number;
list: ShortUrlFormData[];
}>(`${action}?page=${currentPage}&size=${pageSize}`, fetcher, {
revalidateOnFocus: false,
});
const handleRefresh = () => {
mutate("/api/url", undefined);
mutate(`${action}?page=${currentPage}&size=${pageSize}`, undefined);
};
return (
<>
<Card className="xl:col-span-2">
<CardHeader className="flex flex-row items-center">
<div className="grid gap-2">
<CardTitle>Short URLs</CardTitle>
<CardDescription className="text-balance">
All Short URLs
{action.includes("/admin") ? (
<CardDescription className="text-balance text-lg font-bold">
<span>Total URLs:</span>{" "}
<span className="font-bold">
{data && <CountUpFn count={data.total ?? 0} />}
</span>
</CardDescription>
</div>
) : (
<div className="grid gap-2">
<CardTitle>Short URLs</CardTitle>
<CardDescription className="text-balance">
Your Short URLs
</CardDescription>
</div>
)}
<div className="ml-auto flex items-center justify-end gap-3">
<Button
variant={"outline"}
@@ -124,6 +137,7 @@ export default function UserUrlsList({ user }: UrlListProps) {
setShowForm={setShowForm}
type={formType}
initData={currentEditUrl}
action={action}
onRefresh={handleRefresh}
/>
)}
@@ -154,8 +168,8 @@ export default function UserUrlsList({ user }: UrlListProps) {
<TableColumnSekleton />
<TableColumnSekleton />
</>
) : data && data.length > 0 ? (
data.map((short) => (
) : data && data.list ? (
data.list.map((short) => (
<TableRow
key={short.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
@@ -170,7 +184,6 @@ export default function UserUrlsList({ user }: UrlListProps) {
{short.target.startsWith("http")
? short.target.split("//")[1]
: short.target}
{/* {isMobile ? short.target.split("//")[1] :short.target} */}
</Link>
</TableCell>
<TableCell className="col-span-1 flex items-center gap-1 sm:col-span-2">
@@ -236,6 +249,13 @@ export default function UserUrlsList({ user }: UrlListProps) {
</EmptyPlaceholder>
)}
</TableBody>
{data && Math.ceil(data.total / pageSize) > 1 && (
<PaginationWrapper
total={Math.ceil(data.total / pageSize)}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
)}
</Table>
</CardContent>
</Card>
+36
View File
@@ -0,0 +1,36 @@
import { env } from "@/env.mjs";
import { getUserRecords } from "@/lib/dto/cloudflare-dns-record";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
export async function GET(req: Request) {
try {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
if (user.role !== "ADMIN") {
return Response.json("Unauthorized", {
status: 401,
statusText: "Unauthorized",
});
}
const url = new URL(req.url);
const page = url.searchParams.get("page");
const size = url.searchParams.get("size");
const data = await getUserRecords(
user.id,
1,
Number(page || "1"),
Number(size || "10"),
"ADMIN",
);
return Response.json(data);
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
statusText: error.statusText || "Server error",
});
}
}
+11 -3
View File
@@ -7,11 +7,19 @@ export async function GET(req: Request) {
try {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
// const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env;
const user_records = await getUserRecords(user.id, 1);
const url = new URL(req.url);
const page = url.searchParams.get("page");
const size = url.searchParams.get("size");
return Response.json(user_records);
const data = await getUserRecords(
user.id,
1,
Number(page || "1"),
Number(size || "10"),
);
return Response.json(data);
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
+35
View File
@@ -0,0 +1,35 @@
import { env } from "@/env.mjs";
import { getUserShortUrls } from "@/lib/dto/short-urls";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
export async function GET(req: Request) {
try {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
if (user.role !== "ADMIN") {
return Response.json("Unauthorized", {
status: 401,
statusText: "Unauthorized",
});
}
const url = new URL(req.url);
const page = url.searchParams.get("page");
const size = url.searchParams.get("size");
const data = await getUserShortUrls(
user.id,
1,
Number(page || "1"),
Number(size || "10"),
"ADMIN",
);
return Response.json(data);
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
statusText: error.statusText || "Server error",
});
}
}
+10 -3
View File
@@ -8,10 +8,17 @@ export async function GET(req: Request) {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
const user_urls = await getUserShortUrls(user.id, 1);
// TODO: get meta info
const url = new URL(req.url);
const page = url.searchParams.get("page");
const size = url.searchParams.get("size");
const data = await getUserShortUrls(
user.id,
1,
Number(page || "1"),
Number(size || "10"),
);
return Response.json(user_urls);
return Response.json(data);
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
+5 -2
View File
@@ -13,9 +13,12 @@ export async function GET(req: Request) {
});
}
const user_records = await getAllUsers();
const url = new URL(req.url);
const page = url.searchParams.get("page");
const size = url.searchParams.get("size");
const data = await getAllUsers(Number(page || "1"), Number(size || "10"));
return Response.json(user_records);
return Response.json(data);
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
+21
View File
@@ -0,0 +1,21 @@
export async function POST(req: Request) {
try {
// const user = checkUserStatus(await getCurrentUser());
// if (user instanceof Response) return user;
// if (user.role !== "ADMIN") {
// return Response.json("Unauthorized", {
// status: 401,
// statusText: "Unauthorized",
// });
// }
// const body = await req.json();
// const { id, ...data } = body;
// const result = await updateUserById(id, data);
return Response.json("success");
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
statusText: error.statusText || "Server error",
});
}
}
+3 -7
View File
@@ -1,6 +1,6 @@
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import { deleteUserById } from "@/lib/dto/user";
export const DELETE = auth(async (req) => {
if (!req.auth) {
@@ -8,16 +8,12 @@ export const DELETE = auth(async (req) => {
}
const currentUser = req.auth.user;
if (!currentUser) {
if (!currentUser || !currentUser?.id) {
return new Response("Invalid user", { status: 401 });
}
try {
await prisma.user.delete({
where: {
id: currentUser.id,
},
});
await deleteUserById(currentUser.id);
} catch (error) {
return new Response("Internal server error", { status: 500 });
}
@@ -0,0 +1,76 @@
import Link from "next/link";
import { Link as LinkIcon } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import CountUp from "@/components/dashboard/count-up";
import { Icons } from "@/components/shared/icons";
export async function DashboardInfoCard({
userId,
title,
total,
count,
link,
// icon = "user",
}: {
userId: string;
title: string;
total?: number;
count: number;
link: string;
// icon: keyof typeof Icons;
}) {
// const iconCpn = Icons[icon];
return (
<Card className="grids group bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
<Link
className="font-semibold text-slate-500 duration-500 group-hover:text-blue-500 group-hover:underline"
href={link}
>
{title}
</Link>
</CardTitle>
{/* {icon && <iconCpn />} */}
<LinkIcon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{[-1, undefined].includes(count) ? (
<Skeleton className="h-5 w-20" />
) : (
<div className="flex items-end gap-2 text-2xl font-bold">
<CountUp count={count} />
{total && (
<span className="align-top text-base text-slate-500">
/ {total}
</span>
)}
</div>
)}
<p className="text-xs text-muted-foreground">total</p>
</CardContent>
</Card>
);
}
export function HeroCard() {
return (
<div className="group relative mb-4 h-full w-full shrink-0 origin-left overflow-hidden rounded-lg border bg-gray-50/70 p-4 text-left duration-500 before:absolute before:right-1 before:top-1 before:z-[2] before:h-12 before:w-12 before:rounded-full before:bg-violet-500 before:blur-lg before:duration-500 after:absolute after:right-8 after:top-3 after:z-[2] after:h-20 after:w-20 after:rounded-full after:bg-rose-300 after:blur-lg after:duration-500 hover:border-cyan-600 hover:decoration-2 hover:duration-500 hover:before:-bottom-8 hover:before:right-12 hover:before:blur hover:before:[box-shadow:_20px_20px_20px_30px_#a21caf] hover:after:-right-8 group-hover:before:duration-500 group-hover:after:duration-500 dark:bg-primary-foreground md:max-w-[350px]">
<h1 className="mb-7 text-lg font-bold duration-500 group-hover:text-blue-500">
Free Records & URLs
</h1>
<p className="text-sm">
Learn more from{" "}
<Link
className="text-blue-500 hover:underline"
href="/docs"
target="_blank"
>
documents.
</Link>{" "}
</p>
</div>
);
}
+5 -3
View File
@@ -38,6 +38,7 @@ export interface RecordFormProps {
setShowForm: Dispatch<SetStateAction<boolean>>;
type: FormType;
initData?: UserRecordFormData | null;
action: string;
onRefresh: () => void;
}
@@ -47,6 +48,7 @@ export function RecordForm({
setShowForm,
type,
initData,
action,
onRefresh,
}: RecordFormProps) {
const [isPending, startTransition] = useTransition();
@@ -86,7 +88,7 @@ export function RecordForm({
const handleCreateRecord = async (data: CreateDNSRecord) => {
startTransition(async () => {
const response = await fetch("/api/record/add", {
const response = await fetch(`${action}/add`, {
method: "POST",
body: JSON.stringify({
records: [data],
@@ -108,7 +110,7 @@ export function RecordForm({
const handleUpdateRecord = async (data: CreateDNSRecord) => {
startTransition(async () => {
if (type === "edit") {
const response = await fetch("/api/record/update", {
const response = await fetch(`${action}/update`, {
method: "POST",
body: JSON.stringify({
recordId: initData?.record_id,
@@ -132,7 +134,7 @@ export function RecordForm({
const handleDeleteRecord = async () => {
if (type === "edit") {
startDeleteTransition(async () => {
const response = await fetch("/api/record/delete", {
const response = await fetch(`${action}/delete`, {
method: "POST",
body: JSON.stringify({
record_id: initData?.record_id,
+2
View File
@@ -28,6 +28,7 @@ export interface RecordFormProps {
setShowForm: Dispatch<SetStateAction<boolean>>;
type: FormType;
initData?: ShortUrlFormData | null;
action: string;
onRefresh: () => void;
}
@@ -35,6 +36,7 @@ export function UrlForm({
setShowForm,
type,
initData,
action,
onRefresh,
}: RecordFormProps) {
const [isPending, startTransition] = useTransition();
+1 -1
View File
@@ -147,7 +147,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
{isGithubLoading ? (
<Icons.spinner className="mr-2 size-4 animate-spin" />
) : (
<Icons.gitHub className="mr-2 size-4" />
<Icons.github className="mr-2 size-4" />
)}{" "}
Github
</button>
+1 -1
View File
@@ -179,7 +179,7 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) {
</nav>
{isSidebarExpanded && (
<p className="mx-3 mb-3 mt-auto font-mono text-xs text-muted-foreground/70">
<p className="mx-3 mt-auto pb-3 pt-6 font-mono text-xs text-muted-foreground/70">
&copy; 2024{" "}
<Link
href={siteConfig.links.github}
+1 -1
View File
@@ -121,7 +121,7 @@ export function NavMobile() {
<div className="mt-5 flex items-center justify-end space-x-4">
<Link href={siteConfig.links.github} target="_blank" rel="noreferrer">
<Icons.gitHub className="size-6" />
<Icons.github className="size-6" />
<span className="sr-only">GitHub</span>
</Link>
<ModeToggle />
+1 -1
View File
@@ -96,7 +96,7 @@ export function NavBar({ scroll = false }: NavBarProps) {
target="_blank"
rel="noreferrer"
>
<Icons.gitHub className="size-7" />
<Icons.github className="size-7" />
<span className="sr-only">GitHub</span>
</Link>
</div>
+1 -1
View File
@@ -82,7 +82,7 @@ export function SiteFooter({ className }: React.HTMLAttributes<HTMLElement>) {
rel="noreferrer"
className="font-medium underline underline-offset-1"
>
<Icons.gitHub className="size-5" />
<Icons.github className="size-5" />
</Link>
<ModeToggle />
</div>
+1 -1
View File
@@ -50,7 +50,7 @@ export default async function HeroLanding() {
"px-4 text-[15px]",
)}
>
<Icons.gitHub className="mr-2 size-4" />
<Icons.github className="mr-2 size-4" />
<p>
<span className="hidden sm:inline-block">Star on</span> GitHub
</p>
+1 -1
View File
@@ -51,7 +51,7 @@ export const Icons = {
copy: Copy,
dashboard: LayoutPanelLeft,
ellipsis: MoreVertical,
gitHub: ({ ...props }: LucideProps) => (
github: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
+42
View File
@@ -0,0 +1,42 @@
import { Pagination } from "@nextui-org/pagination";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { Button } from "./button";
export function PaginationWrapper({
total,
currentPage,
setCurrentPage,
}: {
total: number;
currentPage: number;
setCurrentPage: any;
}) {
return (
<div className="my-3 flex items-center justify-end gap-3">
<Button
size="sm"
variant="outline"
color="secondary"
onClick={() => setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev))}
>
<ArrowLeft className="size-4" />
</Button>
<Pagination
isCompact
total={total}
color="success"
page={currentPage}
onChange={setCurrentPage}
/>
<Button
size="sm"
variant="outline"
color="secondary"
onClick={() => setCurrentPage((prev) => (prev < 10 ? prev + 1 : prev))}
>
<ArrowRight className="size-4" />
</Button>
</div>
);
}
+10 -3
View File
@@ -25,13 +25,19 @@ export const sidebarLinks: SidebarNavItem[] = [
{
href: "/admin/users",
icon: "user",
title: "User Management",
title: "Users",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/dashboard/charts",
href: "/admin/records",
icon: "lineChart",
title: "Charts",
title: "Records",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/urls",
icon: "post",
title: "URLs",
authorizeOnly: UserRole.ADMIN,
},
],
@@ -42,6 +48,7 @@ export const sidebarLinks: SidebarNavItem[] = [
{ href: "/dashboard/settings", icon: "settings", title: "Settings" },
{ href: "/", icon: "home", title: "Homepage" },
{ href: "/docs", icon: "bookOpen", title: "Documentation" },
{ href: siteConfig.links.github, icon: "github", title: "Github" },
{
href: "mailto:" + siteConfig.mailSupport,
icon: "mail",
+44 -12
View File
@@ -1,6 +1,7 @@
"use server";
import { auth } from "@/auth";
import { UserRole } from "@prisma/client";
import { prisma } from "@/lib/db";
import {
@@ -82,22 +83,53 @@ export async function createUserRecord(
}
}
export async function getUserRecords(userId: string, active: number = 1) {
return await prisma.userRecord.findMany({
where: {
userId: userId,
active,
},
});
export async function getUserRecords(
userId: string,
active: number = 1,
page: number,
size: number,
role: UserRole = "USER",
) {
const option =
role === "USER"
? {
userId,
active,
}
: {};
const [total, list] = await prisma.$transaction([
prisma.userRecord.count({
where: option,
}),
prisma.userRecord.findMany({
where: option,
skip: (page - 1) * size,
take: size,
orderBy: {
modified_on: "asc",
},
}),
]);
return {
total,
list,
};
}
export async function getUserRecordCount(userId: string, active: number = 1) {
export async function getUserRecordCount(
userId: string,
active: number = 1,
role: UserRole = "USER",
) {
try {
return await prisma.userRecord.count({
where: {
userId: userId,
active,
},
where:
role === "USER"
? {
userId: userId,
active,
}
: {},
});
} catch (error) {
return -1;
+44 -13
View File
@@ -1,5 +1,5 @@
import { auth } from "@/auth";
import { UrlMeta } from "@prisma/client";
import { UrlMeta, UserRole } from "@prisma/client";
import { prisma } from "@/lib/db";
@@ -20,22 +20,53 @@ export interface UserShortUrlInfo extends ShortUrlFormData {
meta?: UrlMeta;
}
export async function getUserShortUrls(userId: string, active: number = 1) {
return await prisma.userUrl.findMany({
where: {
userId,
active,
},
});
export async function getUserShortUrls(
userId: string,
active: number = 1,
page: number,
size: number,
role: UserRole = "USER",
) {
const option =
role === "USER"
? {
userId,
active,
}
: {};
const [total, list] = await prisma.$transaction([
prisma.userUrl.count({
where: option,
}),
prisma.userUrl.findMany({
where: option,
skip: (page - 1) * size,
take: size,
orderBy: {
updatedAt: "asc",
},
}),
]);
return {
total,
list,
};
}
export async function getUserShortUrlCount(userId: string, active: number = 1) {
export async function getUserShortUrlCount(
userId: string,
active: number = 1,
role: UserRole = "USER",
) {
try {
return await prisma.userUrl.count({
where: {
userId,
active,
},
where:
role === "USER"
? {
userId,
active,
}
: {},
});
} catch (error) {
return -1;
+53 -6
View File
@@ -1,4 +1,4 @@
import { User } from "@prisma/client";
import { User, UserRole } from "@prisma/client";
import { prisma } from "@/lib/db";
@@ -43,12 +43,59 @@ export const getUserById = async (id: string) => {
}
};
export const getAllUsers = async () => {
export const getAllUsers = async (page: number, size: number) => {
try {
// TODO: paginate
const users = await prisma.user.findMany();
return users;
} catch {
const [total, list] = await prisma.$transaction([
prisma.user.count(), // 获取所有用户的总数
prisma.user.findMany({
skip: (page - 1) * size,
take: size,
orderBy: {
createdAt: "asc",
},
}),
]);
return {
total,
list,
};
} catch (error) {
console.error(error);
return null;
}
};
export async function getAllUsersCount() {
try {
return await prisma.user.count();
} catch (error) {
return -1;
}
}
export const updateUser = async (userId: string, data: UpdateUserForm) => {
try {
const session = await prisma.user.update({
where: {
id: userId,
},
data: data,
});
return session;
} catch (error) {
return null;
}
};
export const deleteUserById = async (userId: string) => {
try {
const session = await prisma.user.delete({
where: {
id: userId,
},
});
return session;
} catch (error) {
return null;
}
};
+1 -1
View File
@@ -10,7 +10,7 @@ export const createRecordSchema = z.object({
name: z
.string()
.regex(/^[a-zA-Z0-9-]+$/, "Invalid characters")
.min(1)
.min(2)
.max(32),
content: z
.string()
+1
View File
@@ -21,6 +21,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.4.1",
"@hookform/resolvers": "^3.9.0",
"@nextui-org/pagination": "^2.0.35",
"@prisma/client": "^5.17.0",
"@radix-ui/react-accessible-icon": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.0",
+476
View File
@@ -14,6 +14,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.0
version: 3.9.0(react-hook-form@7.52.1(react@18.3.1))
'@nextui-org/pagination':
specifier: ^2.0.35
version: 2.0.35(@nextui-org/system@2.2.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(framer-motion@10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@prisma/client':
specifier: ^5.17.0
version: 5.17.0(prisma@5.17.0)
@@ -782,6 +785,21 @@ packages:
'@floating-ui/utils@0.1.6':
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
'@formatjs/ecma402-abstract@2.0.0':
resolution: {integrity: sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==}
'@formatjs/fast-memoize@2.2.0':
resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==}
'@formatjs/icu-messageformat-parser@2.7.8':
resolution: {integrity: sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==}
'@formatjs/icu-skeleton-parser@1.8.2':
resolution: {integrity: sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==}
'@formatjs/intl-localematcher@0.5.4':
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
'@grpc/grpc-js@1.10.6':
resolution: {integrity: sha512-xP58G7wDQ4TCmN/cMUHh00DS7SRDv/+lC+xFLrTkMIN8h55X5NhZMLYbvy7dSELP15qlI6hPhNCRWVMtZMwqLA==}
engines: {node: '>=12.10.0'}
@@ -941,6 +959,18 @@ packages:
cpu: [x64]
os: [win32]
'@internationalized/date@3.5.5':
resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==}
'@internationalized/message@3.1.4':
resolution: {integrity: sha512-Dygi9hH1s7V9nha07pggCkvmRfDd3q2lWnMGvrJyrOwYMe1yj4D2T9BoH9I6MGR7xz0biQrtLPsqUkqXzIrBOw==}
'@internationalized/number@3.5.3':
resolution: {integrity: sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==}
'@internationalized/string@3.2.3':
resolution: {integrity: sha512-9kpfLoA8HegiWTeCbR2livhdVeKobCnVv8tlJ6M2jF+4tcMqDo94ezwlnrUANBWPgd8U7OXIHCk2Ov2qhk4KXw==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1124,6 +1154,53 @@ packages:
cpu: [x64]
os: [win32]
'@nextui-org/pagination@2.0.35':
resolution: {integrity: sha512-07KJgZcJBt2e9RY6TsiQm5qrjDLH+gT3yB7yQ4jPdCK9fkTB0r2kvTOYdPUvrtVJYRq2bwFCWOz+9mokdNfcwg==}
peerDependencies:
'@nextui-org/system': '>=2.0.0'
'@nextui-org/theme': '>=2.1.0'
react: '>=18'
react-dom: '>=18'
'@nextui-org/react-rsc-utils@2.0.13':
resolution: {integrity: sha512-QewsXtoQlMsR9stThdazKEImg9oyZkPLs7wsymhrzh6/HdQCl9bTdb6tJcROg4vg5LRYKGG11USSQO2nKlfCcQ==}
'@nextui-org/react-utils@2.0.16':
resolution: {integrity: sha512-QdDoqzhx+4t9cDTVmtw5iOrfyLvpqyKsq8PARHUniCiQQDQd1ao7FCpzHgvU9poYcEdRk+Lsna66zbeMkFBB6w==}
peerDependencies:
react: '>=18'
'@nextui-org/shared-icons@2.0.9':
resolution: {integrity: sha512-WG3yinVY7Tk9VqJgcdF4V8Ok9+fcm5ey7S1els7kujrfqLYxtqoKywgiY/7QHwZlfQkzpykAfy+NAlHkTP5hMg==}
peerDependencies:
react: '>=18'
'@nextui-org/shared-utils@2.0.7':
resolution: {integrity: sha512-FxY3N0i1Al7Oz3yOQN0dSpG8UUrLIP3iYh3ubD7BhdQoZLl5xbG6++q1gqOzZXV+ZWeUFMY/or0ofzWxGHiOow==}
'@nextui-org/system-rsc@2.1.5':
resolution: {integrity: sha512-tkJLAyJu34Rr5KUMMqoB7cZjOVXB+7a/7N4ushZfuiLdoYijgmcXFMzLxjm+tbt9zA5AV+ivsfbHvscg77dJ6w==}
peerDependencies:
'@nextui-org/theme': '>=2.1.0'
react: '>=18'
'@nextui-org/system@2.2.5':
resolution: {integrity: sha512-nrX6768aiyWtpxX3OTFBIVWR+v9nlMsC3KaBinNfek97sNm7gAfTHi7q5kylE3L5yIMpNG+DclAKpuxgDQEmvw==}
peerDependencies:
framer-motion: '>=10.17.0'
react: '>=18'
react-dom: '>=18'
'@nextui-org/theme@2.2.9':
resolution: {integrity: sha512-TN2I9sMriLaj00pXsIMlg19+UHeOdjzS2JV0u4gjL14mSbQl5BYNxgbvU3gbMqkZZQ6OpwT4RnT8RS+ks6TXCw==}
peerDependencies:
tailwindcss: '>=3.4.0'
'@nextui-org/use-pagination@2.0.9':
resolution: {integrity: sha512-p5Gssyb71/SjRezq2o1aRsYTmC9idziW3pLCJFpVwLGfgWNARf9C6NS1oQsqKgjF5lvzoa88soZRDhKKvRAt/g==}
peerDependencies:
react: '>=18'
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -2089,6 +2166,58 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@react-aria/focus@3.17.1':
resolution: {integrity: sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-aria/focus@3.18.1':
resolution: {integrity: sha512-N0Cy61WCIv+57mbqC7hiZAsB+3rF5n4JKabxUmg/2RTJL6lq7hJ5N4gx75ymKxkN8GnVDwt4pKZah48Wopa5jw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/i18n@3.11.1':
resolution: {integrity: sha512-vuiBHw1kZruNMYeKkTGGnmPyMnM5T+gT8bz97H1FqIq1hQ6OPzmtBZ6W6l6OIMjeHI5oJo4utTwfZl495GALFQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-aria/interactions@3.21.3':
resolution: {integrity: sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-aria/interactions@3.22.1':
resolution: {integrity: sha512-5TLzQaDAQQ5C70yG8GInbO4wIylKY67RfTIIwQPGR/4n5OIjbUD8BOj3NuSsuZ/frUPaBXo1VEBBmSO23fxkjw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/overlays@3.22.1':
resolution: {integrity: sha512-GHiFMWO4EQ6+j6b5QCnNoOYiyx1Gk8ZiwLzzglCI4q1NY5AG2EAmfU4Z1+Gtrf2S5Y0zHbumC7rs9GnPoGLUYg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-aria/ssr@3.9.5':
resolution: {integrity: sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/utils@3.24.1':
resolution: {integrity: sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-aria/utils@3.25.1':
resolution: {integrity: sha512-5Uj864e7T5+yj78ZfLnfHqmypLiqW2mN+nsdslog2z5ssunTqjolVeM15ootXskjISlZ7MojLpq97kIC4nlnAw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/visually-hidden@3.8.14':
resolution: {integrity: sha512-DV3yagbAgO4ywQTq6D/AxcIaTC8c77r/SxlIMhQBMQ6vScJWTCh6zFG55wmLe3NKqvRrowv1OstlmYfZQ4v/XA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-email/body@0.0.8':
resolution: {integrity: sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==}
peerDependencies:
@@ -2218,6 +2347,41 @@ packages:
peerDependencies:
react: ^18.2.0
'@react-stately/overlays@3.6.9':
resolution: {integrity: sha512-4chfyzKw7P2UEainm0yzjUgYwG1ovBejN88eTrn+O62x5huuMCwe0cbMxmYh4y7IhRFSee3jIJd0SP0u/+i39w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-stately/utils@3.10.1':
resolution: {integrity: sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-stately/utils@3.10.2':
resolution: {integrity: sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-types/button@3.9.6':
resolution: {integrity: sha512-8lA+D5JLbNyQikf8M/cPP2cji91aVTcqjrGpDqI7sQnaLFikM8eFR6l1ZWGtZS5MCcbfooko77ha35SYplSQvw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-types/overlays@3.8.9':
resolution: {integrity: sha512-9ni9upQgXPnR+K9cWmbYWvm3ll9gH8P/XsEZprqIV5zNLMF334jADK48h4jafb1X9RFnj0WbHo6BqcSObzjTig==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-types/shared@3.23.1':
resolution: {integrity: sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@react-types/shared@3.24.1':
resolution: {integrity: sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@resvg/resvg-wasm@2.4.0':
resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==}
engines: {node: '>= 10'}
@@ -2925,6 +3089,9 @@ packages:
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color2k@2.0.3:
resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
@@ -2954,6 +3121,9 @@ packages:
compare-func@2.0.0:
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
compute-scroll-into-view@3.1.0:
resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -3559,6 +3729,10 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flat@5.0.2:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
flatted@3.2.9:
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
@@ -3867,6 +4041,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
intl-messageformat@10.5.14:
resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -4156,18 +4333,30 @@ packages:
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
lodash.mapkeys@4.6.0:
resolution: {integrity: sha512-0Al+hxpYvONWtg+ZqHpa/GaVzxuN3V7Xeo2p+bY06EaK/n+Y9R7nBePPN2o1LxmL0TWQSwP8LYZ008/hc9JzhA==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.mergewith@4.6.2:
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
lodash.omit@4.5.0:
resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==}
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
@@ -5174,6 +5363,9 @@ packages:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'}
scroll-into-view-if-needed@3.0.10:
resolution: {integrity: sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==}
section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
@@ -5421,12 +5613,21 @@ packages:
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
tailwind-merge@2.2.0:
resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==}
tailwind-merge@2.4.0:
resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==}
tailwind-variants@0.1.20:
resolution: {integrity: sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==}
engines: {node: '>=16.x', pnpm: '>=7.x'}
peerDependencies:
tailwindcss: '*'
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
@@ -6367,6 +6568,30 @@ snapshots:
'@floating-ui/utils@0.1.6': {}
'@formatjs/ecma402-abstract@2.0.0':
dependencies:
'@formatjs/intl-localematcher': 0.5.4
tslib: 2.6.2
'@formatjs/fast-memoize@2.2.0':
dependencies:
tslib: 2.6.2
'@formatjs/icu-messageformat-parser@2.7.8':
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
'@formatjs/icu-skeleton-parser': 1.8.2
tslib: 2.6.2
'@formatjs/icu-skeleton-parser@1.8.2':
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
tslib: 2.6.2
'@formatjs/intl-localematcher@0.5.4':
dependencies:
tslib: 2.6.2
'@grpc/grpc-js@1.10.6':
dependencies:
'@grpc/proto-loader': 0.7.12
@@ -6482,6 +6707,23 @@ snapshots:
'@img/sharp-win32-x64@0.33.4':
optional: true
'@internationalized/date@3.5.5':
dependencies:
'@swc/helpers': 0.5.5
'@internationalized/message@3.1.4':
dependencies:
'@swc/helpers': 0.5.5
intl-messageformat: 10.5.14
'@internationalized/number@3.5.3':
dependencies:
'@swc/helpers': 0.5.5
'@internationalized/string@3.2.3':
dependencies:
'@swc/helpers': 0.5.5
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -6636,6 +6878,80 @@ snapshots:
'@next/swc-win32-x64-msvc@14.2.5':
optional: true
'@nextui-org/pagination@2.0.35(@nextui-org/system@2.2.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(framer-motion@10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@nextui-org/react-utils': 2.0.16(react@18.3.1)
'@nextui-org/shared-icons': 2.0.9(react@18.3.1)
'@nextui-org/shared-utils': 2.0.7
'@nextui-org/system': 2.2.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(framer-motion@10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@nextui-org/theme': 2.2.9(tailwindcss@3.4.6)
'@nextui-org/use-pagination': 2.0.9(react@18.3.1)
'@react-aria/focus': 3.17.1(react@18.3.1)
'@react-aria/i18n': 3.11.1(react@18.3.1)
'@react-aria/interactions': 3.21.3(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
scroll-into-view-if-needed: 3.0.10
'@nextui-org/react-rsc-utils@2.0.13': {}
'@nextui-org/react-utils@2.0.16(react@18.3.1)':
dependencies:
'@nextui-org/react-rsc-utils': 2.0.13
'@nextui-org/shared-utils': 2.0.7
react: 18.3.1
'@nextui-org/shared-icons@2.0.9(react@18.3.1)':
dependencies:
react: 18.3.1
'@nextui-org/shared-utils@2.0.7': {}
'@nextui-org/system-rsc@2.1.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(react@18.3.1)':
dependencies:
'@nextui-org/theme': 2.2.9(tailwindcss@3.4.6)
'@react-types/shared': 3.23.1(react@18.3.1)
clsx: 1.2.1
react: 18.3.1
'@nextui-org/system@2.2.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(framer-motion@10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@internationalized/date': 3.5.5
'@nextui-org/react-utils': 2.0.16(react@18.3.1)
'@nextui-org/system-rsc': 2.1.5(@nextui-org/theme@2.2.9(tailwindcss@3.4.6))(react@18.3.1)
'@react-aria/i18n': 3.11.1(react@18.3.1)
'@react-aria/overlays': 3.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
'@react-stately/utils': 3.10.1(react@18.3.1)
framer-motion: 10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@nextui-org/theme'
'@nextui-org/theme@2.2.9(tailwindcss@3.4.6)':
dependencies:
clsx: 1.2.1
color: 4.2.3
color2k: 2.0.3
deepmerge: 4.3.1
flat: 5.0.2
lodash.foreach: 4.5.0
lodash.get: 4.4.2
lodash.kebabcase: 4.1.1
lodash.mapkeys: 4.6.0
lodash.omit: 4.5.0
tailwind-merge: 1.14.0
tailwind-variants: 0.1.20(tailwindcss@3.4.6)
tailwindcss: 3.4.6
'@nextui-org/use-pagination@2.0.9(react@18.3.1)':
dependencies:
'@nextui-org/shared-utils': 2.0.7
'@react-aria/i18n': 3.11.1(react@18.3.1)
react: 18.3.1
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -7900,6 +8216,99 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@react-aria/focus@3.17.1(react@18.3.1)':
dependencies:
'@react-aria/interactions': 3.21.3(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
clsx: 2.1.1
react: 18.3.1
'@react-aria/focus@3.18.1(react@18.3.1)':
dependencies:
'@react-aria/interactions': 3.22.1(react@18.3.1)
'@react-aria/utils': 3.25.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
clsx: 2.1.1
react: 18.3.1
'@react-aria/i18n@3.11.1(react@18.3.1)':
dependencies:
'@internationalized/date': 3.5.5
'@internationalized/message': 3.1.4
'@internationalized/number': 3.5.3
'@internationalized/string': 3.2.3
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-aria/interactions@3.21.3(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-aria/interactions@3.22.1(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-aria/utils': 3.25.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-aria/overlays@3.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-aria/focus': 3.18.1(react@18.3.1)
'@react-aria/i18n': 3.11.1(react@18.3.1)
'@react-aria/interactions': 3.22.1(react@18.3.1)
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-aria/utils': 3.24.1(react@18.3.1)
'@react-aria/visually-hidden': 3.8.14(react@18.3.1)
'@react-stately/overlays': 3.6.9(react@18.3.1)
'@react-types/button': 3.9.6(react@18.3.1)
'@react-types/overlays': 3.8.9(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@react-aria/ssr@3.9.5(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-aria/utils@3.24.1(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-stately/utils': 3.10.2(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
clsx: 2.1.1
react: 18.3.1
'@react-aria/utils@3.25.1(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-stately/utils': 3.10.2(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
clsx: 2.1.1
react: 18.3.1
'@react-aria/visually-hidden@3.8.14(react@18.3.1)':
dependencies:
'@react-aria/interactions': 3.22.1(react@18.3.1)
'@react-aria/utils': 3.25.1(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-email/body@0.0.8(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -8024,6 +8433,41 @@ snapshots:
dependencies:
react: 18.3.1
'@react-stately/overlays@3.6.9(react@18.3.1)':
dependencies:
'@react-stately/utils': 3.10.2(react@18.3.1)
'@react-types/overlays': 3.8.9(react@18.3.1)
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-stately/utils@3.10.1(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-stately/utils@3.10.2(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.5
react: 18.3.1
'@react-types/button@3.9.6(react@18.3.1)':
dependencies:
'@react-types/shared': 3.24.1(react@18.3.1)
react: 18.3.1
'@react-types/overlays@3.8.9(react@18.3.1)':
dependencies:
'@react-types/shared': 3.24.1(react@18.3.1)
react: 18.3.1
'@react-types/shared@3.23.1(react@18.3.1)':
dependencies:
react: 18.3.1
'@react-types/shared@3.24.1(react@18.3.1)':
dependencies:
react: 18.3.1
'@resvg/resvg-wasm@2.4.0': {}
'@rushstack/eslint-patch@1.5.1': {}
@@ -8777,6 +9221,8 @@ snapshots:
color-name: 1.1.4
simple-swizzle: 0.2.2
color2k@2.0.3: {}
color@4.2.3:
dependencies:
color-convert: 2.0.1
@@ -8805,6 +9251,8 @@ snapshots:
array-ify: 1.0.0
dot-prop: 5.3.0
compute-scroll-into-view@3.1.0: {}
concat-map@0.0.1: {}
concurrently@8.2.2:
@@ -9604,6 +10052,8 @@ snapshots:
keyv: 4.5.4
rimraf: 3.0.2
flat@5.0.2: {}
flatted@3.2.9: {}
for-each@0.3.3:
@@ -9984,6 +10434,13 @@ snapshots:
internmap@2.0.3: {}
intl-messageformat@10.5.14:
dependencies:
'@formatjs/ecma402-abstract': 2.0.0
'@formatjs/fast-memoize': 2.2.0
'@formatjs/icu-messageformat-parser': 2.7.8
tslib: 2.6.2
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@@ -10240,14 +10697,22 @@ snapshots:
lodash.castarray@4.4.0: {}
lodash.foreach@4.5.0: {}
lodash.get@4.4.2: {}
lodash.isplainobject@4.0.6: {}
lodash.kebabcase@4.1.1: {}
lodash.mapkeys@4.6.0: {}
lodash.merge@4.6.2: {}
lodash.mergewith@4.6.2: {}
lodash.omit@4.5.0: {}
lodash.snakecase@4.1.1: {}
lodash.startcase@4.4.0: {}
@@ -11686,6 +12151,10 @@ snapshots:
ajv: 6.12.6
ajv-keywords: 3.5.2(ajv@6.12.6)
scroll-into-view-if-needed@3.0.10:
dependencies:
compute-scroll-into-view: 3.1.0
section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
@@ -11987,12 +12456,19 @@ snapshots:
react: 18.3.1
use-sync-external-store: 1.2.2(react@18.3.1)
tailwind-merge@1.14.0: {}
tailwind-merge@2.2.0:
dependencies:
'@babel/runtime': 7.24.4
tailwind-merge@2.4.0: {}
tailwind-variants@0.1.20(tailwindcss@3.4.6):
dependencies:
tailwind-merge: 1.14.0
tailwindcss: 3.4.6
tailwindcss-animate@1.0.7(tailwindcss@3.4.6):
dependencies:
tailwindcss: 3.4.6