feat: add landing page
This commit is contained in:
parent
21a17cb753
commit
ef050b5e55
@ -18,3 +18,4 @@ SPACES_KEY="xyz"
|
|||||||
SPACES_SECRET="aaa"
|
SPACES_SECRET="aaa"
|
||||||
|
|
||||||
JWT_SECRET="openssl rand -hex 32"
|
JWT_SECRET="openssl rand -hex 32"
|
||||||
|
PLAUSIBLE_DOMAIN=
|
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
23
web/next-gen/next-gen.config.json
Normal file
23
web/next-gen/next-gen.config.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "Comfy Deploy",
|
||||||
|
"og:title": "Comfy Deploy | Stable Diffusion from your terminal to the world",
|
||||||
|
"og:description": "",
|
||||||
|
"author": "BennyKok",
|
||||||
|
"tagline": "Stable Diffusion from your terminal to the world",
|
||||||
|
"description": "Manage all your open development ports and remote Vercel deployment.",
|
||||||
|
"pricingPlan": [
|
||||||
|
{
|
||||||
|
"name": "Free",
|
||||||
|
"description": "Free",
|
||||||
|
"prices": [
|
||||||
|
{
|
||||||
|
"price": 0,
|
||||||
|
"currency": "usd",
|
||||||
|
"pricingType": "month",
|
||||||
|
"paymentLink": "",
|
||||||
|
"paymentLink_Prod": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -20,6 +20,7 @@
|
|||||||
"@clerk/nextjs": "^4.27.4",
|
"@clerk/nextjs": "^4.27.4",
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
"@neondatabase/serverless": "^0.6.0",
|
"@neondatabase/serverless": "^0.6.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
|
"next-plausible": "^3.12.0",
|
||||||
"next-usequerystate": "^1.13.2",
|
"next-usequerystate": "^1.13.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
@ -4,14 +4,27 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { ClerkProvider, UserButton } from "@clerk/nextjs";
|
import { ClerkProvider, UserButton } from "@clerk/nextjs";
|
||||||
import { Github } from "lucide-react";
|
import { Github } from "lucide-react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import PlausibleProvider from "next-plausible";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
import meta from 'next-gen/config';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Comfy Deploy",
|
title: meta['og:title'],
|
||||||
description: "Generated by create next app",
|
description: meta['og:description'],
|
||||||
|
|
||||||
|
category: 'technology',
|
||||||
|
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
title: meta['og:title'],
|
||||||
|
description: meta['og:description'],
|
||||||
|
locale: 'en_US',
|
||||||
|
images: '/og.jpg',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -22,12 +35,17 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
|
<head>
|
||||||
|
{process.env.PLAUSIBLE_DOMAIN && (
|
||||||
|
<PlausibleProvider domain={process.env.PLAUSIBLE_DOMAIN} />
|
||||||
|
)}
|
||||||
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="w-full flex min-h-[100dvh] flex-col items-center justify-start">
|
||||||
<div className="w-full h-18 flex items-center justify-between gap-4 p-4 border-b border-gray-200">
|
<div className="w-full h-18 flex items-center justify-between gap-4 p-4 border-b border-gray-200">
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
<a className="font-bold text-lg hover:underline" href="/">
|
<a className="font-bold text-md md:text-lg hover:underline" href="/">
|
||||||
ComfyUI Deploy
|
{meta.name}
|
||||||
</a>
|
</a>
|
||||||
<NavbarRight />
|
<NavbarRight />
|
||||||
</div>
|
</div>
|
||||||
@ -38,14 +56,17 @@ export default function RootLayout({
|
|||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className="rounded-full aspect-square p-2"
|
className="rounded-full aspect-square p-2"
|
||||||
>
|
>
|
||||||
<a target="_blank" href="https://github.com/BennyKok/comfyui-deploy">
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/BennyKok/comfyui-deploy"
|
||||||
|
>
|
||||||
<Github />
|
<Github />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* <div></div> */}
|
{/* <div></div> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="md:px-10 px-6 w-full flex items-start">
|
<div className="md:px-10 px-6 w-full h-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
|
@ -1,77 +1,5 @@
|
|||||||
import { WorkflowList } from "@/components/WorkflowList";
|
import Main from '@/components/Main';
|
||||||
import { db } from "@/db/db";
|
|
||||||
import { usersTable, workflowTable, workflowVersionTable } from "@/db/schema";
|
|
||||||
import { auth, clerkClient } from "@clerk/nextjs";
|
|
||||||
import { desc, eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <WorkflowServer />;
|
return <Main />;
|
||||||
}
|
|
||||||
|
|
||||||
async function WorkflowServer() {
|
|
||||||
const { userId } = await auth();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return <div>No auth</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await db.query.usersTable.findFirst({
|
|
||||||
where: eq(usersTable.id, userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
await setInitialUserData(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflow = await db.query.workflowTable.findMany({
|
|
||||||
// extras: {
|
|
||||||
// count: sql<number>`(select count(*) from ${workflowVersionTable})`.as(
|
|
||||||
// "count",
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
with: {
|
|
||||||
versions: {
|
|
||||||
limit: 1,
|
|
||||||
orderBy: desc(workflowVersionTable.version),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: desc(workflowTable.updated_at),
|
|
||||||
where: eq(workflowTable.user_id, userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkflowList
|
|
||||||
data={workflow.map((x) => {
|
|
||||||
return {
|
|
||||||
id: x.id,
|
|
||||||
email: x.name,
|
|
||||||
amount: x.versions[0]?.version ?? 0,
|
|
||||||
date: x.updated_at,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setInitialUserData(userId: string) {
|
|
||||||
const user = await clerkClient.users.getUser(userId);
|
|
||||||
|
|
||||||
// incase we dont have username such as google login, fallback to first name + last name
|
|
||||||
const usernameFallback =
|
|
||||||
user.username ?? (user.firstName ?? "") + (user.lastName ?? "");
|
|
||||||
|
|
||||||
// For the display name, if it for some reason is empty, fallback to username
|
|
||||||
let nameFallback = (user.firstName ?? "") + (user.lastName ?? "");
|
|
||||||
if (nameFallback === "") {
|
|
||||||
nameFallback = usernameFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.insert(usersTable).values({
|
|
||||||
id: userId,
|
|
||||||
// this is used for path, make sure this is unique
|
|
||||||
username: usernameFallback,
|
|
||||||
|
|
||||||
// this is for display name, maybe different from username
|
|
||||||
name: nameFallback,
|
|
||||||
});
|
|
||||||
}
|
}
|
77
web/src/app/workflows/page.tsx
Normal file
77
web/src/app/workflows/page.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { WorkflowList } from "@/components/WorkflowList";
|
||||||
|
import { db } from "@/db/db";
|
||||||
|
import { usersTable, workflowTable, workflowVersionTable } from "@/db/schema";
|
||||||
|
import { auth, clerkClient } from "@clerk/nextjs";
|
||||||
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <WorkflowServer />;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function WorkflowServer() {
|
||||||
|
const { userId } = await auth();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return <div>No auth</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.query.usersTable.findFirst({
|
||||||
|
where: eq(usersTable.id, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await setInitialUserData(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = await db.query.workflowTable.findMany({
|
||||||
|
// extras: {
|
||||||
|
// count: sql<number>`(select count(*) from ${workflowVersionTable})`.as(
|
||||||
|
// "count",
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
with: {
|
||||||
|
versions: {
|
||||||
|
limit: 1,
|
||||||
|
orderBy: desc(workflowVersionTable.version),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: desc(workflowTable.updated_at),
|
||||||
|
where: eq(workflowTable.user_id, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowList
|
||||||
|
data={workflow.map((x) => {
|
||||||
|
return {
|
||||||
|
id: x.id,
|
||||||
|
email: x.name,
|
||||||
|
amount: x.versions[0]?.version ?? 0,
|
||||||
|
date: x.updated_at,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setInitialUserData(userId: string) {
|
||||||
|
const user = await clerkClient.users.getUser(userId);
|
||||||
|
|
||||||
|
// incase we dont have username such as google login, fallback to first name + last name
|
||||||
|
const usernameFallback =
|
||||||
|
user.username ?? (user.firstName ?? "") + (user.lastName ?? "");
|
||||||
|
|
||||||
|
// For the display name, if it for some reason is empty, fallback to username
|
||||||
|
let nameFallback = (user.firstName ?? "") + (user.lastName ?? "");
|
||||||
|
if (nameFallback === "") {
|
||||||
|
nameFallback = usernameFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.insert(usersTable).values({
|
||||||
|
id: userId,
|
||||||
|
// this is used for path, make sure this is unique
|
||||||
|
username: usernameFallback,
|
||||||
|
|
||||||
|
// this is for display name, maybe different from username
|
||||||
|
name: nameFallback,
|
||||||
|
});
|
||||||
|
}
|
100
web/src/components/Main.tsx
Normal file
100
web/src/components/Main.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import macBookMainImage from "@/assets/images/macbook-main.png";
|
||||||
|
import { Section } from "@/components/Section";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import meta from 'next-gen/config';
|
||||||
|
|
||||||
|
function isDevelopment() {
|
||||||
|
return process.env.NODE_ENV === "development";
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureCard(props: {
|
||||||
|
className?: String;
|
||||||
|
title: React.ReactNode;
|
||||||
|
description: String;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative text-center bg-opacity-20 rounded-lg py-6 ring-1 shadow-sm ring-stone-200/50 overflow-hidden",
|
||||||
|
// props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* <div className="z-[1] top-0 absolute h-full w-full bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div> */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"opacity-60 group-hover:opacity-100 transition-all -z-[5] absolute top-0 h-full w-full duration-700",
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div className="opacity-60 group-hover:opacity-100 absolute top-0 inset-0 -z-[5] h-full w-full bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
<div className="font-mono text-lg ">{props.title}</div>
|
||||||
|
<div className="divider px-4 py-0 h-[1px] opacity-30 my-2"></div>
|
||||||
|
<div className="px-8 text-stone-800 ">{props.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Main() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="flex flex-col items-center gap-10">
|
||||||
|
{/* Hero Section */}
|
||||||
|
|
||||||
|
<Section className="items-left min-h-[calc(100dvh-60px)] flex-col ">
|
||||||
|
<div className="flex flex-col justify-center gap-2">
|
||||||
|
<Section.Announcement
|
||||||
|
className="text-sm"
|
||||||
|
href="https://github.com/BennyKok/comfyui-deploy"
|
||||||
|
>
|
||||||
|
✨ Open Source on Github
|
||||||
|
</Section.Announcement>
|
||||||
|
|
||||||
|
<Section.Title className="text-left">
|
||||||
|
<span className="text-6xl md:text-7xl pb-2 inline-flex animate-background-shine bg-[linear-gradient(110deg,#1e293b,45%,#939393,55%,#1e293b)] bg-[length:250%_100%] bg-clip-text text-transparent">
|
||||||
|
{meta.tagline}
|
||||||
|
</span>
|
||||||
|
</Section.Title>
|
||||||
|
|
||||||
|
<Section.Subtitle className="text-left">
|
||||||
|
{meta.description}
|
||||||
|
</Section.Subtitle>
|
||||||
|
|
||||||
|
<Section.PrimaryAction
|
||||||
|
href="/workflows"
|
||||||
|
className="mt-10 px-8 py-8 rounded-2xl w-fit text-lg font-bold"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</Section.PrimaryAction>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="z-[-10] flex items-center mt-8 mb-4 w-full">
|
||||||
|
{/* <Image
|
||||||
|
loading="eager"
|
||||||
|
// placeholder="blur"
|
||||||
|
// blurDataURL="data:image/webp;base64,LPFO]k}w-Rn1F,K-NjR#-UwDf1o*"
|
||||||
|
className="shadow-lg object-contain object-top w-full rounded-2xl h-fit"
|
||||||
|
src={macBookMainImage}
|
||||||
|
alt="Find My Ports on MacBook Pro 14"
|
||||||
|
></Image> */}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="text-base-content mx-auto flex flex-col md:flex-row items-center justify-center w-full max-w-5xl gap-4 p-10 ">
|
||||||
|
{/* <div className="md:col-span-4"> */}
|
||||||
|
<div className="font-bold">{meta.name}</div>
|
||||||
|
<div>© {meta.author} 2023 . All rights reserved.</div>
|
||||||
|
{/* </div> */}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -15,7 +15,8 @@ export function NavbarRight() {
|
|||||||
? "machines"
|
? "machines"
|
||||||
: pathname.startsWith("/api-keys")
|
: pathname.startsWith("/api-keys")
|
||||||
? "api-keys"
|
? "api-keys"
|
||||||
: "workflow"
|
: pathname.startsWith("/workflows")
|
||||||
|
? "workflows" : ""
|
||||||
}
|
}
|
||||||
className="w-[300px]"
|
className="w-[300px]"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@ -24,12 +25,12 @@ export function NavbarRight() {
|
|||||||
} else if (value === "api-keys") {
|
} else if (value === "api-keys") {
|
||||||
router.push("/api-keys");
|
router.push("/api-keys");
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
router.push("/workflows");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="workflow">Workflow</TabsTrigger>
|
<TabsTrigger value="workflows">Workflows</TabsTrigger>
|
||||||
<TabsTrigger value="machines">Machines</TabsTrigger>
|
<TabsTrigger value="machines">Machines</TabsTrigger>
|
||||||
<TabsTrigger value="api-keys">API Keys</TabsTrigger>
|
<TabsTrigger value="api-keys">API Keys</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
540
web/src/components/Section.tsx
Normal file
540
web/src/components/Section.tsx
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
|
type ButtonProps = React.ComponentProps<typeof Button>;
|
||||||
|
type LinkProps = React.ComponentProps<typeof Link>;
|
||||||
|
import { Card as BaseCard } from '@/components/ui/card';
|
||||||
|
type CardProps = React.ComponentProps<typeof BaseCard>;
|
||||||
|
import { Tabs, TabsTrigger as Tab, TabsList } from '@/components/ui/tabs';
|
||||||
|
type TabsProps = React.ComponentProps<typeof Tabs>;
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/ui/accordion';
|
||||||
|
type AccordionProps = React.ComponentProps<typeof Accordion>;
|
||||||
|
import { Badge as Chip } from '@/components/ui/badge';
|
||||||
|
type ChipProps = React.ComponentProps<typeof Chip>;
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import type {
|
||||||
|
HTMLAttributeAnchorTarget,
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { ChevronRight as MdChevronRight} from 'lucide-react'
|
||||||
|
// import { MdChevronRight } from 'react-icons/md';
|
||||||
|
import React from 'react';
|
||||||
|
import { CheckCircle as PiCheckCircleDuotone } from 'lucide-react'
|
||||||
|
// import { PiCheckCircleDuotone } from 'react-icons/pi';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function Section({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLElement>) {
|
||||||
|
// extract the primary action and secondary action from the children
|
||||||
|
const primaryAction = getChildComponent(children, PrimaryAction);
|
||||||
|
const secondaryAction = getChildComponent(children, SecondaryAction);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={twMerge(
|
||||||
|
'flex min-h-[400px] w-full max-w-6xl flex-col justify-center gap-2 rounded-lg px-10 py-10 md:px-20',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{removeFromChildren(children, [PrimaryAction, SecondaryAction])}
|
||||||
|
<div className="mt-2 flex flex-row gap-2">
|
||||||
|
{primaryAction}
|
||||||
|
{secondaryAction}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Title({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'text-center text-4xl font-bold md:text-6xl',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
// @ts-ignore
|
||||||
|
textWrap: 'balance',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Subtitle({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'text text-center overflow-hidden text-ellipsis text-xl',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
// @ts-ignore
|
||||||
|
textWrap: 'balance',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Announcement({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
target = '_blank',
|
||||||
|
...props
|
||||||
|
}: ChipProps & {
|
||||||
|
href?: string; //string | UrlObject;
|
||||||
|
target?: HTMLAttributeAnchorTarget | undefined;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
className={twMerge(
|
||||||
|
'w-fit group bg-foreground-50 text-center transition-colors hover:bg-gray-200',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
variant="outline"
|
||||||
|
// href={href}
|
||||||
|
// target={target}
|
||||||
|
// as={Link}
|
||||||
|
// endContent={
|
||||||
|
// <MdChevronRight
|
||||||
|
// size={20}
|
||||||
|
// className="pr-1 transition-transform group-hover:translate-x-[2px]"
|
||||||
|
// />
|
||||||
|
// }
|
||||||
|
style={{
|
||||||
|
// @ts-ignore
|
||||||
|
textWrap: 'balance',
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<a href={href} target={target}>
|
||||||
|
{children}
|
||||||
|
</a>{' '}
|
||||||
|
<MdChevronRight
|
||||||
|
size={20}
|
||||||
|
className="pr-1 transition-transform group-hover:translate-x-[2px]"
|
||||||
|
/>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionProps = ButtonProps & {
|
||||||
|
be: 'button';
|
||||||
|
hideArrow?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionLinkProps = LinkProps & {
|
||||||
|
be?: 'a';
|
||||||
|
hideArrow?: boolean;
|
||||||
|
variant?: ButtonProps['variant'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function PrimaryAction({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
hideArrow,
|
||||||
|
...props
|
||||||
|
}: ActionLinkProps | ActionProps) {
|
||||||
|
if (props.be === 'button') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: variant,
|
||||||
|
}),
|
||||||
|
'group',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{!hideArrow && (
|
||||||
|
<MdChevronRight className="transition-transform group-hover:translate-x-1" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: variant,
|
||||||
|
}),
|
||||||
|
'group',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{!hideArrow && (
|
||||||
|
<MdChevronRight className="transition-transform group-hover:translate-x-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SecondaryAction({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
hideArrow,
|
||||||
|
...props
|
||||||
|
}: ActionLinkProps | ActionProps) {
|
||||||
|
if (props.be === 'button') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: variant,
|
||||||
|
}),
|
||||||
|
'group',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
variant={'ghost'}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{!hideArrow && (
|
||||||
|
<MdChevronRight className="transition-transform group-hover:translate-x-1" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
}),
|
||||||
|
'group',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{!hideArrow && (
|
||||||
|
<MdChevronRight className="transition-transform group-hover:translate-x-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function PricingCard({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: Omit<CardProps, 'children'> & {
|
||||||
|
children:
|
||||||
|
| ReactNode
|
||||||
|
| ReactNode[]
|
||||||
|
| ((pricingType: PricingType) => ReactNode | ReactNode[]);
|
||||||
|
}) {
|
||||||
|
// const { pricingType } = usePricingContext();
|
||||||
|
if (typeof children === 'function')
|
||||||
|
children = (children('month') as React.ReactElement).props.children as
|
||||||
|
| ReactNode
|
||||||
|
| ReactNode[];
|
||||||
|
|
||||||
|
// extract the title and subtitle from the children
|
||||||
|
// const cardTitleStyles =
|
||||||
|
const title = getChildComponent(children, Title, {
|
||||||
|
className: 'text-2xl md:text-2xl text-start font-bold',
|
||||||
|
});
|
||||||
|
const subTitle = getChildComponent(children, Subtitle, {
|
||||||
|
className: 'text-md text-start text-foreground-500 mt-4',
|
||||||
|
});
|
||||||
|
const priceTags = getChildComponents(children, PriceTag, {
|
||||||
|
className: 'text-4xl font-bold',
|
||||||
|
});
|
||||||
|
const primaryAction = getChildComponent(children, PrimaryAction, {
|
||||||
|
className: 'w-full',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseCard
|
||||||
|
// shadow="sm"
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'flex flex-col min-h-[400px] w-full max-w-full items-start justify-between gap-2 p-8 text-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{priceTags}
|
||||||
|
{subTitle}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex h-full flex-col gap-2">
|
||||||
|
{removeFromChildren(children, [
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
ImageArea,
|
||||||
|
PriceTag,
|
||||||
|
PrimaryAction,
|
||||||
|
]).map((item, i) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2" key={i}>
|
||||||
|
<PiCheckCircleDuotone className="text-green-600" size={20} />
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="w-full">{primaryAction}</div>
|
||||||
|
</BaseCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a Pricing Context that store the monthly and pricing state
|
||||||
|
// const PricingContext = createContext<{
|
||||||
|
// pricingType: PricingType;
|
||||||
|
// setPricingType: (pricingType: PricingType) => void;
|
||||||
|
// }>({
|
||||||
|
// pricingType: 'month',
|
||||||
|
// setPricingType: (pricingType: PricingType) => {},
|
||||||
|
// });
|
||||||
|
|
||||||
|
const PricingTypeValue = ['month', 'year'] as const;
|
||||||
|
export type PricingType = (typeof PricingTypeValue)[number];
|
||||||
|
|
||||||
|
// // an helper function to useContext
|
||||||
|
// export function usePricingContext() {
|
||||||
|
// return React.useContext(PricingContext);
|
||||||
|
// }
|
||||||
|
|
||||||
|
function Pricing({ children, ...props }: HTMLAttributes<HTMLElement>) {
|
||||||
|
// const [pricingType, setPricingType] = useState<PricingType>('month');
|
||||||
|
|
||||||
|
// const context = useMemo(() => {
|
||||||
|
// return {
|
||||||
|
// pricingType,
|
||||||
|
// setPricingType,
|
||||||
|
// };
|
||||||
|
// }, [pricingType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PricingContext.Provider value={context}>
|
||||||
|
<Section {...props}>{children}</Section>
|
||||||
|
// </PricingContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PricingOption({ className, ...props }: TabsProps) {
|
||||||
|
// const { setPricingType } = usePricingContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
className={twMerge('w-fit', className)}
|
||||||
|
defaultValue="month"
|
||||||
|
aria-label="Pricing Options"
|
||||||
|
{...props}
|
||||||
|
onValueChange={(key) => {
|
||||||
|
// setPricingType(key as PricingType);
|
||||||
|
}}
|
||||||
|
// onSelectionChange={(key: any) => {
|
||||||
|
// setPricingType(key as PricingType);
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
{PricingTypeValue.map((pricingType) => {
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
className="capitalize"
|
||||||
|
value={pricingType}
|
||||||
|
key={pricingType}
|
||||||
|
// title={}
|
||||||
|
>
|
||||||
|
{pricingType}
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PriceTag({
|
||||||
|
children,
|
||||||
|
pricingType,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLHeadingElement> & {
|
||||||
|
pricingType?: 'month' | 'year' | string;
|
||||||
|
}) {
|
||||||
|
// const { pricingType: currentPricingType } = usePricingContext();
|
||||||
|
let currentPricingType = 'month';
|
||||||
|
|
||||||
|
if (pricingType != undefined && currentPricingType !== pricingType)
|
||||||
|
return <></>;
|
||||||
|
|
||||||
|
return <h2 {...props}>{children}</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ className, children, ...props }: CardProps) {
|
||||||
|
// extract the title and subtitle from the children
|
||||||
|
// const cardTitleStyles =
|
||||||
|
const title = getChildComponent(children, Title, {
|
||||||
|
className: 'text-2xl md:text-2xl font-normal text-center',
|
||||||
|
});
|
||||||
|
const subTitle = getChildComponent(children, Subtitle, {
|
||||||
|
className: 'text-md text-center',
|
||||||
|
});
|
||||||
|
const image = getChildComponent(children, ImageArea);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseCard
|
||||||
|
// shadow="sm"
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'flex min-h-[280px] w-full max-w-full items-center justify-center gap-2 p-4 text-sm flex-col',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{image}
|
||||||
|
{title}
|
||||||
|
{subTitle}
|
||||||
|
{removeFromChildren(children, [Title, Subtitle, ImageArea])}
|
||||||
|
</BaseCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImageArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge('aspect-square w-14 bg-foreground-300', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a helper to get and remove the title and subtitle from the children
|
||||||
|
function getChildComponent<T extends (...args: any[]) => React.JSX.Element>(
|
||||||
|
children: React.ReactNode | React.ReactNode[],
|
||||||
|
type: T,
|
||||||
|
propsOverride?: Partial<Parameters<T>[0]>,
|
||||||
|
) {
|
||||||
|
const childrenArr = React.Children.toArray(children);
|
||||||
|
let child = childrenArr.find(
|
||||||
|
(child) => React.isValidElement(child) && child.type === type,
|
||||||
|
) as React.ReactElement<
|
||||||
|
Parameters<T>[0],
|
||||||
|
string | React.JSXElementConstructor<any>
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (child && propsOverride) {
|
||||||
|
const { className, ...rest } = child.props;
|
||||||
|
child = React.cloneElement(child, {
|
||||||
|
className: twMerge(className, propsOverride.className),
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChildComponents<T extends (...args: any[]) => React.JSX.Element>(
|
||||||
|
children: React.ReactNode | React.ReactNode[],
|
||||||
|
type: T,
|
||||||
|
propsOverride?: Partial<Parameters<T>[0]>,
|
||||||
|
) {
|
||||||
|
const childrenArr = React.Children.toArray(children);
|
||||||
|
let child = (
|
||||||
|
childrenArr.filter(
|
||||||
|
(child) => React.isValidElement(child) && child.type === type,
|
||||||
|
) as React.ReactElement<
|
||||||
|
Parameters<T>[0],
|
||||||
|
string | React.JSXElementConstructor<any>
|
||||||
|
>[]
|
||||||
|
).map((child) => {
|
||||||
|
if (child && propsOverride) {
|
||||||
|
const { className, ...rest } = child.props;
|
||||||
|
child = React.cloneElement(child, {
|
||||||
|
className: twMerge(className, propsOverride.className),
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromChildren(
|
||||||
|
children: React.ReactNode | React.ReactNode[],
|
||||||
|
types: any[],
|
||||||
|
): React.ReactNode[] {
|
||||||
|
return React.Children.toArray(children).filter(
|
||||||
|
(child) => React.isValidElement(child) && !types.includes(child.type),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FAQ({ children, ...props }: AccordionProps) {
|
||||||
|
return <Accordion {...props}>{children}</Accordion>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FAQItem({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
'aria-label': string;
|
||||||
|
title: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<AccordionItem value={props['aria-label']}>
|
||||||
|
<AccordionTrigger>{props.title}</AccordionTrigger>
|
||||||
|
<AccordionContent>{children}</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const FAQItem = AccordionItem;
|
||||||
|
|
||||||
|
const pkg = Object.assign(Section, {
|
||||||
|
Pricing,
|
||||||
|
PricingOption,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Announcement,
|
||||||
|
PrimaryAction,
|
||||||
|
SecondaryAction,
|
||||||
|
ImageArea,
|
||||||
|
Card,
|
||||||
|
FAQItem,
|
||||||
|
FAQ,
|
||||||
|
PricingCard,
|
||||||
|
PriceTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { pkg as Section };
|
@ -84,7 +84,7 @@ export const columns: ColumnDef<Payment>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<a className="hover:underline" href={`/workflow/${row.original.id}`}>
|
<a className="hover:underline" href={`/workflows/${row.original.id}`}>
|
||||||
{row.getValue("email")}
|
{row.getValue("email")}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
58
web/src/components/ui/accordion.tsx
Normal file
58
web/src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
@ -5,7 +5,7 @@ import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
|
|||||||
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
||||||
export default authMiddleware({
|
export default authMiddleware({
|
||||||
// debug: true,
|
// debug: true,
|
||||||
publicRoutes: ["/api/(.*)"],
|
publicRoutes: ['/',"/api/(.*)"],
|
||||||
// publicRoutes: ["/", "/(.*)"],
|
// publicRoutes: ["/", "/(.*)"],
|
||||||
async afterAuth(auth, req, evt) {
|
async afterAuth(auth, req, evt) {
|
||||||
// redirect them to organization selection page
|
// redirect them to organization selection page
|
||||||
|
@ -66,10 +66,19 @@ const config: Config = {
|
|||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: "0" },
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
|
'background-shine': {
|
||||||
|
from: {
|
||||||
|
backgroundPosition: '0 0',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
backgroundPosition: '-200% 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
'background-shine': 'background-shine 2s linear infinite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"next-gen/config": ["./next-gen/next-gen.config.json"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user