feat: add landing page

This commit is contained in:
BennyKok 2023-12-16 17:14:04 +08:00
parent 21a17cb753
commit ef050b5e55
17 changed files with 851 additions and 90 deletions

View File

@ -17,4 +17,5 @@ SPACES_BUCKET="comfyui-deploy"
SPACES_KEY="xyz"
SPACES_SECRET="aaa"
JWT_SECRET="openssl rand -hex 32"
JWT_SECRET="openssl rand -hex 32"
PLAUSIBLE_DOMAIN=

Binary file not shown.

View 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": ""
}
]
}
]
}

View File

@ -20,6 +20,7 @@
"@clerk/nextjs": "^4.27.4",
"@hookform/resolvers": "^3.3.2",
"@neondatabase/serverless": "^0.6.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
@ -38,6 +39,7 @@
"lucide-react": "^0.294.0",
"nanoid": "^5.0.4",
"next": "14.0.3",
"next-plausible": "^3.12.0",
"next-usequerystate": "^1.13.2",
"react": "^18",
"react-dom": "^18",

View File

@ -4,14 +4,27 @@ import { Button } from "@/components/ui/button";
import { ClerkProvider, UserButton } from "@clerk/nextjs";
import { Github } from "lucide-react";
import type { Metadata } from "next";
import PlausibleProvider from "next-plausible";
import { Inter } from "next/font/google";
import { Toaster } from "sonner";
const inter = Inter({ subsets: ["latin"] });
import meta from 'next-gen/config';
export const metadata: Metadata = {
title: "Comfy Deploy",
description: "Generated by create next app",
title: meta['og:title'],
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({
@ -22,12 +35,17 @@ export default function RootLayout({
return (
<html lang="en">
<ClerkProvider>
<head>
{process.env.PLAUSIBLE_DOMAIN && (
<PlausibleProvider domain={process.env.PLAUSIBLE_DOMAIN} />
)}
</head>
<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="flex flex-row items-center gap-4">
<a className="font-bold text-lg hover:underline" href="/">
ComfyUI Deploy
<a className="font-bold text-md md:text-lg hover:underline" href="/">
{meta.name}
</a>
<NavbarRight />
</div>
@ -38,14 +56,17 @@ export default function RootLayout({
variant={"outline"}
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 />
</a>
</Button>
</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}
</div>
<Toaster richColors />

View File

@ -1,77 +1,5 @@
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";
import Main from '@/components/Main';
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,
});
}
return <Main />;
}

View 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,
});
}

View File

@ -21,4 +21,4 @@ export function CopyButton({
<Copy size={14} />
</Button>
);
}
}

100
web/src/components/Main.tsx Normal file
View 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>
);
}

View File

@ -15,7 +15,8 @@ export function NavbarRight() {
? "machines"
: pathname.startsWith("/api-keys")
? "api-keys"
: "workflow"
: pathname.startsWith("/workflows")
? "workflows" : ""
}
className="w-[300px]"
onValueChange={(value) => {
@ -24,12 +25,12 @@ export function NavbarRight() {
} else if (value === "api-keys") {
router.push("/api-keys");
} else {
router.push("/");
router.push("/workflows");
}
}}
>
<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="api-keys">API Keys</TabsTrigger>
</TabsList>

View 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 };

View File

@ -84,7 +84,7 @@ export const columns: ColumnDef<Payment>[] = [
},
cell: ({ row }) => {
return (
<a className="hover:underline" href={`/workflow/${row.original.id}`}>
<a className="hover:underline" href={`/workflows/${row.original.id}`}>
{row.getValue("email")}
</a>
);

View 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 }

View File

@ -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
export default authMiddleware({
// debug: true,
publicRoutes: ["/api/(.*)"],
publicRoutes: ['/',"/api/(.*)"],
// publicRoutes: ["/", "/(.*)"],
async afterAuth(auth, req, evt) {
// redirect them to organization selection page

View File

@ -66,10 +66,19 @@ const config: Config = {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
'background-shine': {
from: {
backgroundPosition: '0 0',
},
to: {
backgroundPosition: '-200% 0',
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
'background-shine': 'background-shine 2s linear infinite',
},
},
},

View File

@ -19,7 +19,8 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"next-gen/config": ["./next-gen/next-gen.config.json"],
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],