B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

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

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

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

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

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

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

View File

@@ -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>
);
};

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

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

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

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

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

View File

@@ -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>
);
}

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

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

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