Update UI design across multiple pages and components
Several changes have been made to improve the user interface and enhance the user experience. This includes redesigning Auth buttons, modifying website layouts and routing, tweaking heading and text sizes for clarity, and revamping the marketing, documentation, and pricing pages. These changes collectively contribute to a cleaner, more concise and navigable interface.
This commit is contained in:
@@ -3,7 +3,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
function CookiePolicyPage() {
|
function CookiePolicyPage() {
|
||||||
return (
|
return (
|
||||||
<div className={'mt-8'}>
|
<div>
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={`Cookie Policy`}
|
title={`Cookie Policy`}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const YEAR = new Date().getFullYear();
|
|||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className={'py-8 lg:py-24'}>
|
<footer className={'border-t py-8 xl:py-12 2xl:py-14'}>
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div className={'flex flex-col space-y-8 lg:flex-row lg:space-y-0'}>
|
<div className={'flex flex-col space-y-8 lg:flex-row lg:space-y-0'}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown
|
|||||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { ModeToggle } from '@kit/ui/mode-toggle';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||||
@@ -55,10 +56,18 @@ function SuspendedPersonalAccountDropdown(props: { user: User | null }) {
|
|||||||
|
|
||||||
function AuthButtons() {
|
function AuthButtons() {
|
||||||
return (
|
return (
|
||||||
<div className={'hidden space-x-2 lg:flex'}>
|
<div className={'hidden space-x-0.5 lg:flex'}>
|
||||||
|
<ModeToggle />
|
||||||
|
|
||||||
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
|
<Button variant={'link'}>
|
||||||
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link href={pathsConfig.auth.signUp}>
|
<Link href={pathsConfig.auth.signUp}>
|
||||||
<Button variant={'outline'}>
|
<Button className={'rounded-full'}>
|
||||||
<Trans i18nKey={'auth:signUp'} />
|
<Trans i18nKey={'auth:getStarted'} />
|
||||||
<ChevronRight className={'h-4'} />
|
<ChevronRight className={'h-4'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import type { User } from '@supabase/supabase-js';
|
import type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { ModeToggle } from '@kit/ui/mode-toggle';
|
|
||||||
|
|
||||||
import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header-account-section';
|
import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header-account-section';
|
||||||
import { SiteNavigation } from '~/(marketing)/_components/site-navigation';
|
import { SiteNavigation } from '~/(marketing)/_components/site-navigation';
|
||||||
import { AppLogo } from '~/components/app-logo';
|
import { AppLogo } from '~/components/app-logo';
|
||||||
|
|
||||||
export function SiteHeader(props: { user?: User | null }) {
|
export function SiteHeader(props: { user?: User | null }) {
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div className={'border-b'}>
|
||||||
<div className="flex h-16 items-center justify-between">
|
<div className={'container mx-auto'}>
|
||||||
<div className={'w-4/12'}>
|
<div className="flex h-16 items-center justify-between">
|
||||||
<AppLogo />
|
<div className={'flex w-6/12 items-center space-x-8'}>
|
||||||
</div>
|
<AppLogo />
|
||||||
|
|
||||||
<div className={'hidden w-4/12 justify-center lg:flex'}>
|
<SiteNavigation />
|
||||||
<SiteNavigation />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex flex-1 items-center justify-end space-x-4'}>
|
<div className={'flex flex-1 items-center justify-end space-x-1'}>
|
||||||
<ModeToggle />
|
<SiteHeaderAccountSection user={props.user ?? null} />
|
||||||
|
</div>
|
||||||
<SiteHeaderAccountSection user={props.user ?? null} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import { cn, isRouteActive } from '@kit/ui/utils';
|
|||||||
const getClassName = (path: string, currentPathName: string) => {
|
const getClassName = (path: string, currentPathName: string) => {
|
||||||
const isActive = isRouteActive(path, currentPathName);
|
const isActive = isRouteActive(path, currentPathName);
|
||||||
|
|
||||||
return cn(`text-sm transition-all px-4 py-2 rounded-full font-medium`, {
|
return cn(`text-sm font-medium text-primary`, {
|
||||||
'bg-muted': isActive,
|
'hover:underline': !isActive,
|
||||||
'hover:bg-muted active:bg-muted/50': !isActive,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ export function SiteNavigation() {
|
|||||||
<>
|
<>
|
||||||
<div className={'hidden items-center lg:flex'}>
|
<div className={'hidden items-center lg:flex'}>
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList
|
<NavigationMenuList className={'space-x-6'}>
|
||||||
className={
|
|
||||||
'space-x-1.5 rounded-full px-1 py-2 shadow-sm dark:shadow-primary/10'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{NavItems}
|
{NavItems}
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ export function SitePageHeader(props: {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('border-b py-8 xl:py-12 2xl:py-14', props.className)}>
|
||||||
className={cn('flex flex-col items-center space-y-4', props.className)}
|
<div className={'container flex flex-col space-y-4'}>
|
||||||
>
|
<h1 className={'text-3xl font-semibold xl:text-5xl'}>{props.title}</h1>
|
||||||
<h1 className={'text-center text-3xl font-semibold xl:text-4xl'}>
|
|
||||||
{props.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2 className={'text-center text-xl text-muted-foreground xl:text-2xl'}>
|
<h2
|
||||||
<span className={'font-normal'}>{props.subtitle}</span>
|
className={
|
||||||
</h2>
|
'text-base text-secondary-foreground xl:text-lg 2xl:text-xl'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={'font-normal'}>{props.subtitle}</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
import Post from '~/(marketing)/blog/_components/post';
|
import { Post } from '~/(marketing)/blog/_components/post';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
@@ -55,11 +55,7 @@ async function BlogPost({ params }: { params: { slug: string } }) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Post post={post} content={post.content} />;
|
||||||
<div className={'container mx-auto'}>
|
|
||||||
<Post post={post} content={post.content} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(BlogPost);
|
export default withI18n(BlogPost);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { Cms } from '@kit/cms';
|
import { Cms } from '@kit/cms';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Button } from '@kit/ui/button';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
|
import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
|
||||||
import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter';
|
import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter';
|
||||||
@@ -11,27 +17,37 @@ export const PostHeader: React.FC<{
|
|||||||
const { title, publishedAt, description, image } = post;
|
const { title, publishedAt, description, image } = post;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-8'}>
|
<div className={'flex flex-col'}>
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'container'}>
|
||||||
<Heading level={1}>{title}</Heading>
|
<Link href={'/blog'}>
|
||||||
|
<Button variant={'link'}>
|
||||||
|
<ArrowLeft className={'h-4'} />
|
||||||
|
|
||||||
<Heading level={3}>
|
<span>
|
||||||
<p
|
<Trans i18nKey={'marketing:backToBlog'} />
|
||||||
className={'font-normal text-muted-foreground'}
|
</span>
|
||||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
</Button>
|
||||||
/>
|
</Link>
|
||||||
</Heading>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-2 text-muted-foreground">
|
<div className={cn('border-b py-8')}>
|
||||||
<div className={'text-sm'}>
|
<div className={'container flex flex-col space-y-4'}>
|
||||||
<DateFormatter dateString={publishedAt.toISOString()} />
|
<h1 className={'text-3xl font-semibold xl:text-5xl'}>{title}</h1>
|
||||||
</div>
|
|
||||||
|
<h2 className={'text-base text-secondary-foreground xl:text-lg'}>
|
||||||
|
<span
|
||||||
|
className={'font-normal'}
|
||||||
|
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<DateFormatter dateString={publishedAt.toISOString()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<If condition={image}>
|
<If condition={image}>
|
||||||
{(imageUrl) => (
|
{(imageUrl) => (
|
||||||
<div className="relative mx-auto h-[378px] w-full justify-center">
|
<div className="relative mx-auto mt-8 flex h-[378px] w-full max-w-2xl justify-center">
|
||||||
<CoverImage
|
<CoverImage
|
||||||
preloadImage
|
preloadImage
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function PostPreview({
|
|||||||
const slug = `/blog/${post.slug}`;
|
const slug = `/blog/${post.slug}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl transition-shadow duration-500">
|
<div className="flex flex-col space-y-4 rounded-lg transition-shadow duration-500">
|
||||||
<If condition={image}>
|
<If condition={image}>
|
||||||
{(imageUrl) => (
|
{(imageUrl) => (
|
||||||
<div className="relative mb-2 w-full" style={{ height }}>
|
<div className="relative mb-2 w-full" style={{ height }}>
|
||||||
@@ -39,23 +39,23 @@ export function PostPreview({
|
|||||||
)}
|
)}
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<div className={'px-1'}>
|
<div className={'flex flex-col space-y-2 px-1'}>
|
||||||
<div className="flex flex-col space-y-1 py-2">
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<h3 className="text-2xl font-bold leading-snug dark:text-white">
|
<h3 className="text-2xl font-semibold leading-snug">
|
||||||
<Link href={slug} className="hover:underline">
|
<Link href={slug} className="hover:underline">
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-2 flex flex-row items-center space-x-2 text-sm">
|
<div className="flex flex-row items-center space-x-2 text-sm">
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
<DateFormatter dateString={publishedAt.toISOString()} />
|
<DateFormatter dateString={publishedAt.toISOString()} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className="mb-4 text-sm leading-relaxed text-muted-foreground"
|
className="mb-4 text-base leading-relaxed text-secondary-foreground"
|
||||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ export const Post: React.FC<{
|
|||||||
content: string;
|
content: string;
|
||||||
}> = ({ post, content }) => {
|
}> = ({ post, content }) => {
|
||||||
return (
|
return (
|
||||||
<div className={'mx-auto flex max-w-2xl flex-col space-y-6'}>
|
<div>
|
||||||
<PostHeader post={post} />
|
<PostHeader post={post} />
|
||||||
|
|
||||||
<article className={styles.HTML}>
|
<div className={'mx-auto flex max-w-2xl flex-col space-y-6'}>
|
||||||
<ContentRenderer content={content} />
|
<article className={styles.HTML}>
|
||||||
</article>
|
<ContentRenderer content={content} />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Post;
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ async function BlogPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div className={'flex flex-col space-y-12'}>
|
||||||
<div className={'flex flex-col space-y-12 xl:space-y-24'}>
|
<SitePageHeader
|
||||||
<SitePageHeader
|
title={t('marketing:blog')}
|
||||||
title={t('marketing:blog')}
|
subtitle={t('marketing:blogSubtitle')}
|
||||||
subtitle={t('marketing:blogSubtitle')}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
|
<div className={'container mx-auto'}>
|
||||||
<GridList>
|
<GridList>
|
||||||
{posts.map((post, idx) => {
|
{posts.map((post, idx) => {
|
||||||
return <PostPreview key={idx} post={post} />;
|
return <PostPreview key={idx} post={post} />;
|
||||||
|
|||||||
@@ -48,18 +48,18 @@ async function DocumentationPage({ params }: PageParams) {
|
|||||||
const description = page?.description ?? '';
|
const description = page?.description ?? '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div>
|
||||||
|
<SitePageHeader
|
||||||
|
title={page.title}
|
||||||
|
subtitle={description}
|
||||||
|
className={'items-start'}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'relative mx-auto flex max-w-4xl grow flex-col space-y-4 px-8'
|
'container relative mx-auto flex max-w-4xl grow flex-col space-y-4 py-6'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SitePageHeader
|
|
||||||
title={page.title}
|
|
||||||
subtitle={description}
|
|
||||||
className={'items-start'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<article className={styles.HTML}>
|
<article className={styles.HTML}>
|
||||||
<ContentRenderer content={page.content} />
|
<ContentRenderer content={page.content} />
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -27,13 +27,15 @@ async function DocsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<div className={'flex flex-col items-center space-y-12 xl:space-y-24'}>
|
<div className={'flex flex-col space-y-12 xl:space-y-24'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:documentation')}
|
title={t('marketing:documentation')}
|
||||||
subtitle={t('marketing:documentationSubtitle')}
|
subtitle={t('marketing:documentationSubtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocsCards cards={cards} />
|
<div className={'flex flex-col items-center'}>
|
||||||
|
<DocsCards cards={cards} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ChevronDown } from 'lucide-react';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ArrowRight, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
@@ -60,34 +65,40 @@ async function FAQPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<script
|
<script
|
||||||
key={'ld:json'}
|
key={'ld:json'}
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'container mx-auto'}>
|
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
|
||||||
<div className={'flex flex-col space-y-12 xl:space-y-24'}>
|
<SitePageHeader
|
||||||
<SitePageHeader
|
title={t('marketing:faq')}
|
||||||
title={t('marketing:faq')}
|
subtitle={t('marketing:faqSubtitle')}
|
||||||
subtitle={t('marketing:faqSubtitle')}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div className={'container flex flex-col space-y-8 pb-16'}>
|
||||||
className={
|
<div className="flex w-full max-w-xl flex-col">
|
||||||
'm-auto flex w-full max-w-xl items-center justify-center'
|
{faqItems.map((item, index) => {
|
||||||
}
|
return <FaqItem key={index} item={item} />;
|
||||||
>
|
})}
|
||||||
<div className="flex w-full flex-col">
|
</div>
|
||||||
{faqItems.map((item, index) => {
|
|
||||||
return <FaqItem key={index} item={item} />;
|
<div>
|
||||||
})}
|
<Link href={'/contact'}>
|
||||||
</div>
|
<Button variant={'outline'}>
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey={'marketing:contactFaq'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ArrowRight className={'ml-2 w-4'} />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ function FaqItem({
|
|||||||
};
|
};
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<details className={'group border-b px-2 py-4'}>
|
<details className={'group border-b px-2 py-4 last:border-b-transparent'}>
|
||||||
<summary
|
<summary
|
||||||
className={
|
className={
|
||||||
'flex items-center justify-between hover:cursor-pointer hover:underline'
|
'flex items-center justify-between hover:cursor-pointer hover:underline'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
import { Separator } from '@kit/ui/separator';
|
|
||||||
|
|
||||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||||
@@ -9,13 +8,11 @@ async function SiteLayout(props: React.PropsWithChildren) {
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-6 xl:space-y-10 2xl:space-y-12'}>
|
<div className={'flex flex-col'}>
|
||||||
<SiteHeader user={user} />
|
<SiteHeader user={user} />
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-24'}>
|
<div className={'flex flex-col space-y-24 py-16'}>
|
||||||
<div className={'container mx-auto flex flex-col space-y-24'}>
|
<div className={'container mx-auto flex flex-col space-y-24'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -22,7 +22,11 @@ function Home() {
|
|||||||
' duration-1000 slide-in-from-top-12'
|
' duration-1000 slide-in-from-top-12'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex w-full flex-1 flex-col items-center space-y-8'}>
|
<div
|
||||||
|
className={
|
||||||
|
'flex w-full flex-1 flex-col items-center space-y-8 xl:space-y-12 2xl:space-y-14'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Pill>
|
<Pill>
|
||||||
<span>The leading SaaS Starter Kit for ambitious developers</span>
|
<span>The leading SaaS Starter Kit for ambitious developers</span>
|
||||||
</Pill>
|
</Pill>
|
||||||
@@ -53,9 +57,7 @@ function Home() {
|
|||||||
</span>
|
</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<MainCallToActionButton />
|
<MainCallToActionButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,9 +72,7 @@ function Home() {
|
|||||||
<Image
|
<Image
|
||||||
priority
|
priority
|
||||||
className={
|
className={
|
||||||
'rounded-2xl' +
|
'rounded-lg border delay-500 duration-1000 ease-out animate-in fade-in zoom-in-50 fill-mode-both'
|
||||||
' border animate-in fade-in' +
|
|
||||||
' delay-300 duration-1000 ease-out zoom-in-50 fill-mode-both'
|
|
||||||
}
|
}
|
||||||
width={3069}
|
width={3069}
|
||||||
height={1916}
|
height={1916}
|
||||||
@@ -115,20 +115,20 @@ function Home() {
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<FeatureShowcaseContainer>
|
<FeatureShowcaseContainer>
|
||||||
<LeftFeatureContainer>
|
<LeftFeatureContainer>
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<Heading level={2}>Authentication</Heading>
|
<Heading level={2}>Authentication</Heading>
|
||||||
|
|
||||||
<Heading level={3} className={'text-muted-foreground'}>
|
<Heading level={3} className={'text-muted-foreground'}>
|
||||||
Secure and Easy-to-Use Authentication for Your SaaS Website
|
Secure and Easy-to-Use Authentication for Your SaaS Website
|
||||||
and API
|
and API
|
||||||
</Heading>
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Our authentication system is built on top of the
|
Our authentication system is built on top of the
|
||||||
industry-leading PaaS such as Supabase and Firebase. It is
|
industry-leading PaaS such as Supabase and Firebase. It is
|
||||||
secure, easy-to-use, and fully customizable. It supports
|
secure, easy-to-use, and fully customizable. It supports
|
||||||
email/password, social logins, and more.
|
email/password, social logins, and more.
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</LeftFeatureContainer>
|
</LeftFeatureContainer>
|
||||||
|
|
||||||
@@ -155,18 +155,18 @@ function Home() {
|
|||||||
</LeftFeatureContainer>
|
</LeftFeatureContainer>
|
||||||
|
|
||||||
<RightFeatureContainer>
|
<RightFeatureContainer>
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<Heading level={2}>Dashboard</Heading>
|
<Heading level={2}>Dashboard</Heading>
|
||||||
|
|
||||||
<Heading level={3} className={'text-muted-foreground'}>
|
<Heading level={3} className={'text-muted-foreground'}>
|
||||||
A fantastic dashboard to manage your SaaS business
|
A fantastic dashboard to manage your SaaS business
|
||||||
</Heading>
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Our dashboard offers an overview of your SaaS business. It
|
Our dashboard offers an overview of your SaaS business. It shows
|
||||||
shows at a glance all you need to know about your business. It
|
at a glance all you need to know about your business. It is
|
||||||
is fully customizable and extendable.
|
fully customizable and extendable.
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RightFeatureContainer>
|
</RightFeatureContainer>
|
||||||
</FeatureShowcaseContainer>
|
</FeatureShowcaseContainer>
|
||||||
@@ -247,14 +247,18 @@ function FeatureShowcaseContainer(props: React.PropsWithChildren) {
|
|||||||
|
|
||||||
function LeftFeatureContainer(props: React.PropsWithChildren) {
|
function LeftFeatureContainer(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-col space-y-8 lg:w-6/12'}>
|
<div className={'flex w-full flex-col space-y-6 lg:w-6/12'}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RightFeatureContainer(props: React.PropsWithChildren) {
|
function RightFeatureContainer(props: React.PropsWithChildren) {
|
||||||
return <div className={'flex w-full lg:w-6/12'}>{props.children}</div>;
|
return (
|
||||||
|
<div className={'flex w-full flex-col space-y-6 lg:w-6/12'}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainCallToActionButton() {
|
function MainCallToActionButton() {
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ async function PricingPage() {
|
|||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto mt-8 flex flex-col space-y-24'}>
|
<div className={'flex flex-col space-y-12'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('marketing:pricing')}
|
title={t('marketing:pricing')}
|
||||||
subtitle={t('marketing:pricingSubtitle')}
|
subtitle={t('marketing:pricingSubtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PricingTable paths={pathsConfig.auth} config={billingConfig} />
|
<div className={'container mx-auto pb-8 xl:pb-16'}>
|
||||||
|
<PricingTable paths={pathsConfig.auth} config={billingConfig} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const LogoImage: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={width}
|
width={width}
|
||||||
className={cn(`w-[95px] sm:w-[105px]`, className)}
|
className={cn(`w-[95px]`, className)}
|
||||||
viewBox="0 0 733 140"
|
viewBox="0 0 733 140"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"faqSubtitle": "Frequently asked questions about the platform",
|
"faqSubtitle": "Frequently asked questions about the platform",
|
||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"pricingSubtitle": "Pricing plans and payment options",
|
"pricingSubtitle": "Pricing plans and payment options",
|
||||||
|
"backToBlog": "Back to blog",
|
||||||
|
"contactFaq": "If you have any questions, please contact us",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"product": "Product",
|
"product": "Product",
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ export function PricingTable({
|
|||||||
' justify-center lg:flex-row'
|
' justify-center lg:flex-row'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{config.products.map((product, index) => {
|
{config.products.map((product) => {
|
||||||
const isFirst = index === 0;
|
|
||||||
const isLast = index === config.products.length - 1;
|
|
||||||
|
|
||||||
const plan = product.plans.find((plan) => {
|
const plan = product.plans.find((plan) => {
|
||||||
if (plan.paymentType === 'recurring') {
|
if (plan.paymentType === 'recurring') {
|
||||||
return plan.interval === interval;
|
return plan.interval === interval;
|
||||||
@@ -87,10 +84,7 @@ export function PricingTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PricingItem
|
<PricingItem
|
||||||
className={cn('border-b border-r border-t', {
|
className={cn('border')}
|
||||||
['rounded-l-lg border-l']: isFirst,
|
|
||||||
['rounded-r-lg']: isLast,
|
|
||||||
})}
|
|
||||||
selectable
|
selectable
|
||||||
key={plan.id}
|
key={plan.id}
|
||||||
plan={plan}
|
plan={plan}
|
||||||
@@ -160,7 +154,7 @@ function PricingItem(
|
|||||||
`s-full flex flex-1 grow flex-col items-stretch
|
`s-full flex flex-1 grow flex-col items-stretch
|
||||||
justify-between space-y-8 self-stretch p-6 lg:w-4/12 xl:max-w-[22rem] xl:p-8`,
|
justify-between space-y-8 self-stretch p-6 lg:w-4/12 xl:max-w-[22rem] xl:p-8`,
|
||||||
{
|
{
|
||||||
['bg-primary text-primary-foreground border-primary']: highlighted,
|
['border-primary']: highlighted,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -178,7 +172,7 @@ function PricingItem(
|
|||||||
|
|
||||||
<If condition={props.product.badge}>
|
<If condition={props.product.badge}>
|
||||||
<Badge
|
<Badge
|
||||||
variant={'default'}
|
variant={highlighted ? 'default' : 'outline'}
|
||||||
className={cn({
|
className={cn({
|
||||||
['border-primary-foreground']: highlighted,
|
['border-primary-foreground']: highlighted,
|
||||||
})}
|
})}
|
||||||
@@ -218,9 +212,6 @@ function PricingItem(
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
`animate-in slide-in-from-left-4 fade-in flex items-center space-x-0.5 capitalize`,
|
`animate-in slide-in-from-left-4 fade-in flex items-center space-x-0.5 capitalize`,
|
||||||
{
|
|
||||||
['text-muted-foreground']: !highlighted,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={'text-sm'}>
|
<span className={'text-sm'}>
|
||||||
@@ -240,9 +231,6 @@ function PricingItem(
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
`animate-in slide-in-from-left-4 fade-in text-sm capitalize`,
|
`animate-in slide-in-from-left-4 fade-in text-sm capitalize`,
|
||||||
{
|
|
||||||
['text-muted-foreground']: !highlighted,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<If condition={props.primaryLineItem.type === 'per-seat'}>
|
<If condition={props.primaryLineItem.type === 'per-seat'}>
|
||||||
@@ -319,7 +307,7 @@ function FeaturesList(
|
|||||||
<ul className={'flex flex-col space-y-2'}>
|
<ul className={'flex flex-col space-y-2'}>
|
||||||
{props.features.map((feature) => {
|
{props.features.map((feature) => {
|
||||||
return (
|
return (
|
||||||
<ListItem highlighted={props.highlighted} key={feature}>
|
<ListItem key={feature}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={`common:plans.features.${feature}`}
|
i18nKey={`common:plans.features.${feature}`}
|
||||||
defaults={feature}
|
defaults={feature}
|
||||||
@@ -347,25 +335,18 @@ function Price({ children }: React.PropsWithChildren) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListItem({
|
function ListItem({ children }: React.PropsWithChildren) {
|
||||||
children,
|
|
||||||
highlighted,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
highlighted?: boolean;
|
|
||||||
}>) {
|
|
||||||
return (
|
return (
|
||||||
<li className={'flex items-center space-x-1.5'}>
|
<li className={'flex items-center space-x-1.5'}>
|
||||||
<CheckCircle
|
<CheckCircle
|
||||||
className={cn('h-4', {
|
className={cn('h-4', {
|
||||||
['text-primary-foreground']: highlighted,
|
['text-green-600']: true,
|
||||||
['text-green-600']: !highlighted,
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn('text-sm', {
|
className={cn('text-sm', {
|
||||||
['text-secondary-foreground']: !highlighted,
|
['text-secondary-foreground']: true,
|
||||||
['text-primary-foreground']: highlighted,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -391,7 +372,7 @@ function PlanIntervalSwitcher(
|
|||||||
{
|
{
|
||||||
'rounded-r-none border-r-transparent': index === 0,
|
'rounded-r-none border-r-transparent': index === 0,
|
||||||
'rounded-l-none': index === props.intervals.length - 1,
|
'rounded-l-none': index === props.intervals.length - 1,
|
||||||
['hover:text-current hover:bg-muted']: !selected,
|
['hover:text-primary border']: !selected,
|
||||||
['font-semibold cursor-default hover:text-initial hover:bg-background border-primary']:
|
['font-semibold cursor-default hover:text-initial hover:bg-background border-primary']:
|
||||||
selected,
|
selected,
|
||||||
},
|
},
|
||||||
@@ -445,7 +426,7 @@ function DefaultCheckoutButton(
|
|||||||
<Button
|
<Button
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
variant={props.highlighted ? 'secondary' : 'outline'}
|
variant={props.highlighted ? 'default' : 'outline'}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={label} defaults={label} />
|
<Trans i18nKey={label} defaults={label} />
|
||||||
|
|||||||
Reference in New Issue
Block a user