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:
Giancarlo Buomprisco
2025-10-02 15:14:11 +08:00
committed by GitHub
parent d8bb7f56df
commit 54d6b4897f
56 changed files with 1014 additions and 1142 deletions

View File

@@ -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>

View File

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

View File

@@ -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,
},
)}
className={cn('block rounded-md object-cover', {
className,
})}
src={src}
priority={preloadImage}
alt={`Cover Image for ${title}`}

View File

@@ -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>

View File

@@ -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>
<CoverImage
preloadImage={preloadImage}
title={title}
src={imageUrl}
/>
</div>
)}
</If>
<div className={'flex flex-col space-y-4 px-1'}>
<div className={'flex flex-col space-y-2'}>
<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-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;
}

View File

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

View File

@@ -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={

View File

@@ -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'}>

View File

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

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View File

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

View File

@@ -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'
}
className={'flex items-center justify-between p-4 hover:cursor-pointer'}
>
<h2
className={
'hover:underline-none cursor-pointer font-sans font-medium'
}
>
<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>

View File

@@ -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>

View File

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

View File

@@ -1,6 +1,3 @@
import { z } from 'zod';
import { PlanSchema, ProductSchema } from '@kit/billing';
import { resolveProductPlan } from '@kit/billing-gateway';
import {
BillingPortalCard,
@@ -38,16 +35,22 @@ async function PersonalAccountBillingPage() {
const [subscription, order, customerId] =
await loadPersonalAccountBillingPageData(user.id);
const subscriptionProductPlan = subscription
? await getProductPlan(
subscription.items[0]?.variant_id,
subscription.currency,
)
: undefined;
const subscriptionVariantId = subscription?.items[0]?.variant_id;
const orderVariantId = order?.items[0]?.variant_id;
const orderProductPlan = order
? await getProductPlan(order.items[0]?.variant_id, order.currency)
: undefined;
const subscriptionProductPlan =
subscription && subscriptionVariantId
? await resolveProductPlan(
billingConfig,
subscriptionVariantId,
subscription.currency,
)
: undefined;
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);
}

View File

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