B2B-88: add starter kit structure and elements
This commit is contained in:
107
packages/ui/src/makerkit/marketing/coming-soon.tsx
Normal file
107
packages/ui/src/makerkit/marketing/coming-soon.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { CtaButton } from './cta-button';
|
||||
import { GradientSecondaryText } from './gradient-secondary-text';
|
||||
import { HeroTitle } from './hero-title';
|
||||
|
||||
const ComingSoonHeading: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <HeroTitle className={cn(className)} {...props} />;
|
||||
|
||||
ComingSoonHeading.displayName = 'ComingSoonHeading';
|
||||
|
||||
const ComingSoonText: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<GradientSecondaryText
|
||||
className={cn('text-muted-foreground text-lg md:text-xl', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
ComingSoonText.displayName = 'ComingSoonText';
|
||||
|
||||
const ComingSoonButton: React.FC<
|
||||
React.ComponentPropsWithoutRef<typeof Button>
|
||||
> = ({ className, ...props }) => (
|
||||
<CtaButton className={cn('mt-8', className)} {...props} />
|
||||
);
|
||||
ComingSoonButton.displayName = 'ComingSoonButton';
|
||||
|
||||
const ComingSoon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
const logo = childrenArray.find(
|
||||
(child) => React.isValidElement(child) && child.type === ComingSoonLogo,
|
||||
);
|
||||
|
||||
const heading = childrenArray.find(
|
||||
(child) => React.isValidElement(child) && child.type === ComingSoonHeading,
|
||||
);
|
||||
|
||||
const text = childrenArray.find(
|
||||
(child) => React.isValidElement(child) && child.type === ComingSoonText,
|
||||
);
|
||||
|
||||
const button = childrenArray.find(
|
||||
(child) => React.isValidElement(child) && child.type === ComingSoonButton,
|
||||
);
|
||||
|
||||
const cmps = [
|
||||
ComingSoonHeading,
|
||||
ComingSoonText,
|
||||
ComingSoonButton,
|
||||
ComingSoonLogo,
|
||||
];
|
||||
|
||||
const otherChildren = childrenArray.filter(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
!cmps.includes(child.type as (typeof cmps)[number]),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'container flex min-h-screen flex-col items-center justify-center space-y-12 p-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{logo}
|
||||
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col items-center justify-center space-y-8 text-center">
|
||||
{heading}
|
||||
|
||||
<div className={'mx-auto max-w-2xl'}>{text}</div>
|
||||
|
||||
{button}
|
||||
|
||||
{otherChildren}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ComingSoon.displayName = 'ComingSoon';
|
||||
|
||||
const ComingSoonLogo: React.FC<React.HTMLAttributes<HTMLImageElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <div className={cn(className, 'fixed top-8 left-8')} {...props} />;
|
||||
ComingSoonLogo.displayName = 'ComingSoonLogo';
|
||||
|
||||
export {
|
||||
ComingSoon,
|
||||
ComingSoonHeading,
|
||||
ComingSoonText,
|
||||
ComingSoonButton,
|
||||
ComingSoonLogo,
|
||||
};
|
||||
22
packages/ui/src/makerkit/marketing/cta-button.tsx
Normal file
22
packages/ui/src/makerkit/marketing/cta-button.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../../shadcn/button';
|
||||
|
||||
export const CtaButton: React.FC<React.ComponentProps<typeof Button>> =
|
||||
function CtaButtonComponent({ className, children, ...props }) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'h-12 rounded-xl px-4 text-base font-semibold',
|
||||
className,
|
||||
{
|
||||
['dark:shadow-primary/30 transition-all hover:shadow-2xl']:
|
||||
props.variant === 'default' || !props.variant,
|
||||
},
|
||||
)}
|
||||
asChild
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
28
packages/ui/src/makerkit/marketing/feature-card.tsx
Normal file
28
packages/ui/src/makerkit/marketing/feature-card.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CardDescription, CardHeader, CardTitle } from '../../shadcn/card';
|
||||
|
||||
interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const FeatureCard: React.FC<FeatureCardProps> = ({
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('rounded-xl border p-4', className)} {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-medium">{label}</CardTitle>
|
||||
|
||||
<CardDescription className="text-muted-foreground max-w-xs text-sm font-normal">
|
||||
{description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
packages/ui/src/makerkit/marketing/feature-grid.tsx
Normal file
18
packages/ui/src/makerkit/marketing/feature-grid.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const FeatureGrid: React.FC<React.HTMLAttributes<HTMLDivElement>> =
|
||||
function FeatureGridComponent({ className, children, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 md:grid-cols-3 lg:grid-cols-3',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
51
packages/ui/src/makerkit/marketing/feature-showcase.tsx
Normal file
51
packages/ui/src/makerkit/marketing/feature-showcase.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface FeatureShowcaseProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
heading: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FeatureShowcase: React.FC<FeatureShowcaseProps> =
|
||||
function FeatureShowcaseComponent({
|
||||
className,
|
||||
heading,
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col justify-between space-y-8', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full max-w-5xl flex-col gap-y-4">
|
||||
{icon && <div className="flex">{icon}</div>}
|
||||
<h3 className="text-3xl font-normal tracking-tight xl:text-5xl">
|
||||
{heading}
|
||||
</h3>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function FeatureShowcaseIconContainer(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex'}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center space-x-4 rounded-lg p-3 font-medium',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
packages/ui/src/makerkit/marketing/footer.tsx
Normal file
98
packages/ui/src/makerkit/marketing/footer.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface FooterSection {
|
||||
heading: React.ReactNode;
|
||||
links: Array<{
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface FooterProps extends React.HTMLAttributes<HTMLElement> {
|
||||
logo: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
copyright: React.ReactNode;
|
||||
sections: FooterSection[];
|
||||
}
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({
|
||||
className,
|
||||
logo,
|
||||
description,
|
||||
copyright,
|
||||
sections,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
'site-footer relative mt-auto w-full py-8 2xl:py-20',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-y-0">
|
||||
<div className="flex w-full gap-x-3 lg:w-4/12 xl:w-4/12 xl:space-x-6 2xl:space-x-8">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>{logo}</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm tracking-tight">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex text-xs">
|
||||
<p>{copyright}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-y-4 lg:flex-row lg:justify-end lg:gap-x-6 lg:gap-y-0 xl:gap-x-12">
|
||||
{sections.map((section, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex flex-col gap-y-2.5">
|
||||
<FooterSectionHeading>{section.heading}</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
{section.links.map((link, linkIndex) => (
|
||||
<FooterLink key={linkIndex} href={link.href}>
|
||||
{link.label}
|
||||
</FooterLink>
|
||||
))}
|
||||
</FooterSectionList>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
function FooterSectionHeading(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<span className="font-heading text-sm font-semibold tracking-tight">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterSectionList(props: React.PropsWithChildren) {
|
||||
return <ul className="flex flex-col gap-y-2">{props.children}</ul>;
|
||||
}
|
||||
|
||||
function FooterLink({
|
||||
href,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ href: string }>) {
|
||||
return (
|
||||
<li className="text-muted-foreground text-sm tracking-tight hover:underline [&>a]:transition-colors">
|
||||
<a href={href}>{children}</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const GradientSecondaryText: React.FC<
|
||||
React.HTMLAttributes<HTMLSpanElement> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
> = function GradientSecondaryTextComponent({ className, ...props }) {
|
||||
const Comp = props.asChild ? Slot : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
'dark:from-foreground/60 dark:to-foreground text-secondary-foreground dark:bg-linear-to-r dark:bg-clip-text dark:text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Slottable>{props.children}</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
};
|
||||
18
packages/ui/src/makerkit/marketing/gradient-text.tsx
Normal file
18
packages/ui/src/makerkit/marketing/gradient-text.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const GradientText: React.FC<React.HTMLAttributes<HTMLSpanElement>> =
|
||||
function GradientTextComponent({ className, children, ...props }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'bg-linear-to-r bg-clip-text text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
33
packages/ui/src/makerkit/marketing/header.tsx
Normal file
33
packages/ui/src/makerkit/marketing/header.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
logo?: React.ReactNode;
|
||||
navigation?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = function ({
|
||||
className,
|
||||
logo,
|
||||
navigation,
|
||||
actions,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'site-header bg-background/80 dark:bg-background/50 sticky top-0 z-10 w-full py-1 backdrop-blur-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="grid h-14 grid-cols-3 items-center">
|
||||
<div className={'mx-auto md:mx-0'}>{logo}</div>
|
||||
<div className="order-first md:order-none">{navigation}</div>
|
||||
<div className="flex items-center justify-end gap-x-2">{actions}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
packages/ui/src/makerkit/marketing/hero-title.tsx
Normal file
23
packages/ui/src/makerkit/marketing/hero-title.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const HeroTitle: React.FC<
|
||||
React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
> = function HeroTitleComponent({ children, className, ...props }) {
|
||||
const Comp = props.asChild ? Slot : 'h1';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
'hero-title flex flex-col text-center font-sans text-4xl font-semibold tracking-tighter sm:text-6xl lg:max-w-5xl lg:text-7xl xl:text-[4.5rem] dark:text-white',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Slottable>{children}</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
};
|
||||
90
packages/ui/src/makerkit/marketing/hero.tsx
Normal file
90
packages/ui/src/makerkit/marketing/hero.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { HeroTitle } from './hero-title';
|
||||
|
||||
interface HeroProps {
|
||||
pill?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
cta?: React.ReactNode;
|
||||
image?: React.ReactNode;
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export function Hero({
|
||||
pill,
|
||||
title,
|
||||
subtitle,
|
||||
cta,
|
||||
image,
|
||||
className,
|
||||
animate = true,
|
||||
}: HeroProps) {
|
||||
return (
|
||||
<div className={cn('mx-auto flex flex-col space-y-20', className)}>
|
||||
<div
|
||||
style={{
|
||||
MozAnimationDuration: '100ms',
|
||||
}}
|
||||
className={cn(
|
||||
'mx-auto flex flex-1 flex-col items-center justify-center duration-800 md:flex-row',
|
||||
{
|
||||
['animate-in fade-in zoom-in-90 slide-in-from-top-24']: animate,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-1 flex-col items-center gap-y-6 xl:gap-y-8 2xl:gap-y-12">
|
||||
{pill && (
|
||||
<div
|
||||
className={cn({
|
||||
['animate-in fade-in fill-mode-both delay-300 duration-700']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{pill}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center gap-y-6">
|
||||
<HeroTitle>{title}</HeroTitle>
|
||||
|
||||
{subtitle && (
|
||||
<div className="flex max-w-lg">
|
||||
<h3 className="text-muted-foreground p-0 text-center font-sans text-2xl font-normal tracking-tight">
|
||||
{subtitle}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cta && (
|
||||
<div
|
||||
className={cn({
|
||||
['animate-in fade-in fill-mode-both delay-500 duration-1000']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{cta}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{image && (
|
||||
<div
|
||||
style={{
|
||||
MozAnimationDuration: '100ms',
|
||||
}}
|
||||
className={cn('container mx-auto flex justify-center py-8', {
|
||||
['animate-in fade-in zoom-in-90 slide-in-from-top-32 fill-mode-both delay-600 duration-1000']:
|
||||
animate,
|
||||
})}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
packages/ui/src/makerkit/marketing/index.tsx
Normal file
15
packages/ui/src/makerkit/marketing/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from './hero-title';
|
||||
export * from './pill';
|
||||
export * from './gradient-secondary-text';
|
||||
export * from './gradient-text';
|
||||
export * from './hero';
|
||||
export * from './secondary-hero';
|
||||
export * from './cta-button';
|
||||
export * from './header';
|
||||
export * from './footer';
|
||||
export * from './feature-showcase';
|
||||
export * from './feature-grid';
|
||||
export * from './feature-card';
|
||||
export * from './newsletter-signup';
|
||||
export * from './newsletter-signup-container';
|
||||
export * from './coming-soon';
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../../shadcn/alert';
|
||||
import { Heading } from '../../shadcn/heading';
|
||||
import { Spinner } from '../spinner';
|
||||
import { NewsletterSignup } from './newsletter-signup';
|
||||
|
||||
interface NewsletterSignupContainerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onSignup: (email: string) => Promise<void>;
|
||||
heading?: string;
|
||||
description?: string;
|
||||
successMessage?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export function NewsletterSignupContainer({
|
||||
onSignup,
|
||||
heading = 'Subscribe to our newsletter',
|
||||
description = 'Get the latest updates and offers directly to your inbox.',
|
||||
successMessage = 'Thank you for subscribing!',
|
||||
errorMessage = 'An error occurred. Please try again.',
|
||||
className,
|
||||
...props
|
||||
}: NewsletterSignupContainerProps) {
|
||||
const [status, setStatus] = useState<
|
||||
'idle' | 'loading' | 'success' | 'error'
|
||||
>('idle');
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (data: { email: string }) => {
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
await onSignup(data.email);
|
||||
|
||||
setStatus('success');
|
||||
} catch (error) {
|
||||
console.error('Newsletter signup error:', error);
|
||||
setStatus('error');
|
||||
}
|
||||
},
|
||||
[onSignup],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col items-center space-y-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Heading level={4}>{heading}</Heading>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
{status === 'idle' && <NewsletterSignup onSignup={handleSubmit} />}
|
||||
|
||||
{status === 'loading' && (
|
||||
<div className="flex justify-center">
|
||||
<Spinner className="h-8 w-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<div>
|
||||
<Alert variant="success">
|
||||
<AlertTitle>Success!</AlertTitle>
|
||||
<AlertDescription>{successMessage}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<div>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
packages/ui/src/makerkit/marketing/newsletter-signup.tsx
Normal file
71
packages/ui/src/makerkit/marketing/newsletter-signup.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../../shadcn/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '../../shadcn/form';
|
||||
import { Input } from '../../shadcn/input';
|
||||
|
||||
const NewsletterFormSchema = z.object({
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
});
|
||||
|
||||
type NewsletterFormValues = z.infer<typeof NewsletterFormSchema>;
|
||||
|
||||
interface NewsletterSignupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onSignup: (data: NewsletterFormValues) => void;
|
||||
buttonText?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function NewsletterSignup({
|
||||
onSignup,
|
||||
buttonText = 'Subscribe',
|
||||
placeholder = 'Enter your email',
|
||||
className,
|
||||
...props
|
||||
}: NewsletterSignupProps) {
|
||||
const form = useForm<NewsletterFormValues>({
|
||||
resolver: zodResolver(NewsletterFormSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('w-full max-w-sm', className)} {...props}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSignup)}
|
||||
className="flex flex-col gap-y-3"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder={placeholder} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
packages/ui/src/makerkit/marketing/pill.tsx
Normal file
59
packages/ui/src/makerkit/marketing/pill.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Slot, Slottable } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { GradientSecondaryText } from './gradient-secondary-text';
|
||||
|
||||
export const Pill: React.FC<
|
||||
React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
label?: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
}
|
||||
> = function PillComponent({ className, asChild, ...props }) {
|
||||
const Comp = asChild ? Slot : 'h3';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
'bg-muted/50 flex items-center gap-x-1.5 rounded-full border px-2 py-1 pr-2 text-center text-sm font-medium text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.label && (
|
||||
<span
|
||||
className={
|
||||
'text-primary-foreground bg-primary rounded-2xl border px-1.5 py-0.5 text-xs font-bold tracking-tight'
|
||||
}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
)}
|
||||
<Slottable>
|
||||
<GradientSecondaryText
|
||||
className={'flex items-center gap-x-2 font-semibold tracking-tight'}
|
||||
>
|
||||
{props.children}
|
||||
</GradientSecondaryText>
|
||||
</Slottable>
|
||||
</Comp>
|
||||
);
|
||||
};
|
||||
|
||||
export const PillActionButton: React.FC<
|
||||
React.HTMLAttributes<HTMLButtonElement> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
> = ({ asChild, ...props }) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
{...props}
|
||||
className={
|
||||
'text-secondary-foreground bg-input active:bg-primary active:text-primary-foreground hover:ring-muted-foreground/50 rounded-full px-1.5 py-1.5 text-center text-sm font-medium ring ring-transparent transition-colors'
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Comp>
|
||||
);
|
||||
};
|
||||
42
packages/ui/src/makerkit/marketing/secondary-hero.tsx
Normal file
42
packages/ui/src/makerkit/marketing/secondary-hero.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Heading } from '../../shadcn/heading';
|
||||
|
||||
interface SecondaryHeroProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
pill?: React.ReactNode;
|
||||
heading: React.ReactNode;
|
||||
subheading: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SecondaryHero: React.FC<SecondaryHeroProps> =
|
||||
function SecondaryHeroComponent({
|
||||
className,
|
||||
pill,
|
||||
heading,
|
||||
subheading,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center space-y-6 text-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{pill}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Heading level={2} className="tracking-tighter">
|
||||
{heading}
|
||||
</Heading>
|
||||
|
||||
<h3 className="text-muted-foreground font-sans text-xl font-normal tracking-tight">
|
||||
{subheading}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user