feat: add landing page
This commit is contained in:
parent
21a17cb753
commit
ef050b5e55
@ -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=
|
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",
|
||||
"@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",
|
||||
|
@ -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 />
|
||||
|
@ -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 />;
|
||||
}
|
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,
|
||||
});
|
||||
}
|
@ -21,4 +21,4 @@ export function CopyButton({
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
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"
|
||||
: 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>
|
||||
|
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 }) => {
|
||||
return (
|
||||
<a className="hover:underline" href={`/workflow/${row.original.id}`}>
|
||||
<a className="hover:underline" href={`/workflows/${row.original.id}`}>
|
||||
{row.getValue("email")}
|
||||
</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
|
||||
export default authMiddleware({
|
||||
// debug: true,
|
||||
publicRoutes: ["/api/(.*)"],
|
||||
publicRoutes: ['/',"/api/(.*)"],
|
||||
// publicRoutes: ["/", "/(.*)"],
|
||||
async afterAuth(auth, req, evt) {
|
||||
// redirect them to organization selection page
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user