Design Updates (#379)
* Enhance Marketing Pages and UI Components - Updated the marketing homepage to include an Ecosystem Showcase component, improving the presentation of the SaaS Starter Kit. - Refined various UI components, including adjustments to spacing, typography, and layout for better visual consistency. - Improved accessibility by adding aria-labels and ensuring proper semantic structure in components. - Adjusted styles across multiple components to enhance responsiveness and user experience. - Updated the pricing table and feature cards to align with the new design standards, ensuring a cohesive look and feel throughout the application. - Updated plan picker design
This commit is contained in:
committed by
GitHub
parent
d8bb7f56df
commit
54d6b4897f
@@ -29,8 +29,8 @@
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/node": "^24.6.1",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"@types/react": "19.1.16",
|
||||
"@types/react-dom": "19.1.9",
|
||||
@@ -38,7 +38,7 @@
|
||||
"pino-pretty": "13.0.0",
|
||||
"react-hook-form": "^7.63.0",
|
||||
"recharts": "2.15.3",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss": "4.1.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"zod": "^3.25.74"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@supabase/supabase-js": "2.58.0",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"dotenv": "17.2.3",
|
||||
"node-html-parser": "^7.0.1",
|
||||
"totp-generator": "^2.0.0"
|
||||
|
||||
@@ -59,7 +59,9 @@ export function SiteHeaderAccountSection({
|
||||
|
||||
function AuthButtons() {
|
||||
return (
|
||||
<div className={'animate-in fade-in flex gap-x-2.5 duration-500'}>
|
||||
<div
|
||||
className={'animate-in fade-in flex items-center gap-x-2 duration-500'}
|
||||
>
|
||||
<div className={'hidden md:flex'}>
|
||||
<If condition={features.enableThemeToggle}>
|
||||
<ModeToggle />
|
||||
@@ -72,14 +74,24 @@ function AuthButtons() {
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div className={'flex gap-x-2.5'}>
|
||||
<Button className={'hidden md:block'} asChild variant={'ghost'}>
|
||||
<div className={'flex items-center gap-x-2'}>
|
||||
<Button
|
||||
className={'hidden md:flex md:text-sm'}
|
||||
asChild
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
||||
<Button
|
||||
asChild
|
||||
className="text-xs md:text-sm"
|
||||
variant={'default'}
|
||||
size={'sm'}
|
||||
>
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth:signUp'} />
|
||||
</Link>
|
||||
|
||||
@@ -14,11 +14,21 @@ export function SitePageHeader({
|
||||
const containerClass = container ? 'container' : '';
|
||||
|
||||
return (
|
||||
<div className={cn('border-b py-8 xl:py-10 2xl:py-12', className)}>
|
||||
<div className={cn('flex flex-col gap-y-3 lg:gap-y-4', containerClass)}>
|
||||
<div
|
||||
className={cn(
|
||||
'border-border/40 border-b py-6 xl:py-8 2xl:py-10',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-y-2 lg:gap-y-3',
|
||||
containerClass,
|
||||
)}
|
||||
>
|
||||
<h1
|
||||
className={
|
||||
'font-heading text-3xl font-medium tracking-tighter xl:text-5xl dark:text-white'
|
||||
'font-heading text-3xl tracking-tighter xl:text-5xl dark:text-white'
|
||||
}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -12,13 +12,9 @@ type Props = {
|
||||
export function CoverImage({ title, src, preloadImage, className }: Props) {
|
||||
return (
|
||||
<Image
|
||||
className={cn(
|
||||
'block rounded-xl object-cover duration-250' +
|
||||
' transition-all hover:opacity-90',
|
||||
{
|
||||
className={cn('block rounded-md object-cover', {
|
||||
className,
|
||||
},
|
||||
)}
|
||||
})}
|
||||
src={src}
|
||||
priority={preloadImage}
|
||||
alt={`Cover Image for ${title}`}
|
||||
|
||||
@@ -10,24 +10,24 @@ export function PostHeader({ post }: { post: Cms.ContentItem }) {
|
||||
|
||||
return (
|
||||
<div className={'flex flex-1 flex-col'}>
|
||||
<div className={cn('border-b py-8')}>
|
||||
<div className={'mx-auto flex max-w-3xl flex-col space-y-4'}>
|
||||
<div className={cn('border-border/50 border-b py-8')}>
|
||||
<div className={'mx-auto flex max-w-3xl flex-col gap-y-2.5'}>
|
||||
<div>
|
||||
<span className={'text-muted-foreground text-xs'}>
|
||||
<DateFormatter dateString={publishedAt} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
className={
|
||||
'font-heading text-3xl font-semibold tracking-tighter xl:text-5xl dark:text-white'
|
||||
'font-heading text-2xl font-medium tracking-tighter xl:text-4xl dark:text-white'
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<span className={'text-muted-foreground'}>
|
||||
<DateFormatter dateString={publishedAt} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className={'text-muted-foreground text-base xl:text-lg'}
|
||||
className={'text-muted-foreground text-base'}
|
||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||
></h2>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
imageHeight?: string | number;
|
||||
};
|
||||
|
||||
const DEFAULT_IMAGE_HEIGHT = 250;
|
||||
const DEFAULT_IMAGE_HEIGHT = 220;
|
||||
|
||||
export function PostPreview({
|
||||
post,
|
||||
@@ -25,41 +25,44 @@ export function PostPreview({
|
||||
const slug = `/blog/${post.slug}`;
|
||||
|
||||
return (
|
||||
<div className="transition-shadow-sm flex flex-col gap-y-4 rounded-lg duration-500">
|
||||
<Link
|
||||
href={slug}
|
||||
className="hover:bg-muted/50 active:bg-muted flex flex-col gap-y-2.5 rounded-md p-4 transition-all"
|
||||
>
|
||||
<If condition={image}>
|
||||
{(imageUrl) => (
|
||||
<div className="relative mb-2 w-full" style={{ height }}>
|
||||
<Link href={slug}>
|
||||
<CoverImage
|
||||
preloadImage={preloadImage}
|
||||
title={title}
|
||||
src={imageUrl}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<div className={'flex flex-col space-y-4 px-1'}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<h2 className="text-xl leading-snug font-semibold tracking-tight">
|
||||
<Link href={slug} className="hover:underline">
|
||||
{title}
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-3 text-sm">
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div className="flex flex-row items-center gap-x-3 text-xs">
|
||||
<div className="text-muted-foreground">
|
||||
<DateFormatter dateString={publishedAt} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg leading-snug font-medium tracking-tight">
|
||||
<span className="hover:underline">{title}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="text-muted-foreground mb-4 text-sm leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||
dangerouslySetInnerHTML={{ __html: trimText(description ?? '', 200) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function trimText(text: string, maxLength: number) {
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ async function BlogPage(props: BlogPageProps) {
|
||||
subtitle={t('marketing:blogSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'container flex flex-col space-y-6 py-12'}>
|
||||
<div className={'container flex flex-col space-y-6 py-8'}>
|
||||
<If
|
||||
condition={posts.length > 0}
|
||||
fallback={<Trans i18nKey="marketing:noPosts" />}
|
||||
@@ -115,7 +115,7 @@ export default withI18n(BlogPage);
|
||||
|
||||
function PostsGridList({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-y-8 md:grid-cols-2 md:gap-x-8 md:gap-y-12 lg:grid-cols-3 lg:gap-x-12">
|
||||
<div className="grid grid-cols-1 gap-y-8 md:grid-cols-2 md:gap-x-2 md:gap-y-12 lg:grid-cols-3">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ async function ContactPage() {
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
className={'flex flex-1 flex-col items-center justify-center py-12'}
|
||||
className={'flex flex-1 flex-col items-center justify-center py-8'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -59,15 +59,17 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden py-5'}>
|
||||
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden py-4'}>
|
||||
<div className={'flex overflow-y-hidden'}>
|
||||
<article className={cn('gap-y-12 overflow-y-auto px-6')}>
|
||||
<section className={'flex flex-col gap-y-2.5'}>
|
||||
<h1 className={'text-foreground text-3xl font-semibold'}>
|
||||
{page.title}
|
||||
</h1>
|
||||
<article className={cn('gap-y-12 overflow-y-auto px-4')}>
|
||||
<section
|
||||
className={'flex flex-col gap-y-1 border-b border-dashed pb-4'}
|
||||
>
|
||||
<h1 className={'text-foreground text-3xl'}>{page.title}</h1>
|
||||
|
||||
<h2 className={'text-muted-foreground text-lg'}>{description}</h2>
|
||||
<h2 className={'text-secondary-foreground/80 text-lg'}>
|
||||
{description}
|
||||
</h2>
|
||||
</section>
|
||||
|
||||
<div className={'markdoc'}>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function DocsCard({
|
||||
title,
|
||||
subtitle,
|
||||
@@ -15,11 +11,11 @@ export function DocsCard({
|
||||
link: { url: string; label?: string };
|
||||
}>) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Link href={link.url} className="flex flex-col">
|
||||
<div
|
||||
className={`bg-background flex grow flex-col gap-y-2 border p-6 ${link ? 'rounded-t-lg border-b-0' : 'rounded-lg'}`}
|
||||
className={`bg-muted/50 hover:bg-muted/70 flex grow flex-col gap-y-2 rounded p-4`}
|
||||
>
|
||||
<h3 className="mt-0 text-lg font-semibold hover:underline dark:text-white">
|
||||
<h3 className="mt-0 text-lg font-medium hover:underline dark:text-white">
|
||||
<Link href={link.url}>{title}</Link>
|
||||
</h3>
|
||||
|
||||
@@ -31,23 +27,6 @@ export function DocsCard({
|
||||
|
||||
{children && <div className="text-sm">{children}</div>}
|
||||
</div>
|
||||
|
||||
{link && (
|
||||
<div className="bg-muted/50 rounded-b-lg border p-6 py-4">
|
||||
<Link
|
||||
className={
|
||||
'flex items-center space-x-2 text-sm font-medium hover:underline'
|
||||
}
|
||||
href={link.url}
|
||||
>
|
||||
<span>
|
||||
{link.label ?? <Trans i18nKey={'marketing:readMore'} />}
|
||||
</span>
|
||||
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
|
||||
const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order);
|
||||
|
||||
return (
|
||||
<div className={'grid grid-cols-1 gap-6 lg:grid-cols-2'}>
|
||||
<div className={'grid grid-cols-1 gap-4 lg:grid-cols-2'}>
|
||||
{cardsSortedByOrder.map((item) => {
|
||||
return (
|
||||
<DocsCard
|
||||
|
||||
@@ -116,7 +116,9 @@ export function DocsNavigation({
|
||||
<>
|
||||
<Sidebar
|
||||
variant={'ghost'}
|
||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto'}
|
||||
className={
|
||||
'sticky z-1 mt-4 max-h-full overflow-y-auto border-r-transparent'
|
||||
}
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function DocsTableOfContents(props: { data: NavItem[] }) {
|
||||
const navData = props.data;
|
||||
|
||||
return (
|
||||
<div className="bg-background sticky inset-y-0 hidden h-svh max-h-full min-w-[14em] border-l p-4 lg:block">
|
||||
<div className="bg-background sticky inset-y-0 hidden h-svh max-h-full min-w-[14em] p-2.5 lg:block">
|
||||
<ol
|
||||
role="list"
|
||||
className="relative text-sm text-gray-600 dark:text-gray-400"
|
||||
|
||||
@@ -21,7 +21,7 @@ async function DocsPage() {
|
||||
const cards = items.filter((item) => !item.parentId);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-y-6 xl:gap-y-10'}>
|
||||
<div className={'flex flex-col gap-y-6 xl:gap-y-8'}>
|
||||
<SitePageHeader
|
||||
title={t('marketing:documentation')}
|
||||
subtitle={t('marketing:documentationSubtitle')}
|
||||
|
||||
@@ -79,8 +79,8 @@ async function FAQPage() {
|
||||
subtitle={t('marketing:faqSubtitle')}
|
||||
/>
|
||||
|
||||
<div className={'container flex flex-col space-y-8 pb-16'}>
|
||||
<div className="flex w-full max-w-xl flex-col">
|
||||
<div className={'container flex flex-col items-center space-y-8 pb-16'}>
|
||||
<div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border">
|
||||
{faqItems.map((item, index) => {
|
||||
return <FaqItem key={index} item={item} />;
|
||||
})}
|
||||
@@ -114,17 +114,15 @@ function FaqItem({
|
||||
};
|
||||
}>) {
|
||||
return (
|
||||
<details className={'group border-b px-2 py-4 last:border-b-transparent'}>
|
||||
<details
|
||||
className={
|
||||
'hover:bg-muted/70 [&:open]:bg-muted/70 [&:open]:hover:bg-muted transition-all'
|
||||
}
|
||||
>
|
||||
<summary
|
||||
className={
|
||||
'flex items-center justify-between hover:cursor-pointer hover:underline'
|
||||
}
|
||||
>
|
||||
<h2
|
||||
className={
|
||||
'hover:underline-none cursor-pointer font-sans font-medium'
|
||||
}
|
||||
className={'flex items-center justify-between p-4 hover:cursor-pointer'}
|
||||
>
|
||||
<h2 className={'cursor-pointer font-sans text-base'}>
|
||||
<Trans i18nKey={item.question} defaults={item.question} />
|
||||
</h2>
|
||||
|
||||
@@ -135,7 +133,7 @@ function FaqItem({
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div className={'text-muted-foreground flex flex-col gap-y-3 py-1'}>
|
||||
<div className={'text-muted-foreground flex flex-col gap-y-2 px-4 pb-2'}>
|
||||
<Trans i18nKey={item.answer} defaults={item.answer} />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ArrowRightIcon, LayoutDashboard } from 'lucide-react';
|
||||
import { PricingTable } from '@kit/billing-gateway/marketing';
|
||||
import {
|
||||
CtaButton,
|
||||
EcosystemShowcase,
|
||||
FeatureCard,
|
||||
FeatureGrid,
|
||||
FeatureShowcase,
|
||||
@@ -24,7 +25,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
function Home() {
|
||||
return (
|
||||
<div className={'mt-4 flex flex-col space-y-24 py-14'}>
|
||||
<div className={'container mx-auto'}>
|
||||
<div className={'mx-auto'}>
|
||||
<Hero
|
||||
pill={
|
||||
<Pill label={'New'}>
|
||||
@@ -37,15 +38,15 @@ function Home() {
|
||||
</Pill>
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<span>The ultimate SaaS Starter</span>
|
||||
<span>for your next project</span>
|
||||
</>
|
||||
<span className="text-secondary-foreground">
|
||||
<span>Ship a SaaS faster than ever.</span>
|
||||
</span>
|
||||
}
|
||||
subtitle={
|
||||
<span>
|
||||
Build and Ship a SaaS faster than ever before with the next-gen
|
||||
SaaS Starter Kit. Ship your SaaS in days, not months.
|
||||
Makerkit gives you a production-ready boilerplate to build your
|
||||
SaaS faster than ever before with the next-gen SaaS Starter Kit.
|
||||
Get started in minutes.
|
||||
</span>
|
||||
}
|
||||
cta={<MainCallToActionButton />}
|
||||
@@ -53,7 +54,7 @@ function Home() {
|
||||
<Image
|
||||
priority
|
||||
className={
|
||||
'dark:border-primary/10 rounded-xl border border-gray-200'
|
||||
'dark:border-primary/10 w-full rounded-lg border border-gray-200'
|
||||
}
|
||||
width={3558}
|
||||
height={2222}
|
||||
@@ -65,17 +66,15 @@ function Home() {
|
||||
</div>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
className={'flex flex-col space-y-16 xl:space-y-32 2xl:space-y-36'}
|
||||
>
|
||||
<div className={'py-4 xl:py-8'}>
|
||||
<FeatureShowcase
|
||||
heading={
|
||||
<>
|
||||
<b className="font-medium tracking-tighter dark:text-white">
|
||||
<b className="font-medium tracking-tight dark:text-white">
|
||||
The ultimate SaaS Starter Kit
|
||||
</b>
|
||||
.{' '}
|
||||
<span className="text-muted-foreground font-normal tracking-tighter">
|
||||
<span className="text-secondary-foreground/70 block font-normal tracking-tight">
|
||||
Unleash your creativity and build your SaaS faster than ever
|
||||
with Makerkit.
|
||||
</span>
|
||||
@@ -83,7 +82,7 @@ function Home() {
|
||||
}
|
||||
icon={
|
||||
<FeatureShowcaseIconContainer>
|
||||
<LayoutDashboard className="h-5" />
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
<span>All-in-one solution</span>
|
||||
</FeatureShowcaseIconContainer>
|
||||
}
|
||||
@@ -108,7 +107,7 @@ function Home() {
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
className={'relative col-span-1 overflow-hidden md:col-span-2'}
|
||||
className={'relative col-span-1 overflow-hidden'}
|
||||
label={'Billing'}
|
||||
description={`Makerkit supports multiple payment gateways to charge your customers.`}
|
||||
/>
|
||||
@@ -118,15 +117,36 @@ function Home() {
|
||||
label={'Plugins'}
|
||||
description={`Extend your SaaS with plugins that you can install using the CLI.`}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
className={'relative col-span-1 overflow-hidden'}
|
||||
label={'Documentation'}
|
||||
description={`Makerkit provides a comprehensive documentation to help you get started.`}
|
||||
/>
|
||||
</FeatureGrid>
|
||||
</FeatureShowcase>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<EcosystemShowcase
|
||||
heading="The ultimate SaaS Starter Kit for founders."
|
||||
description="Unleash your creativity and build your SaaS faster than ever with Makerkit. Get started in minutes and ship your SaaS in no time."
|
||||
>
|
||||
<Image
|
||||
className="rounded-md"
|
||||
src={'/images/sign-in.webp'}
|
||||
alt="Sign in"
|
||||
width={1000}
|
||||
height={1000}
|
||||
/>
|
||||
</EcosystemShowcase>
|
||||
</div>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-center justify-center space-y-16 py-16'
|
||||
'flex flex-col items-center justify-center space-y-12 py-4 xl:py-8'
|
||||
}
|
||||
>
|
||||
<SecondaryHero
|
||||
@@ -154,8 +174,8 @@ export default withI18n(Home);
|
||||
|
||||
function MainCallToActionButton() {
|
||||
return (
|
||||
<div className={'flex space-x-4'}>
|
||||
<CtaButton>
|
||||
<div className={'flex space-x-2.5'}>
|
||||
<CtaButton className="h-10 text-sm">
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<span className={'flex items-center space-x-0.5'}>
|
||||
<span>
|
||||
@@ -172,9 +192,9 @@ function MainCallToActionButton() {
|
||||
</Link>
|
||||
</CtaButton>
|
||||
|
||||
<CtaButton variant={'link'}>
|
||||
<Link href={'/contact'}>
|
||||
<Trans i18nKey={'common:contactUs'} />
|
||||
<CtaButton variant={'link'} className="h-10 text-sm">
|
||||
<Link href={'/pricing'}>
|
||||
<Trans i18nKey={'common:pricing'} />
|
||||
</Link>
|
||||
</CtaButton>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ async function PricingPage() {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-12'}>
|
||||
<div className={'flex flex-col space-y-8'}>
|
||||
<SitePageHeader
|
||||
title={t('marketing:pricing')}
|
||||
subtitle={t('marketing:pricingSubtitle')}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PlanSchema, ProductSchema } from '@kit/billing';
|
||||
import { resolveProductPlan } from '@kit/billing-gateway';
|
||||
import {
|
||||
BillingPortalCard,
|
||||
@@ -38,15 +35,21 @@ async function PersonalAccountBillingPage() {
|
||||
const [subscription, order, customerId] =
|
||||
await loadPersonalAccountBillingPageData(user.id);
|
||||
|
||||
const subscriptionProductPlan = subscription
|
||||
? await getProductPlan(
|
||||
subscription.items[0]?.variant_id,
|
||||
const subscriptionVariantId = subscription?.items[0]?.variant_id;
|
||||
const orderVariantId = order?.items[0]?.variant_id;
|
||||
|
||||
const subscriptionProductPlan =
|
||||
subscription && subscriptionVariantId
|
||||
? await resolveProductPlan(
|
||||
billingConfig,
|
||||
subscriptionVariantId,
|
||||
subscription.currency,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const orderProductPlan = order
|
||||
? await getProductPlan(order.items[0]?.variant_id, order.currency)
|
||||
const orderProductPlan =
|
||||
order && orderVariantId
|
||||
? await resolveProductPlan(billingConfig, orderVariantId, order.currency)
|
||||
: undefined;
|
||||
|
||||
const hasBillingData = subscription || order;
|
||||
@@ -59,7 +62,7 @@ async function PersonalAccountBillingPage() {
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex max-w-2xl flex-col space-y-4'}>
|
||||
<If
|
||||
condition={hasBillingData}
|
||||
fallback={
|
||||
@@ -68,7 +71,7 @@ async function PersonalAccountBillingPage() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={'flex w-full max-w-2xl flex-col space-y-6'}>
|
||||
<div className={'flex w-full flex-col space-y-6'}>
|
||||
<If condition={subscription}>
|
||||
{(subscription) => {
|
||||
return (
|
||||
@@ -95,9 +98,7 @@ async function PersonalAccountBillingPage() {
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={customerId}>
|
||||
<CustomerBillingPortalForm />
|
||||
</If>
|
||||
<If condition={customerId}>{() => <CustomerBillingPortalForm />}</If>
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
@@ -113,20 +114,3 @@ function CustomerBillingPortalForm() {
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProductPlan(
|
||||
variantId: string | undefined,
|
||||
currency: string,
|
||||
): Promise<
|
||||
| {
|
||||
product: ProductSchema;
|
||||
plan: z.infer<typeof PlanSchema>;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
if (!variantId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolveProductPlan(billingConfig, variantId, currency);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PlanSchema, ProductSchema } from '@kit/billing';
|
||||
import { resolveProductPlan } from '@kit/billing-gateway';
|
||||
import {
|
||||
BillingPortalCard,
|
||||
@@ -47,15 +45,15 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
|
||||
const [subscription, order, customerId] =
|
||||
await loadTeamAccountBillingPage(accountId);
|
||||
|
||||
const subscriptionProductPlan = subscription
|
||||
? await getProductPlan(
|
||||
subscription.items[0]?.variant_id,
|
||||
subscription.currency,
|
||||
)
|
||||
const variantId = subscription?.items[0]?.variant_id;
|
||||
const orderVariantId = order?.items[0]?.variant_id;
|
||||
|
||||
const subscriptionProductPlan = variantId
|
||||
? await resolveProductPlan(billingConfig, variantId, subscription.currency)
|
||||
: undefined;
|
||||
|
||||
const orderProductPlan = order
|
||||
? await getProductPlan(order.items[0]?.variant_id, order.currency)
|
||||
const orderProductPlan = orderVariantId
|
||||
? await resolveProductPlan(billingConfig, orderVariantId, order.currency)
|
||||
: undefined;
|
||||
|
||||
const hasBillingData = subscription || order;
|
||||
@@ -97,11 +95,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<div
|
||||
className={cn(`flex w-full flex-col space-y-4`, {
|
||||
'max-w-2xl': hasBillingData,
|
||||
})}
|
||||
>
|
||||
<div className={cn(`flex max-w-2xl flex-col space-y-4`)}>
|
||||
<If condition={!hasBillingData}>
|
||||
<Checkout />
|
||||
</If>
|
||||
@@ -154,20 +148,3 @@ function CannotManageBillingAlert() {
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProductPlan(
|
||||
variantId: string | undefined,
|
||||
currency: string,
|
||||
): Promise<
|
||||
| {
|
||||
product: ProductSchema;
|
||||
plan: z.infer<typeof PlanSchema>;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
if (!variantId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolveProductPlan(billingConfig, variantId, currency);
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@next/bundle-analyzer": "15.5.4",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/node": "^24.6.1",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/react": "19.1.16",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
@@ -86,7 +86,7 @@
|
||||
"pino-pretty": "13.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"supabase": "2.47.2",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss": "4.1.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 22 KiB |
@@ -5,7 +5,8 @@
|
||||
"planRenewal": "Renews every {{interval}} at {{price}}",
|
||||
"planDetails": "Plan Details",
|
||||
"checkout": "Proceed to Checkout",
|
||||
"trialEndsOn": "Your trial ends on",
|
||||
"trialAlertTitle": "Your Trial is ending soon",
|
||||
"trialAlertDescription": "Your trial ends on {{date}}. Upgrade to a paid plan to continue using all features.",
|
||||
"billingPortalCardButton": "Visit Billing Portal",
|
||||
"billingPortalCardTitle": "Manage your Billing Details",
|
||||
"billingPortalCardDescription": "Visit your Billing Portal to manage your subscription and billing. You can update or cancel your plan, or download your invoices.",
|
||||
@@ -45,6 +46,7 @@
|
||||
"forEveryUnit": "for every {{ unit }}",
|
||||
"setupFee": "plus a {{ setupFee }} setup fee",
|
||||
"perUnitIncluded": "({{included}} included)",
|
||||
"features": "Features",
|
||||
"featuresLabel": "Features",
|
||||
"detailsLabel": "Details",
|
||||
"planPickerLabel": "Pick your preferred plan",
|
||||
@@ -52,7 +54,7 @@
|
||||
"planPickerAlertErrorTitle": "Error requesting checkout",
|
||||
"planPickerAlertErrorDescription": "There was an error requesting checkout. Please try again later.",
|
||||
"subscriptionCancelled": "Subscription Cancelled",
|
||||
"cancelSubscriptionDate": "Your subscription will be cancelled at the end of the period",
|
||||
"cancelSubscriptionDate": "Your subscription will be cancelled at the end of the billing period on {{date}}",
|
||||
"noPlanChosen": "Please choose a plan",
|
||||
"noIntervalPlanChosen": "Please choose a billing interval",
|
||||
"status": {
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
|
||||
*,
|
||||
|
||||
@@ -10,35 +10,35 @@
|
||||
}
|
||||
|
||||
.markdoc h1 {
|
||||
@apply mt-8 lg:mt-14 text-4xl font-semibold font-heading tracking-tight dark:text-white text-foreground;
|
||||
@apply font-heading text-foreground text-3xl font-medium tracking-tight lg:mt-14 dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc h2 {
|
||||
@apply mb-3 lg:mb-6 mt-6 lg:mt-12 font-semibold text-2xl font-heading tracking-tight dark:text-white text-foreground;
|
||||
@apply font-heading text-foreground text-2xl font-medium tracking-tight lg:mt-6 lg:mb-3 dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc h3 {
|
||||
@apply mt-6 lg:mt-12 text-xl font-semibold font-heading tracking-tight dark:text-white text-foreground;
|
||||
@apply font-heading text-foreground text-xl font-medium tracking-tight lg:mt-12 dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc h4 {
|
||||
@apply mt-4 lg:mt-8 text-lg font-medium tracking-tight dark:text-white text-foreground;
|
||||
@apply text-foreground mt-4 text-lg font-medium tracking-tight lg:mt-8 dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc h5 {
|
||||
@apply mt-3 lg:mt-6 text-base font-medium tracking-tight dark:text-white text-foreground;
|
||||
@apply text-foreground mt-3 text-base font-medium tracking-tight lg:mt-6 dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc h6 {
|
||||
@apply mt-2 text-sm font-normal tracking-tight dark:text-white text-foreground;
|
||||
@apply text-foreground mt-2 text-sm font-normal tracking-tight dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc p {
|
||||
@apply mb-3 lg:mb-6 mt-2 lg:mt-4 text-base leading-7 text-muted-foreground;
|
||||
@apply text-muted-foreground my-3 text-base leading-7;
|
||||
}
|
||||
|
||||
.markdoc li {
|
||||
@apply relative my-1.5 text-base leading-7 text-muted-foreground;
|
||||
@apply text-muted-foreground relative my-0.5 text-base leading-7;
|
||||
}
|
||||
|
||||
.markdoc ul > li:before {
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.markdoc ol > li:before {
|
||||
@apply inline-flex font-medium text-muted-foreground;
|
||||
@apply text-secondary-foreground inline-flex font-medium;
|
||||
|
||||
content: counters(counts, '.') '. ';
|
||||
font-feature-settings: 'tnum';
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
.markdoc b,
|
||||
.markdoc strong {
|
||||
@apply font-semibold text-secondary-foreground dark:text-white;
|
||||
@apply text-secondary-foreground font-semibold dark:text-white;
|
||||
}
|
||||
|
||||
.markdoc img,
|
||||
@@ -80,28 +80,29 @@
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.markdoc p > code, .markdoc li > code {
|
||||
@apply p-0.5 text-sm font-semibold bg-muted/50 border font-mono text-secondary-foreground;
|
||||
.markdoc p > code,
|
||||
.markdoc li > code {
|
||||
@apply bg-muted/50 text-secondary-foreground border p-0.5 font-mono text-sm font-semibold;
|
||||
}
|
||||
|
||||
.markdoc pre {
|
||||
@apply overflow-x-auto bg-muted/50 rounded-md border border-border p-4 text-sm font-mono text-foreground;
|
||||
@apply bg-muted/50 border-border text-foreground my-4 overflow-x-auto rounded-md border p-4 font-mono text-sm;
|
||||
}
|
||||
|
||||
.markdoc blockquote {
|
||||
@apply my-4 border-l-8 border border-primary px-6 py-4 text-lg font-medium text-muted-foreground;
|
||||
@apply border-primary text-muted-foreground my-4 border border-l-8 px-6 py-4 text-lg font-medium;
|
||||
}
|
||||
|
||||
.markdoc a {
|
||||
@apply border-b-black border-b hover:border-b-2 pb-0.5 text-secondary-foreground font-semibold dark:border-yellow-300;
|
||||
@apply text-secondary-foreground border-b border-b-black pb-0.5 font-semibold hover:border-b-2 dark:border-yellow-300;
|
||||
}
|
||||
|
||||
.markdoc hr {
|
||||
@apply mt-8 mb-6 border-border;
|
||||
@apply border-border mt-8 mb-6;
|
||||
}
|
||||
|
||||
.markdoc [role='alert'] {
|
||||
@apply py-4 m-0 my-8;
|
||||
@apply m-0 my-8 py-4;
|
||||
}
|
||||
|
||||
.markdoc [role='alert'] * {
|
||||
@@ -114,7 +115,7 @@
|
||||
}
|
||||
|
||||
.markdoc table {
|
||||
@apply w-full caption-bottom text-sm my-4;
|
||||
@apply my-4 w-full caption-bottom text-sm;
|
||||
}
|
||||
|
||||
.markdoc th {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--font-sans:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
--font-heading: var(--font-sans);
|
||||
|
||||
--background: var(--color-white);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-supabase-saas-kit-turbo",
|
||||
"version": "2.16.1",
|
||||
"version": "2.17.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/node": "^24.6.1"
|
||||
"@types/node": "^24.6.2"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
@@ -24,7 +24,7 @@ export function BillingSessionStatus({
|
||||
className={
|
||||
'fade-in dark:border-border mx-auto max-w-xl rounded-xl border border-transparent p-16 xl:drop-shadow-2xl' +
|
||||
' bg-background animate-in slide-in-from-bottom-8 ease-out' +
|
||||
' zoom-in-50 dark:shadow-primary/20 duration-1000 dark:shadow-2xl'
|
||||
' zoom-in-50 dark:shadow-primary/5 duration-1000 dark:shadow-2xl'
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatDate } from 'date-fns';
|
||||
import { BadgeCheck } from 'lucide-react';
|
||||
import { BadgeCheck, InfoIcon, MessageCircleWarning } from 'lucide-react';
|
||||
|
||||
import { PlanSchema, type ProductSchema } from '@kit/billing';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
@@ -58,16 +58,16 @@ export function CurrentSubscriptionCard({
|
||||
|
||||
<CardContent className={'space-y-4 border-t pt-4 text-sm'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<div className={'flex items-center gap-x-3 text-lg font-semibold'}>
|
||||
<div className={'flex items-center gap-x-4 text-lg font-semibold'}>
|
||||
<span className={'flex items-center gap-x-1.5'}>
|
||||
<BadgeCheck
|
||||
className={
|
||||
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
|
||||
}
|
||||
className={'s-6 fill-green-500 text-white dark:text-stone-900'}
|
||||
/>
|
||||
|
||||
<span data-test={'current-plan-card-product-name'}>
|
||||
<Trans i18nKey={product.name} defaults={product.name} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<CurrentPlanBadge status={subscription.status} />
|
||||
</div>
|
||||
@@ -92,38 +92,7 @@ export function CurrentSubscriptionCard({
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={subscription.status === 'trialing'}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<span className="font-semibold">
|
||||
<Trans i18nKey="billing:trialEndsOn" />
|
||||
</span>
|
||||
|
||||
<div className={'text-muted-foreground'}>
|
||||
<span>
|
||||
{subscription.trial_ends_at
|
||||
? formatDate(subscription.trial_ends_at, 'P')
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={subscription.cancel_at_period_end}>
|
||||
<Alert variant={'warning'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="billing:subscriptionCancelled" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey="billing:cancelSubscriptionDate" />:
|
||||
<span className={'ml-1'}>
|
||||
{formatDate(subscription.period_ends_at ?? '', 'P')}
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex flex-col gap-y-1 border-y border-dashed py-4">
|
||||
<span className="font-semibold">
|
||||
<Trans i18nKey="billing:detailsLabel" />
|
||||
</span>
|
||||
@@ -134,6 +103,54 @@ export function CurrentSubscriptionCard({
|
||||
selectedInterval={firstLineItem.interval}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={subscription.status === 'trialing'}>
|
||||
{() => (
|
||||
<Alert variant={'info'}>
|
||||
<InfoIcon className={'h-4 w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="billing:trialAlertTitle" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="billing:trialAlertDescription"
|
||||
values={{
|
||||
date: formatDate(
|
||||
subscription.trial_ends_at ?? '',
|
||||
'MMMM d, yyyy',
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<If condition={subscription.cancel_at_period_end}>
|
||||
{() => (
|
||||
<Alert variant={'warning'}>
|
||||
<MessageCircleWarning className={'h-4 w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="billing:subscriptionCancelled" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="billing:cancelSubscriptionDate"
|
||||
values={{
|
||||
date: formatDate(
|
||||
subscription.period_ends_at ?? '',
|
||||
'MMMM d, yyyy',
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</If>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
RadioGroupItem,
|
||||
RadioGroupItemLabel,
|
||||
} from '@kit/ui/radio-group';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
@@ -125,7 +123,7 @@ export function PlanPicker(
|
||||
className={'flex flex-col gap-y-4 lg:flex-row lg:gap-x-4 lg:gap-y-0'}
|
||||
>
|
||||
<form
|
||||
className={'flex w-full max-w-xl flex-col gap-y-8'}
|
||||
className={'flex w-full flex-col gap-y-4'}
|
||||
onSubmit={form.handleSubmit(props.onSubmit)}
|
||||
>
|
||||
<If condition={intervals.length}>
|
||||
@@ -139,13 +137,9 @@ export function PlanPicker(
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'flex flex-col gap-4'}>
|
||||
<FormLabel htmlFor={'plan-picker-id'}>
|
||||
<Trans i18nKey={'common:billingInterval.label'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl id={'plan-picker-id'}>
|
||||
<RadioGroup name={field.name} value={field.value}>
|
||||
<div className={'flex space-x-2.5'}>
|
||||
<div className={'flex space-x-1'}>
|
||||
{intervals.map((interval) => {
|
||||
const selected = field.value === interval;
|
||||
|
||||
@@ -154,11 +148,10 @@ export function PlanPicker(
|
||||
htmlFor={interval}
|
||||
key={interval}
|
||||
className={cn(
|
||||
'focus-within:border-primary flex items-center gap-x-2.5 rounded-md border px-2.5 py-2 transition-colors',
|
||||
'focus-within:border-primary flex items-center gap-x-2.5 rounded-md px-2.5 py-2 transition-colors',
|
||||
{
|
||||
['bg-muted border-input']: selected,
|
||||
['hover:border-input border-transparent']:
|
||||
!selected,
|
||||
['bg-muted']: selected,
|
||||
['hover:bg-muted/50']: !selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -216,12 +209,12 @@ export function PlanPicker(
|
||||
name={'planId'}
|
||||
render={({ field }) => (
|
||||
<FormItem className={'flex flex-col gap-4'}>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:planPickerLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<RadioGroup value={field.value} name={field.name}>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
name={field.name}
|
||||
className="gap-y-0.5"
|
||||
>
|
||||
{visibleProducts.map((product) => {
|
||||
const plan = product.plans.find((item) => {
|
||||
if (item.paymentType === 'one-time') {
|
||||
@@ -251,7 +244,20 @@ export function PlanPicker(
|
||||
<RadioGroupItemLabel
|
||||
selected={selected}
|
||||
key={primaryLineItem.id}
|
||||
className="rounded-md !border-transparent"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-col content-center gap-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0'
|
||||
}
|
||||
>
|
||||
<Label
|
||||
htmlFor={plan.id}
|
||||
className={
|
||||
'flex flex-col justify-center space-y-2.5'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center space-x-2.5'}>
|
||||
<RadioGroupItem
|
||||
data-test-plan={plan.id}
|
||||
key={plan.id + selected}
|
||||
@@ -272,18 +278,6 @@ export function PlanPicker(
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-col content-center gap-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0'
|
||||
}
|
||||
>
|
||||
<Label
|
||||
htmlFor={plan.id}
|
||||
className={
|
||||
'flex flex-col justify-center space-y-2'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center space-x-2.5'}>
|
||||
<span className="font-semibold">
|
||||
<Trans
|
||||
i18nKey={`billing:plans.${product.id}.name`}
|
||||
@@ -363,6 +357,14 @@ export function PlanPicker(
|
||||
)}
|
||||
/>
|
||||
|
||||
{selectedPlan && selectedInterval && selectedProduct ? (
|
||||
<PlanDetails
|
||||
selectedInterval={selectedInterval}
|
||||
selectedPlan={selectedPlan}
|
||||
selectedProduct={selectedProduct}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
data-test="checkout-submit-button"
|
||||
@@ -385,14 +387,6 @@ export function PlanPicker(
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{selectedPlan && selectedInterval && selectedProduct ? (
|
||||
<PlanDetails
|
||||
selectedInterval={selectedInterval}
|
||||
selectedPlan={selectedPlan}
|
||||
selectedProduct={selectedProduct}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
@@ -427,78 +421,49 @@ function PlanDetails({
|
||||
<div
|
||||
key={key}
|
||||
className={
|
||||
'fade-in animate-in zoom-in-95 flex w-full flex-col space-y-4 py-2 lg:px-8'
|
||||
'fade-in animate-in flex w-full flex-col space-y-2 rounded-md border p-4'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col space-y-0.5'}>
|
||||
<span className={'text-sm font-medium'}>
|
||||
<b>
|
||||
<Trans
|
||||
i18nKey={`billing:plans.${selectedProduct.id}.name`}
|
||||
defaults={selectedProduct.name}
|
||||
/>
|
||||
</b>{' '}
|
||||
<If condition={isRecurring}>
|
||||
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} />
|
||||
</If>
|
||||
</span>
|
||||
|
||||
<p>
|
||||
<span className={'text-muted-foreground text-sm'}>
|
||||
<Trans
|
||||
i18nKey={`billing:plans.${selectedProduct.id}.description`}
|
||||
defaults={selectedProduct.description}
|
||||
/>
|
||||
</span>
|
||||
</p>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm font-semibold'}>{selectedProduct.name}</span>
|
||||
</div>
|
||||
|
||||
<If condition={selectedPlan.lineItems.length > 0}>
|
||||
<Separator />
|
||||
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<span className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:detailsLabel'} />
|
||||
</span>
|
||||
|
||||
<LineItemDetails
|
||||
lineItems={selectedPlan.lineItems ?? []}
|
||||
selectedInterval={isRecurring ? selectedInterval : undefined}
|
||||
currency={selectedProduct.currency}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<span className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:featuresLabel'} />
|
||||
</span>
|
||||
|
||||
<div className={'flex flex-wrap gap-1'}>
|
||||
{selectedProduct.features.map((item) => {
|
||||
return (
|
||||
<div key={item} className={'flex items-center gap-x-2 text-sm'}>
|
||||
<CheckCircle className={'h-4 text-green-500'} />
|
||||
<Badge
|
||||
key={item}
|
||||
className={'flex items-center gap-x-2'}
|
||||
variant={'outline'}
|
||||
>
|
||||
<CheckCircle className={'h-3 w-3 text-green-500'} />
|
||||
|
||||
<span className={'text-secondary-foreground'}>
|
||||
<span className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={item} defaults={item} />
|
||||
</span>
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Price(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
'animate-in slide-in-from-left-4 fade-in text-xl font-semibold tracking-tight duration-500'
|
||||
}
|
||||
>
|
||||
<span className={'animate-in fade-in text-xl font-medium tracking-tight'}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export function PricingTable({
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-start space-y-6 lg:space-y-0' +
|
||||
' justify-center lg:flex-row lg:space-x-4'
|
||||
' justify-center lg:flex-row lg:gap-x-2.5'
|
||||
}
|
||||
>
|
||||
{visibleProducts.map((product) => {
|
||||
@@ -171,17 +171,13 @@ function PricingItem(
|
||||
data-cy={'subscription-plan'}
|
||||
className={cn(
|
||||
props.className,
|
||||
`s-full relative flex flex-1 grow flex-col items-stretch justify-between self-stretch rounded-lg border px-6 py-5 lg:w-4/12 xl:max-w-[20rem]`,
|
||||
{
|
||||
['border-primary']: highlighted,
|
||||
['border-border']: !highlighted,
|
||||
},
|
||||
`s-full bg-muted/50 relative flex flex-1 grow flex-col items-stretch justify-between self-stretch rounded px-6 py-5 lg:w-4/12 xl:max-w-[20rem]`,
|
||||
)}
|
||||
>
|
||||
<If condition={props.product.badge}>
|
||||
<div className={'absolute -top-2.5 left-0 flex w-full justify-center'}>
|
||||
<Badge
|
||||
className={highlighted ? '' : 'bg-background'}
|
||||
className={highlighted ? '' : 'bg-muted'}
|
||||
variant={highlighted ? 'default' : 'outline'}
|
||||
>
|
||||
<span>
|
||||
@@ -194,12 +190,12 @@ function PricingItem(
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className={'flex flex-col gap-y-5'}>
|
||||
<div className={'flex flex-col gap-y-1'}>
|
||||
<div className={'flex flex-col gap-y-4'}>
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'flex items-center space-x-6'}>
|
||||
<b
|
||||
className={
|
||||
'text-secondary-foreground font-heading text-xl font-medium tracking-tight'
|
||||
'text-secondary-foreground font-heading text-xl font-medium tracking-tight text-orange-800'
|
||||
}
|
||||
>
|
||||
<Trans
|
||||
@@ -208,9 +204,20 @@ function PricingItem(
|
||||
/>
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(`text-muted-foreground text-base tracking-tight`)}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={props.product.description}
|
||||
defaults={props.product.description}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={'mt-6 flex flex-col gap-y-1'}>
|
||||
<div className={'h-px w-full border border-dashed'} />
|
||||
|
||||
<div className={'flex flex-col gap-y-1'}>
|
||||
<Price
|
||||
isMonthlyPrice={props.alwaysDisplayMonthlyPrice}
|
||||
displayBillingPeriod={!props.plan.label}
|
||||
@@ -296,13 +303,6 @@ function PricingItem(
|
||||
</If>
|
||||
</If>
|
||||
|
||||
<span className={cn(`text-muted-foreground text-base tracking-tight`)}>
|
||||
<Trans
|
||||
i18nKey={props.product.description}
|
||||
defaults={props.product.description}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className={'h-px w-full border border-dashed'} />
|
||||
|
||||
<div className={'flex flex-col'}>
|
||||
@@ -389,9 +389,9 @@ function ListItem({
|
||||
highlighted: boolean;
|
||||
}>) {
|
||||
return (
|
||||
<li className={'flex items-center gap-x-2.5'}>
|
||||
<li className={'flex items-center gap-x-2'}>
|
||||
<CheckCircle
|
||||
className={cn('h-4 min-h-4 w-4 min-w-4', {
|
||||
className={cn('h-3.5 min-h-3.5 w-3.5 min-w-3.5', {
|
||||
'text-secondary-foreground': highlighted,
|
||||
'text-muted-foreground': !highlighted,
|
||||
})}
|
||||
@@ -417,7 +417,7 @@ function PlanIntervalSwitcher(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex gap-x-1 rounded-full border p-1.5'}>
|
||||
<div className={'flex gap-x-1 rounded-full border'}>
|
||||
{props.intervals.map((plan, index) => {
|
||||
const selected = plan === props.interval;
|
||||
|
||||
@@ -434,8 +434,7 @@ function PlanIntervalSwitcher(
|
||||
return (
|
||||
<Button
|
||||
key={plan}
|
||||
size={'sm'}
|
||||
variant={selected ? 'default' : 'ghost'}
|
||||
variant={selected ? 'secondary' : 'ghost'}
|
||||
className={className}
|
||||
onClick={() => props.setInterval(plan)}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'server-only';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/wordpress": "workspace:*",
|
||||
"@types/node": "^24.6.1"
|
||||
"@types/node": "^24.6.2"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/react": "19.1.16",
|
||||
"react": "19.1.1",
|
||||
"zod": "^3.25.74"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"@types/react": "19.1.16",
|
||||
"wp-types": "^4.68.1"
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"react-i18next": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.5.2",
|
||||
"i18next": "25.5.3",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@kit/resend": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@kit/mailers-shared": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "1.18.2",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/node": "^24.6.2",
|
||||
"postgres": "3.4.7",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"react-hook-form": "^7.63.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss": "4.1.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"zod": "^3.25.74"
|
||||
|
||||
@@ -9,7 +9,7 @@ export function If<Value = unknown>({
|
||||
}: React.PropsWithoutRef<{
|
||||
condition: Condition<Value>;
|
||||
children: React.ReactNode | ((value: Value) => React.ReactNode);
|
||||
fallback?: React.ReactNode;
|
||||
fallback?: React.ReactNode | (() => React.ReactNode);
|
||||
}>) {
|
||||
return useMemo(() => {
|
||||
if (condition) {
|
||||
@@ -21,6 +21,10 @@ export function If<Value = unknown>({
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
if (typeof fallback === 'function') {
|
||||
return <>{fallback()}</>;
|
||||
}
|
||||
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
|
||||
61
packages/ui/src/makerkit/marketing/ecosystem-showcase.tsx
Normal file
61
packages/ui/src/makerkit/marketing/ecosystem-showcase.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface EcosystemShowcaseProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
heading: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
textPosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const EcosystemShowcase: React.FC<EcosystemShowcaseProps> =
|
||||
function EcosystemShowcaseComponent({
|
||||
className,
|
||||
heading,
|
||||
description,
|
||||
textPosition = 'left',
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted/50 flex flex-1 flex-col space-y-8 rounded-md p-6 lg:space-y-0 lg:space-x-16',
|
||||
className,
|
||||
{
|
||||
'lg:flex-row': textPosition === 'left',
|
||||
'lg:flex-row-reverse': textPosition === 'right',
|
||||
},
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full w-full flex-col items-start gap-y-4 text-left lg:w-1/3',
|
||||
{
|
||||
'text-right': textPosition === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<h2 className="text-secondary-foreground text-3xl font-normal tracking-tight">
|
||||
{heading}
|
||||
</h2>
|
||||
|
||||
{description && (
|
||||
<p className="text-muted-foreground mt-2 text-base lg:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full lg:w-2/3',
|
||||
textPosition === 'right' && 'm-0 text-right',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,9 +15,9 @@ export const FeatureCard: React.FC<FeatureCardProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('rounded-xl border p-4', className)} {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-medium">{label}</CardTitle>
|
||||
<div className={cn('bg-muted/50 rounded', className)} {...props}>
|
||||
<CardHeader className="p-4">
|
||||
<CardTitle className="text-lg font-medium">{label}</CardTitle>
|
||||
|
||||
<CardDescription className="text-muted-foreground max-w-xs text-sm font-normal">
|
||||
{description}
|
||||
|
||||
@@ -20,12 +20,13 @@ export const FeatureShowcase: React.FC<FeatureShowcaseProps> =
|
||||
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">
|
||||
<div className="flex w-full 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>
|
||||
);
|
||||
@@ -40,7 +41,7 @@ export function FeatureShowcaseIconContainer(
|
||||
<div className={'flex'}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center space-x-4 rounded-lg p-3 font-medium',
|
||||
'flex items-center justify-center space-x-2.5 font-medium',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
'site-footer relative mt-auto w-full py-8 2xl:py-20',
|
||||
'site-footer bg-muted/20 relative mt-auto w-full py-8 2xl:py-20',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -39,9 +39,7 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm tracking-tight">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex text-xs">
|
||||
@@ -51,10 +49,10 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
</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">
|
||||
<div className="flex w-full flex-1 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">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<FooterSectionHeading>{section.heading}</FooterSectionHeading>
|
||||
|
||||
<FooterSectionList>
|
||||
@@ -76,14 +74,14 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
|
||||
function FooterSectionHeading(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<span className="font-heading text-sm font-semibold tracking-tight">
|
||||
<span className="font-heading text-secondary-foreground/90 text-sm font-medium">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterSectionList(props: React.PropsWithChildren) {
|
||||
return <ul className="flex flex-col gap-y-2">{props.children}</ul>;
|
||||
return <ul className="flex flex-col gap-y-1">{props.children}</ul>;
|
||||
}
|
||||
|
||||
function FooterLink({
|
||||
@@ -91,7 +89,7 @@ function FooterLink({
|
||||
children,
|
||||
}: React.PropsWithChildren<{ href: string }>) {
|
||||
return (
|
||||
<li className="text-muted-foreground text-sm tracking-tight hover:underline [&>a]:transition-colors">
|
||||
<li className="text-muted-foreground text-sm font-medium hover:underline [&>a]:transition-colors">
|
||||
<a href={href}>{children}</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Header: React.FC<HeaderProps> = function ({
|
||||
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',
|
||||
'site-header bg-background/80 dark:bg-background/80 sticky top-0 z-10 w-full backdrop-blur-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@ export const HeroTitle: React.FC<
|
||||
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',
|
||||
'hero-title flex flex-col text-center font-sans text-4xl font-medium tracking-tighter sm:text-6xl lg:max-w-5xl lg:text-7xl xl:max-w-6xl dark:text-white',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Hero({
|
||||
animate = true,
|
||||
}: HeroProps) {
|
||||
return (
|
||||
<div className={cn('mx-auto flex flex-col space-y-20', className)}>
|
||||
<div className={cn('mx-auto flex flex-col space-y-14', className)}>
|
||||
<div
|
||||
style={{
|
||||
MozAnimationDuration: '100ms',
|
||||
@@ -35,7 +35,7 @@ export function Hero({
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-1 flex-col items-center gap-y-6 xl:gap-y-8 2xl:gap-y-12">
|
||||
<div className="flex w-full flex-1 flex-col items-center gap-y-6 xl:gap-y-8">
|
||||
{pill && (
|
||||
<div
|
||||
className={cn({
|
||||
@@ -51,8 +51,8 @@ export function Hero({
|
||||
<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">
|
||||
<div className="flex max-w-3xl">
|
||||
<h3 className="text-secondary-foreground/70 p-0 text-center font-sans text-xl font-medium tracking-tight">
|
||||
{subtitle}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from './feature-card';
|
||||
export * from './newsletter-signup';
|
||||
export * from './newsletter-signup-container';
|
||||
export * from './coming-soon';
|
||||
export * from './ecosystem-showcase';
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Pill: React.FC<
|
||||
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',
|
||||
'bg-muted/50 flex min-h-10 items-center gap-x-1.5 rounded-full border px-2 py-1 text-center text-sm font-medium text-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SecondaryHero: React.FC<SecondaryHeroProps> =
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center space-y-6 text-center',
|
||||
'flex flex-col items-center space-y-4 text-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -31,7 +31,7 @@ export const SecondaryHero: React.FC<SecondaryHeroProps> =
|
||||
{heading}
|
||||
</Heading>
|
||||
|
||||
<h3 className="text-muted-foreground font-sans text-xl font-normal tracking-tight">
|
||||
<h3 className="text-secondary-foreground/70 text-center font-sans text-xl font-medium tracking-tight">
|
||||
{subheading}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -6,24 +6,30 @@ export function Spinner(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div role="status">
|
||||
<div role="status" aria-label="loading">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
`fill-primary-foreground text-primary dark:fill-primary dark:text-primary/30 h-8 w-8 animate-spin`,
|
||||
props.className,
|
||||
'stroke-muted-foreground h-6 w-6 animate-spin',
|
||||
)}
|
||||
viewBox="0 0 100 101"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9023_61563)">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
d="M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444"
|
||||
stroke="stroke-current"
|
||||
strokeWidth="1.4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<clipPath id="clip0_9023_61563">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn('bg-card text-card-foreground rounded-xl border', className)}
|
||||
className={cn('bg-card text-card-foreground rounded-lg border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -25,13 +25,13 @@ const RadioGroupItem: React.FC<
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
className={cn(
|
||||
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow-xs focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<CheckIcon className="fill-primary h-3.5 w-3.5" />
|
||||
<CheckIcon className="fill-primary animate-in fade-in slide-in-from-left-4 h-3.5 w-3.5" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
@@ -46,14 +46,15 @@ const RadioGroupItemLabel = (
|
||||
) => {
|
||||
return (
|
||||
<label
|
||||
data-selected={props.selected}
|
||||
className={cn(
|
||||
props.className,
|
||||
'flex cursor-pointer rounded-md' +
|
||||
' border-input items-center space-x-4 border' +
|
||||
' transition-duration-500 focus-within:border-primary p-4 text-sm transition-all',
|
||||
'focus-within:border-primary active:bg-muted p-2.5 text-sm transition-all',
|
||||
{
|
||||
[`bg-muted`]: props.selected,
|
||||
[`hover:bg-muted`]: !props.selected,
|
||||
[`bg-muted/70`]: props.selected,
|
||||
[`hover:bg-muted/50`]: !props.selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
1065
pnpm-lock.yaml
generated
1065
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user