feat: support send email

This commit is contained in:
oiov
2025-04-01 17:30:05 +08:00
parent 13c0b6a805
commit 2e248df1f6
21 changed files with 702 additions and 68 deletions
+76 -8
View File
@@ -5,7 +5,6 @@ import { User } from "@prisma/client";
import { PenLine, RefreshCwIcon } from "lucide-react";
import useSWR, { useSWRConfig } from "swr";
import { siteConfig } from "@/config/site";
import { fetcher, timeAgo } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Badge } from "@/components/ui/badge";
@@ -15,8 +14,8 @@ import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
@@ -33,9 +32,9 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import StatusDot from "@/components/dashboard/status-dot";
import { FormType } from "@/components/forms/record-form";
import { 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 CountUpFn from "../../../../components/dashboard/count-up";
@@ -46,7 +45,7 @@ export interface UrlListProps {
function TableColumnSekleton({ className }: { className?: string }) {
return (
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-7">
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-8">
<TableCell className="col-span-1">
<Skeleton className="h-5 w-20" />
</TableCell>
@@ -62,6 +61,9 @@ function TableColumnSekleton({ className }: { className?: string }) {
<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>
@@ -75,10 +77,14 @@ export default function UsersList({ user }: UrlListProps) {
const [currentEditUser, setcurrentEditUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchParams, setSearchParams] = useState({
email: "",
userName: "",
});
const { mutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<{ total: number; list: User[] }>(
`/api/user/admin?page=${currentPage}&size=${pageSize}`,
`/api/user/admin?page=${currentPage}&size=${pageSize}&email=${searchParams.email}&userName=${searchParams.userName}`,
fetcher,
{
revalidateOnFocus: false,
@@ -86,7 +92,10 @@ export default function UsersList({ user }: UrlListProps) {
);
const handleRefresh = () => {
mutate(`/api/user/admin?page=${currentPage}&size=${pageSize}`, undefined);
mutate(
`/api/user/admin?page=${currentPage}&size=${pageSize}&email=${searchParams.email}&userName=${searchParams.userName}`,
undefined,
);
};
return (
@@ -124,9 +133,60 @@ export default function UsersList({ user }: UrlListProps) {
onRefresh={handleRefresh}
/>
)}
<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-7">
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-8">
<TableHead className="col-span-1 flex items-center font-bold">
Name
</TableHead>
@@ -136,6 +196,9 @@ export default function UsersList({ user }: UrlListProps) {
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
Role
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
Plan
</TableHead>
<TableHead className="col-span-1 hidden items-center justify-center font-bold sm:flex">
Status
</TableHead>
@@ -158,7 +221,7 @@ export default function UsersList({ user }: UrlListProps) {
data.list.map((user) => (
<TableRow
key={user.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-8"
>
<TableCell className="col-span-1 truncate">
<TooltipProvider>
@@ -187,6 +250,11 @@ export default function UsersList({ user }: UrlListProps) {
{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">
<StatusDot status={user.active} />
</TableCell>
+7 -3
View File
@@ -5,6 +5,7 @@ import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
import { getAllUserEmailsCount } from "@/lib/dto/email";
import { getUserShortUrlCount } from "@/lib/dto/short-urls";
import { getCurrentUser } from "@/lib/session";
import { Team_Plan_Quota } from "@/lib/team";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
@@ -35,12 +36,15 @@ export default async function DashboardPage() {
<DashboardHeader heading="Dashboard" text="" />
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 xl:grid-cols-3">
<HeroCard count={email_count} total={siteConfig.freeQuota.url} />
<HeroCard
count={email_count}
total={Team_Plan_Quota[user.team].EM_EmailAddresses}
/>
<DashboardInfoCard
userId={user.id}
title="DNS Records"
count={record_count}
total={siteConfig.freeQuota.record}
total={Team_Plan_Quota[user.team].RC_NewRecords}
link="/dashboard/records"
icon="globeLock"
/>
@@ -48,7 +52,7 @@ export default async function DashboardPage() {
userId={user.id}
title="Short URLs"
count={url_count}
total={siteConfig.freeQuota.url}
total={Team_Plan_Quota[user.team].SL_NewLinks}
link="/dashboard/urls"
icon="link"
/>
+35
View File
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { resend } from "@/lib/email";
import { isValidEmail } from "@/lib/utils";
export async function POST(req: NextRequest) {
try {
const { from, to, subject, html } = await req.json();
if (!from || !to || !subject || !html) {
return NextResponse.json("Missing required fields", { status: 400 });
}
if (!isValidEmail(from) || !isValidEmail(to)) {
return NextResponse.json("Invalid email address", { status: 403 });
}
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
});
if (error) {
console.log("Resend error:", error);
return NextResponse.json("Failed to send email", { status: 500 });
}
return NextResponse.json("success", { status: 200 });
} catch (error) {
console.log("Error sending email:", error);
return NextResponse.json("Internal server error", { status: 500 });
}
}
+8 -1
View File
@@ -16,7 +16,14 @@ export async function GET(req: Request) {
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"));
const email = url.searchParams.get("email") || "";
const userName = url.searchParams.get("userName") || "";
const data = await getAllUsers(
Number(page || "1"),
Number(size || "10"),
email,
userName,
);
return Response.json(data);
} catch (error) {
+4 -4
View File
@@ -1,9 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { User } from "@prisma/client";
import { useMediaQuery } from "@/hooks/use-media-query";
// import { useMediaQuery } from "@/hooks/use-media-query";
import EmailList from "@/components/email/EmailList";
import EmailSidebar from "@/components/email/EmailSidebar";
@@ -13,7 +13,7 @@ export function EmailDashboard({ user }: { user: User }) {
>(null);
const [selectedEmailId, setSelectedEmailId] = useState<string | null>(null);
const { isTablet } = useMediaQuery();
// const { isTablet } = useMediaQuery();
const [isCollapsed, setIsCollapsed] = useState(false);
// useEffect(() => {
@@ -23,7 +23,7 @@ export function EmailDashboard({ user }: { user: User }) {
return (
<div className="flex h-[calc(100vh-60px)]">
<EmailSidebar
className={!isCollapsed ? "w-56 xl:w-64" : "w-16"}
className={!isCollapsed ? "w-64 xl:w-72" : "w-16"}
user={user}
onSelectEmail={setSelectedEmailAddress}
selectedEmailAddress={selectedEmailAddress}
+1 -1
View File
@@ -65,7 +65,7 @@ export const {
token.picture = dbUser.image;
token.role = dbUser.role;
token.active = dbUser.active;
token.team = dbUser.team;
token.team = dbUser.team || "free";
token.apiKey = dbUser.apiKey;
return token;
+8 -5
View File
@@ -220,15 +220,15 @@ export default function EmailDetail({
<h3 className="mb-2 text-sm font-semibold text-neutral-700 dark:text-neutral-400">
Attachments ({attachments.length})
</h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
{attachments.map((attachment, index) => {
const FileIcon = getFileIcon(attachment.mimeType); // 动态获取图标
return (
<div
key={index}
className="group flex items-center justify-between rounded-md border border-dotted bg-gray-100 p-2 transition-shadow hover:border-dashed dark:bg-neutral-800"
className="group relative flex items-center justify-between rounded-md border border-dotted bg-gray-100 p-2 transition-shadow hover:border-dashed dark:bg-neutral-800"
>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 overflow-hidden">
{attachment.mimeType.startsWith("image/") ? (
<BlurImg
src={`${siteConfig.emailR2Domain}/${attachment.r2Path}`}
@@ -244,7 +244,10 @@ export default function EmailDetail({
<FileIcon className="size-4 text-neutral-500 dark:text-neutral-400" />
)}
<div>
<p className="max-w-[120px] truncate text-xs text-neutral-800 dark:text-neutral-400">
<p
className="max-w-full truncate text-xs text-neutral-800 dark:text-neutral-400"
title={attachment.filename}
>
{attachment.filename}
</p>
<p className="text-xs text-neutral-500">
@@ -256,7 +259,7 @@ export default function EmailDetail({
</div>
<Button
onClick={() => handleDownload(attachment)}
className="hidden animate-fade-in px-2 group-hover:block"
className="absolute right-0 top-0 hidden transform animate-fade-in px-2 group-hover:block"
size="sm"
variant="ghost"
>
+136 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useTransition } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { ForwardEmail } from "@prisma/client";
import { toast } from "sonner";
@@ -12,8 +13,11 @@ import { Icons } from "../shared/icons";
import { PaginationWrapper } from "../shared/pagination";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Modal } from "../ui/modal";
import { Skeleton } from "../ui/skeleton";
import { Switch } from "../ui/switch";
import { Textarea } from "../ui/textarea";
import {
Tooltip,
TooltipContent,
@@ -23,6 +27,19 @@ import {
import EmailDetail from "./EmailDetail";
import Loader from "./Loader";
import "react-quill/dist/quill.snow.css";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "../ui/drawer";
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
interface EmailListProps {
emailAddress: string | null;
selectedEmailId: string | null;
@@ -40,6 +57,9 @@ export default function EmailList({
const [isAutoRefresh, setIsAutoRefresh] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [showSendDrawer, setShowSendDrawer] = useState(false);
const [sendForm, setSendForm] = useState({ to: "", subject: "", html: "" });
const [isPending, startTransition] = useTransition();
const { data, error, isLoading, mutate } = useSWR<{
total: number;
@@ -217,7 +237,44 @@ export default function EmailList({
};
const handleOpenSendEmailModal = () => {
toast.warning(`Work in progress...`);
setShowSendDrawer(true);
setSendForm({ to: "", subject: "", html: "" });
};
const handleSendEmail = async () => {
if (!emailAddress) {
toast.error("No email address selected");
return;
}
if (!sendForm.to || !sendForm.subject || !sendForm.html) {
toast.error("Please fill in all fields");
return;
}
startTransition(async () => {
try {
const response = await fetch("/api/email/send", {
method: "POST",
body: JSON.stringify({
from: emailAddress,
to: sendForm.to,
subject: sendForm.subject,
html: sendForm.html,
}),
});
if (response.ok) {
toast.success("Email sent successfully");
setShowSendDrawer(false);
} else {
toast.error("Failed to send email", {
description: await response.text(),
});
}
} catch (error) {
toast.error(error.message || "Error sending email");
}
});
};
return (
@@ -344,6 +401,83 @@ export default function EmailList({
setCurrentPage={setCurrentPage}
/>
)}
{/* 发送邮件 Modal */}
<Drawer open={showSendDrawer} onOpenChange={setShowSendDrawer}>
<DrawerContent className="fixed bottom-0 right-0 top-0 w-full rounded-none sm:max-w-xl">
<DrawerHeader>
<DrawerTitle className="flex items-center gap-1">
Send Email{" "}
<Icons.help className="size-5 text-neutral-600 hover:text-neutral-400" />
</DrawerTitle>
<DrawerClose asChild>
<Button variant="ghost" className="absolute right-4 top-4">
<Icons.close className="h-4 w-4" />
</Button>
</DrawerClose>
</DrawerHeader>
<div className="scrollbar-hidden h-[calc(100vh)] space-y-4 overflow-y-auto p-6">
<div>
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
From
</label>
<Input value={emailAddress || ""} disabled className="mt-1" />
</div>
<div>
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
To
</label>
<Input
value={sendForm.to}
onChange={(e) =>
setSendForm({ ...sendForm, to: e.target.value })
}
placeholder="recipient@example.com"
className="mt-1"
/>
</div>
<div>
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Subject
</label>
<Input
value={sendForm.subject}
onChange={(e) =>
setSendForm({ ...sendForm, subject: e.target.value })
}
placeholder="Enter subject"
className="mt-1"
/>
</div>
<div>
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Content
</label>
<ReactQuill
value={sendForm.html}
onChange={(value) => setSendForm({ ...sendForm, html: value })}
className="mt-1 h-40 rounded-lg"
theme="snow"
placeholder="Enter your message"
/>
</div>
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline" disabled={isPending}>
Cancel
</Button>
</DrawerClose>
<Button
onClick={handleSendEmail}
disabled={isPending}
variant={"default"}
>
{isPending ? "Sending..." : "Send"}
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</div>
);
}
+2 -6
View File
@@ -1,7 +1,6 @@
"use client";
import { useEffect, useState, useTransition } from "react";
import Link from "next/link";
import { useState, useTransition } from "react";
import { User, UserEmail } from "@prisma/client";
import randomName from "@scaleway/random-name";
import {
@@ -18,7 +17,6 @@ import useSWRInfinite from "swr/infinite";
import { siteConfig } from "@/config/site";
import { UserEmailList } from "@/lib/dto/email";
import { cn, fetcher, timeAgo } from "@/lib/utils";
import { useMediaQuery } from "@/hooks/use-media-query";
import { CopyButton } from "../shared/copy-button";
import { EmptyPlaceholder } from "../shared/empty-placeholder";
@@ -79,8 +77,7 @@ export default function EmailSidebar({
const { data, isLoading, error, size, setSize, mutate } = useSWRInfinite<{
list: UserEmailList[];
total: number;
}>(getKey, fetcher, { revalidateOnFocus: false, dedupingInterval: 3000 });
console.log("[数据]", data);
}>(getKey, fetcher, { dedupingInterval: 3000 });
if (
!selectedEmailAddress &&
@@ -208,7 +205,6 @@ export default function EmailSidebar({
<div
className={cn(`flex h-full flex-col border-r transition-all`, className)}
>
{isLoading}
{/* Header */}
<div className="border-b p-2 text-center">
<div className="mb-2 flex items-center justify-center gap-2">
+36 -16
View File
@@ -60,7 +60,7 @@ export function UserForm({
email: initData?.email || "",
image: initData?.image || "",
role: initData?.role || "USER",
team: initData?.team || "",
team: initData?.team || "free",
},
});
@@ -151,6 +151,22 @@ export function UserForm({
</p>
)}
</FormSectionColumns>
<FormSectionColumns title="Active">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="active">
Active
</Label>
<Switch
id="active"
{...register("active")}
defaultChecked={initData?.active === 1}
onCheckedChange={(value) => setValue("active", value ? 1 : 0)}
/>
</div>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Role">
<Select
onValueChange={(value: string) => {
@@ -171,21 +187,25 @@ export function UserForm({
</SelectContent>
</Select>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Active">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="active">
Active
</Label>
<Switch
id="active"
{...register("active")}
defaultChecked={initData?.active === 1}
onCheckedChange={(value) => setValue("active", value ? 1 : 0)}
/>
</div>
<FormSectionColumns title="Plan">
<Select
onValueChange={(value: string) => {
setValue("team", value);
}}
name="team"
defaultValue={`${initData?.team}` || "free"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{["free", "premium", "business"].map((role) => (
<SelectItem key={role} value={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
</FormSectionColumns>
</div>
+118
View File
@@ -0,0 +1,118 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed bottom-0 right-0 z-50 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
+11 -11
View File
@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -14,14 +14,14 @@ const ScrollArea = React.forwardRef<
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
<ScrollAreaPrimitive.Viewport className="rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
@@ -36,13 +36,13 @@ const ScrollBar = React.forwardRef<
"h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-px",
className
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };
+4
View File
@@ -0,0 +1,4 @@
---
title: Team Plan
description: Three different plans to choose from
---
+21 -2
View File
@@ -33,16 +33,35 @@ export const getUserById = async (id: string) => {
}
};
export const getAllUsers = async (page: number, size: number) => {
export const getAllUsers = async (
page: number,
size: number,
email?: string,
userName?: string,
) => {
try {
let options;
if (email) {
options = { where: { email: { contains: email } } };
}
if (userName) {
options = { where: { name: { contains: userName } } };
}
if (email && userName) {
options = {
where: { email: { contains: email }, name: { contains: userName } },
};
}
const [total, list] = await prisma.$transaction([
prisma.user.count(), // 获取所有用户的总数
prisma.user.count(options),
prisma.user.findMany({
skip: (page - 1) * size,
take: size,
orderBy: {
createdAt: "desc",
},
...options,
}),
]);
return {
+38
View File
@@ -0,0 +1,38 @@
export const Team_Plan_Quota = {
free: {
SL_TrackedClicks: 100000,
SL_NewLinks: 1000,
SL_AnalyticsRetention: 180,
SL_Domains: 1,
SL_AdvancedAnalytics: true,
RC_NewRecords: 3,
EM_EmailAddresses: 1000,
EM_Domains: 1,
APP_Support: "basic",
APP_ApiAccess: true,
},
premium: {
SL_TrackedClicks: 10000000,
SL_NewLinks: 10000,
SL_AnalyticsRetention: 360,
SL_Domains: 3,
SL_AdvancedAnalytics: true,
RC_NewRecords: 3,
EM_EmailAddresses: 10000,
EM_Domains: 3,
APP_Support: "live",
APP_ApiAccess: true,
},
business: {
SL_TrackedClicks: 10000000,
SL_NewLinks: 10000,
SL_AnalyticsRetention: 360,
SL_Domains: 3,
SL_AdvancedAnalytics: true,
RC_NewRecords: 3,
EM_EmailAddresses: 10000,
EM_Domains: 3,
APP_Support: "live",
APP_ApiAccess: true,
},
};
+3
View File
@@ -147,6 +147,9 @@ export async function fetcher<JSON = any>(
return res.json();
}
export const isValidEmail = (email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
export function nFormatter(num: number, digits?: number) {
if (!num) return "0";
const lookup = [
+1
View File
@@ -89,6 +89,7 @@
"react-dom": "18.3.1",
"react-email": "2.1.5",
"react-hook-form": "^7.52.1",
"react-quill": "^2.0.0",
"react-textarea-autosize": "^8.5.3",
"recharts": "^2.12.7",
"resend": "^3.4.0",
+190 -6
View File
@@ -218,6 +218,9 @@ importers:
react-hook-form:
specifier: ^7.52.1
version: 7.52.1(react@18.3.1)
react-quill:
specifier: ^2.0.0
version: 2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-textarea-autosize:
specifier: ^8.5.3
version: 8.5.3(@types/react@18.3.3)(react@18.3.1)
@@ -3174,6 +3177,9 @@ packages:
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/quill@1.3.10':
resolution: {integrity: sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==}
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
@@ -3661,10 +3667,18 @@ packages:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -3767,6 +3781,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
@@ -4184,6 +4202,10 @@ packages:
decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
deep-equal@1.1.2:
resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
engines: {node: '>= 0.4'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -4268,6 +4290,10 @@ packages:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
@@ -4340,6 +4366,10 @@ packages:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
@@ -4355,6 +4385,10 @@ packages:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.0.3:
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
engines: {node: '>= 0.4'}
@@ -4561,6 +4595,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
eventemitter3@2.0.3:
resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@@ -4589,6 +4626,9 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-diff@1.1.2:
resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==}
fast-equals@5.0.1:
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
engines: {node: '>=6.0.0'}
@@ -4729,6 +4769,10 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
@@ -4736,6 +4780,10 @@ packages:
get-own-enumerable-property-symbols@3.0.2:
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
@@ -4822,6 +4870,10 @@ packages:
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -4858,6 +4910,10 @@ packages:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
@@ -5012,6 +5068,10 @@ packages:
is-alphanumerical@2.0.1:
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
is-arguments@1.2.0:
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
engines: {node: '>= 0.4'}
is-array-buffer@3.0.4:
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
engines: {node: '>= 0.4'}
@@ -5451,6 +5511,10 @@ packages:
engines: {node: '>= 16'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
md-to-react-email@5.0.2:
resolution: {integrity: sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==}
peerDependencies:
@@ -5856,6 +5920,10 @@ packages:
object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
object-is@1.1.6:
resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
engines: {node: '>= 0.4'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -5937,6 +6005,9 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
parchment@1.1.4:
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -6246,6 +6317,13 @@ packages:
quickselect@2.0.0:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
quill-delta@3.6.3:
resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==}
engines: {node: '>=0.10'}
quill@1.3.7:
resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -6282,6 +6360,12 @@ packages:
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
react-quill@2.0.0:
resolution: {integrity: sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
react-remove-scroll-bar@2.3.4:
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
engines: {node: '>=10'}
@@ -10689,6 +10773,10 @@ snapshots:
dependencies:
'@types/node': 20.14.11
'@types/quill@1.3.10':
dependencies:
parchment: 1.1.4
'@types/react-dom@18.3.0':
dependencies:
'@types/react': 18.3.3
@@ -11295,6 +11383,11 @@ snapshots:
dependencies:
streamsearch: 1.1.0
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.0
@@ -11303,6 +11396,11 @@ snapshots:
get-intrinsic: 1.2.4
set-function-length: 1.2.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
callsites@3.1.0: {}
camel-case@4.1.2:
@@ -11411,6 +11509,8 @@ snapshots:
clone@1.0.4: {}
clone@2.1.2: {}
clsx@1.2.1: {}
clsx@2.0.0: {}
@@ -11840,6 +11940,15 @@ snapshots:
dependencies:
character-entities: 2.0.2
deep-equal@1.1.2:
dependencies:
is-arguments: 1.2.0
is-date-object: 1.0.5
is-regex: 1.1.4
object-is: 1.1.6
object-keys: 1.1.1
regexp.prototype.flags: 1.5.2
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
@@ -11931,6 +12040,12 @@ snapshots:
dotenv@16.0.3: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
earcut@2.2.4: {}
eastasianwidth@0.2.0: {}
@@ -12057,6 +12172,8 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-iterator-helpers@1.0.19:
@@ -12082,6 +12199,10 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.0.3:
dependencies:
get-intrinsic: 1.2.4
@@ -12144,7 +12265,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -12180,8 +12301,8 @@ snapshots:
debug: 4.3.4
enhanced-resolve: 5.15.0
eslint: 8.57.0
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
is-core-module: 2.13.1
@@ -12192,7 +12313,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -12203,7 +12324,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.3
@@ -12213,7 +12334,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -12399,6 +12520,8 @@ snapshots:
esutils@2.0.3: {}
eventemitter3@2.0.3: {}
eventemitter3@4.0.7: {}
events@3.3.0: {}
@@ -12437,6 +12560,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-diff@1.1.2: {}
fast-equals@5.0.1: {}
fast-glob@3.3.2:
@@ -12575,10 +12700,28 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.2
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-nonce@1.0.1: {}
get-own-enumerable-property-symbols@3.0.2: {}
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@6.0.1: {}
get-stream@8.0.1: {}
@@ -12688,6 +12831,8 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
@@ -12715,6 +12860,8 @@ snapshots:
has-symbols@1.0.3: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.0.3
@@ -12941,6 +13088,11 @@ snapshots:
is-alphabetical: 2.0.1
is-decimal: 2.0.1
is-arguments@1.2.0:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-array-buffer@3.0.4:
dependencies:
call-bind: 1.0.7
@@ -13343,6 +13495,8 @@ snapshots:
marked@7.0.4: {}
math-intrinsics@1.1.0: {}
md-to-react-email@5.0.2(react@18.3.1):
dependencies:
marked: 7.0.4
@@ -14043,6 +14197,11 @@ snapshots:
object-inspect@1.13.1: {}
object-is@1.1.6:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
object-keys@1.1.1: {}
object.assign@4.1.5:
@@ -14143,6 +14302,8 @@ snapshots:
pako@0.2.9: {}
parchment@1.1.4: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -14396,6 +14557,21 @@ snapshots:
quickselect@2.0.0: {}
quill-delta@3.6.3:
dependencies:
deep-equal: 1.1.2
extend: 3.0.2
fast-diff: 1.1.2
quill@1.3.7:
dependencies:
clone: 2.1.2
deep-equal: 1.1.2
eventemitter3: 2.0.3
extend: 3.0.2
parchment: 1.1.4
quill-delta: 3.6.3
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@@ -14481,6 +14657,14 @@ snapshots:
dependencies:
fast-deep-equal: 2.0.1
react-quill@2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@types/quill': 1.3.10
lodash: 4.17.21
quill: 1.3.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll-bar@2.3.4(@types/react@18.2.47)(react@18.3.1):
dependencies:
react: 18.3.1
@@ -43,7 +43,7 @@ CREATE TABLE "users"
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"active" INTEGER NOT NULL DEFAULT 1,
"team" TEXT,
"team" TEXT NOT NULL DEFAULT 'free',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"role" "UserRole" NOT NULL DEFAULT 'USER',
+1 -1
View File
@@ -56,7 +56,7 @@ model User {
emailVerified DateTime?
image String?
active Int @default(1) // 0 封禁,1 正常
team String?
team String? @default("free")
apiKey String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
+1 -1
View File
File diff suppressed because one or more lines are too long