541 lines
13 KiB
TypeScript
541 lines
13 KiB
TypeScript
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 };
|