This commit is contained in:
giancarlo
2024-03-24 02:23:22 +08:00
parent 648d77b430
commit bce3479368
589 changed files with 37067 additions and 9596 deletions

View File

@@ -0,0 +1,93 @@
import { Heading } from '@kit/ui/heading';
export const metadata = {
title: 'About',
};
const AboutPage = () => {
return (
<div>
<div className={'container mx-auto'}>
<div className={'my-8 flex flex-col space-y-14'}>
<div className={'flex flex-col items-center space-y-4'}>
<Heading level={1}>About us</Heading>
<Heading level={2}>
We are a team of passionate developers and designers who love to
build great products.
</Heading>
</div>
<div
className={
'm-auto flex w-full max-w-xl flex-col items-center space-y-8' +
' justify-center text-gray-600 dark:text-gray-400'
}
>
<div>
We are a team of visionaries, dreamers, and doers who are on a
mission to change the world for the better
</div>
<div>
With a passion for innovation and a commitment to excellence, we
are dedicated to creating products and services that will improve
people&apos;s lives and make a positive impact on society.
</div>
<div>
It all started with a simple idea: to use technology to solve some
of the biggest challenges facing humanity. We realized that with
the right team and the right approach, we could make a difference
and leave a lasting legacy. And so, with a lot of hard work and
determination, we set out on a journey to turn our vision into
reality.
</div>
<div>
Today, we are proud to be a leader in our field, and our products
and services are used by millions of people all over the world.
But we&apos;re not done yet. We still have big dreams and even
bigger plans, and we&apos;re always looking for ways to push the
boundaries of what&apos;s possible.
</div>
<div>
Our Values: At the heart of everything we do is a set of core
values that guide us in all that we do. These values are what make
us who we are, and they are what set us apart from the rest.
</div>
<div>
<ul className={'flex list-decimal flex-col space-y-1 pl-4'}>
<li>
Innovation: We are always looking for new and better ways to
do things.
</li>
<li>
Excellence: We strive for excellence in all that we do, and we
never settle for less.
</li>
<li>
Responsibility: We take our responsibilities seriously, and we
always act with integrity.
</li>
<li>
Collaboration: We believe that by working together, we can
achieve more than we can on our own.
</li>
</ul>
</div>
<div>Yes, this was generated with ChatGPT</div>
</div>
</div>
</div>
</div>
);
};
export default AboutPage;

View File

@@ -0,0 +1,70 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Script from 'next/script';
import { allPosts } from 'contentlayer/generated';
import appConfig from '~/config/app.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import Post from '../components/post';
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata | undefined> {
const post = allPosts.find((post) => post.slug === params.slug);
if (!post) {
return;
}
const { title, date, description, image, slug } = post;
const url = [appConfig.url, 'blog', slug].join('/');
return {
title,
description,
openGraph: {
title,
description,
type: 'article',
publishedTime: date,
url,
images: image
? [
{
url: image,
},
]
: [],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: image ? [image] : [],
},
};
}
async function BlogPost({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post.slug === params.slug);
if (!post) {
notFound();
}
return (
<div className={'container mx-auto'}>
<Script id={'ld-json'} type="application/ld+json">
{JSON.stringify(post.structuredData)}
</Script>
<Post post={post} content={post.body.code} />
</div>
);
}
export default withI18n(BlogPost);

View File

@@ -0,0 +1,33 @@
import Image from 'next/image';
import { cn } from '@kit/ui/utils';
type Props = {
title: string;
src: string;
preloadImage?: boolean;
className?: string;
};
export const CoverImage: React.FC<Props> = ({
title,
src,
preloadImage,
className,
}) => {
return (
<Image
className={cn(
'duration-250 block rounded-xl object-cover' +
' transition-all hover:opacity-90',
{
className,
},
)}
src={src}
priority={preloadImage}
alt={`Cover Image for ${title}`}
fill
/>
);
};

View File

@@ -0,0 +1,11 @@
import { format, parseISO } from 'date-fns';
type Props = {
dateString: string;
};
export const DateFormatter = ({ dateString }: Props) => {
const date = parseISO(dateString);
return <time dateTime={dateString}>{format(date, 'PP')}</time>;
};

View File

@@ -0,0 +1,9 @@
import React from 'react';
export function DraftPostBadge({ children }: React.PropsWithChildren) {
return (
<span className="dark:text-dark-800 rounded-md bg-yellow-200 px-4 py-2 font-semibold">
{children}
</span>
);
}

View File

@@ -0,0 +1,56 @@
import type { Post } from 'contentlayer/generated';
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
const PostHeader: React.FC<{
post: Post;
}> = ({ post }) => {
const { title, date, readingTime, description, image } = post;
// NB: change this to display the post's image
const displayImage = true;
const preloadImage = true;
return (
<div className={'flex flex-col space-y-4'}>
<div className={'flex flex-col space-y-4'}>
<Heading level={1}>{title}</Heading>
<Heading level={3}>
<span className={'font-normal text-muted-foreground'}>
{description}
</span>
</Heading>
</div>
<div className="flex">
<div className="flex flex-row items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
<div>
<DateFormatter dateString={date} />
</div>
<span>·</span>
<span>{readingTime} minutes reading</span>
</div>
</div>
<If condition={displayImage && image}>
{(imageUrl) => (
<div className="relative mx-auto h-[378px] w-full justify-center">
<CoverImage
preloadImage={preloadImage}
className="rounded-md"
title={title}
src={imageUrl}
/>
</div>
)}
</If>
</div>
);
};
export default PostHeader;

View File

@@ -0,0 +1,70 @@
import Link from 'next/link';
import type { Post } from 'contentlayer/generated';
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
import { If } from '@kit/ui/if';
type Props = {
post: Post;
preloadImage?: boolean;
imageHeight?: string | number;
};
const DEFAULT_IMAGE_HEIGHT = 250;
function PostPreview({
post,
preloadImage,
imageHeight,
}: React.PropsWithChildren<Props>) {
const { title, image, date, readingTime, description } = post;
const height = imageHeight ?? DEFAULT_IMAGE_HEIGHT;
return (
<div className="rounded-xl transition-shadow duration-500 dark:text-gray-800">
<If condition={image}>
{(imageUrl) => (
<div className="relative mb-2 w-full" style={{ height }}>
<Link href={post.url}>
<CoverImage
preloadImage={preloadImage}
title={title}
src={imageUrl}
/>
</Link>
</div>
)}
</If>
<div className={'px-1'}>
<div className="flex flex-col space-y-1 px-1 py-2">
<h3 className="px-1 text-2xl font-bold leading-snug dark:text-white">
<Link href={post.url} className="hover:underline">
{title}
</Link>
</h3>
</div>
<div className="mb-2 flex flex-row items-center space-x-2 px-1 text-sm">
<div className="text-gray-600 dark:text-gray-300">
<DateFormatter dateString={date} />
</div>
<span className="text-gray-600 dark:text-gray-300">·</span>
<span className="text-gray-600 dark:text-gray-300">
{readingTime} mins reading
</span>
</div>
<p className="mb-4 px-1 text-sm leading-relaxed dark:text-gray-300">
{description}
</p>
</div>
</div>
);
}
export default PostPreview;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import type { Post as PostType } from 'contentlayer/generated';
import { Mdx } from '@kit/ui/mdx';
import PostHeader from './post-header';
export const Post: React.FC<{
post: PostType;
content: string;
}> = ({ post, content }) => {
return (
<div className={'mx-auto my-8 max-w-2xl'}>
<PostHeader post={post} />
<article className={'mx-auto flex justify-center'}>
<Mdx code={content} />
</article>
</div>
);
};
export default Post;

View File

@@ -0,0 +1,41 @@
import type { Metadata } from 'next';
import { allPosts } from 'contentlayer/generated';
import PostPreview from '~/(marketing)/blog/components/post-preview';
import { SitePageHeader } from '~/(marketing)/components/site-page-header';
import appConfig from '~/config/app.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import { GridList } from '../components/grid-list';
export const metadata: Metadata = {
title: `Blog - ${appConfig.name}`,
description: `Tutorials, Guides and Updates from our team`,
};
async function BlogPage() {
const livePosts = allPosts.filter((post) => {
const isProduction = appConfig.production;
return isProduction ? post.live : true;
});
return (
<div className={'container mx-auto'}>
<div className={'my-8 flex flex-col space-y-16'}>
<SitePageHeader
title={'Blog'}
subtitle={'Tutorials, Guides and Updates from our team'}
/>
<GridList>
{livePosts.map((post, idx) => {
return <PostPreview key={idx} post={post} />;
})}
</GridList>
</div>
</div>
);
}
export default withI18n(BlogPage);

View File

@@ -0,0 +1,7 @@
export function GridList({ children }: React.PropsWithChildren) {
return (
<div className="mb-16 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">
{children}
</div>
);
}

View File

@@ -0,0 +1,132 @@
import Link from 'next/link';
import { AppLogo } from '~/components/app-logo';
import appConfig from '~/config/app.config';
const YEAR = new Date().getFullYear();
export function SiteFooter() {
return (
<footer className={'py-8 lg:py-24'}>
<div className={'container mx-auto'}>
<div className={'flex flex-col space-y-8 lg:flex-row lg:space-y-0'}>
<div
className={
'flex w-full space-x-2 lg:w-4/12 xl:w-3/12' +
' xl:space-x-6 2xl:space-x-8'
}
>
<div className={'flex flex-col space-y-4'}>
<div>
<AppLogo className={'w-[85px] md:w-[115px]'} />
</div>
<div>
<p className={'text-sm text-gray-500 dark:text-gray-400'}>
Add a short tagline about your product
</p>
</div>
<div className={'flex text-xs text-gray-500 dark:text-gray-400'}>
<p>
© Copyright {YEAR} {appConfig.name}. All Rights Reserved.
</p>
</div>
</div>
</div>
<div
className={
'flex flex-col space-y-8 lg:space-x-6 lg:space-y-0' +
' xl:space-x-16 2xl:space-x-20' +
' w-full lg:flex-row lg:justify-end'
}
>
<div>
<div className={'flex flex-col space-y-4'}>
<FooterSectionHeading>About</FooterSectionHeading>
<FooterSectionList>
<FooterLink>
<Link href={'#'}>Who we are</Link>
</FooterLink>
<FooterLink>
<Link href={'/blog'}>Blog</Link>
</FooterLink>
<FooterLink>
<Link href={'/contact'}>Contact</Link>
</FooterLink>
</FooterSectionList>
</div>
</div>
<div>
<div className={'flex flex-col space-y-4'}>
<FooterSectionHeading>Product</FooterSectionHeading>
<FooterSectionList>
<FooterLink>
<Link href={'/docs'}>Documentation</Link>
</FooterLink>
<FooterLink>
<Link href={'#'}>Help Center</Link>
</FooterLink>
<FooterLink>
<Link href={'#'}>Changelog</Link>
</FooterLink>
</FooterSectionList>
</div>
</div>
<div>
<div className={'flex flex-col space-y-4'}>
<FooterSectionHeading>Legal</FooterSectionHeading>
<FooterSectionList>
<FooterLink>
<Link href={'#'}>Terms of Service</Link>
</FooterLink>
<FooterLink>
<Link href={'#'}>Privacy Policy</Link>
</FooterLink>
<FooterLink>
<Link href={'#'}>Cookie Policy</Link>
</FooterLink>
</FooterSectionList>
</div>
</div>
</div>
</div>
</div>
</footer>
);
}
function FooterSectionHeading(props: React.PropsWithChildren) {
return (
<p>
<span className={'font-semibold'}>{props.children}</span>
</p>
);
}
function FooterSectionList(props: React.PropsWithChildren) {
return (
<ul className={'flex flex-col space-y-4 text-gray-500 dark:text-gray-400'}>
{props.children}
</ul>
);
}
function FooterLink(props: React.PropsWithChildren) {
return (
<li
className={
'text-sm [&>a]:transition-colors [&>a]:hover:text-gray-800' +
' dark:[&>a]:hover:text-white'
}
>
{props.children}
</li>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import Link from 'next/link';
import { ChevronRightIcon } from 'lucide-react';
import pathsConfig from '~/config/paths.config';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useUserSession } from '@kit/supabase/hooks/use-user-session';
import { Button } from '@kit/ui/button';
export function SiteHeaderAccountSection() {
const signOut = useSignOut();
const userSession = useUserSession();
if (userSession.data) {
return (
<PersonalAccountDropdown
session={userSession.data}
paths={{
home: pathsConfig.app.home,
}}
signOutRequested={() => signOut.mutateAsync()}
/>
);
}
return <AuthButtons />;
}
function AuthButtons() {
return (
<div className={'hidden space-x-2 lg:flex'}>
<Button variant={'link'}>
<Link href={pathsConfig.auth.signIn}>Sign In</Link>
</Button>
<Link href={pathsConfig.auth.signUp}>
<Button className={'rounded-full'}>
Sign Up
<ChevronRightIcon className={'h-4'} />
</Button>
</Link>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { SiteHeaderAccountSection } from '~/(marketing)/components/site-header-account-section';
import { SiteNavigation } from '~/(marketing)/components/site-navigation';
import { AppLogo } from '~/components/app-logo';
export function SiteHeader() {
return (
<div className={'container mx-auto'}>
<div className="flex h-16 items-center justify-between">
<div className={'w-4/12'}>
<AppLogo />
</div>
<div className={'hidden w-4/12 justify-center lg:flex'}>
<SiteNavigation />
</div>
<div className={'flex flex-1 items-center justify-end space-x-4'}>
<div className={'flex items-center'}></div>
<SiteHeaderAccountSection />
<div className={'flex lg:hidden'}>
<SiteNavigation />
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
import Link from 'next/link';
import { MenuIcon } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
} from '@kit/ui/navigation-menu';
const links = {
SignIn: {
label: 'Sign In',
path: '/auth/sign-in',
},
Blog: {
label: 'Blog',
path: '/blog',
},
Docs: {
label: 'Documentation',
path: '/docs',
},
Pricing: {
label: 'Pricing',
path: '/pricing',
},
FAQ: {
label: 'FAQ',
path: '/faq',
},
};
export function SiteNavigation() {
const className = `hover:underline text-sm`;
return (
<>
<div className={'hidden items-center lg:flex'}>
<NavigationMenu>
<NavigationMenuList className={'space-x-2.5'}>
<NavigationMenuItem>
<Link className={className} href={links.Blog.path}>
{links.Blog.label}
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<Link className={className} href={links.Docs.path}>
{links.Docs.label}
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<Link className={className} href={links.Pricing.path}>
{links.Pricing.label}
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<Link className={className} href={links.FAQ.path}>
{links.FAQ.label}
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
<div className={'flex items-center lg:hidden'}>
<MobileDropdown />
</div>
</>
);
}
function MobileDropdown() {
return (
<DropdownMenu>
<DropdownMenuTrigger aria-label={'Open Menu'}>
<MenuIcon className={'h-9'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.values(links).map((item) => {
const className = 'flex w-full h-full items-center';
return (
<DropdownMenuItem key={item.path}>
<Link className={className} href={item.path}>
{item.label}
</Link>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,20 @@
import { Heading } from '@kit/ui/heading';
import { cn } from '@kit/ui/utils';
export function SitePageHeader(props: {
title: string;
subtitle: string;
className?: string;
}) {
return (
<div
className={cn('flex flex-col items-center space-y-2.5', props.className)}
>
<Heading level={1}>{props.title}</Heading>
<Heading level={2} className={'text-muted-foreground'}>
<span className={'font-normal'}>{props.subtitle}</span>
</Heading>
</div>
);
}

View File

@@ -0,0 +1,101 @@
import { cache } from 'react';
import { notFound } from 'next/navigation';
import { allDocumentationPages } from 'contentlayer/generated';
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import { SitePageHeader } from '~/(marketing)/components/site-page-header';
import { DocsCards } from '~/(marketing)/docs/components/docs-cards';
import { DocumentationPageLink } from '~/(marketing)/docs/components/documentation-page-link';
import { getDocumentationPageTree } from '~/(marketing)/docs/utils/get-documentation-page-tree';
import { withI18n } from '~/lib/i18n/with-i18n';
import { If } from '@kit/ui/if';
import { Mdx } from '@kit/ui/mdx';
const getPageBySlug = cache((slug: string) => {
return allDocumentationPages.find((post) => post.resolvedPath === slug);
});
interface PageParams {
params: {
slug: string[];
};
}
export const generateMetadata = ({ params }: PageParams) => {
const page = getPageBySlug(params.slug.join('/'));
if (!page) {
notFound();
}
const { title, description } = page;
return {
title,
description,
};
};
function DocumentationPage({ params }: PageParams) {
const page = getPageBySlug(params.slug.join('/'));
if (!page) {
notFound();
}
const { nextPage, previousPage, children } =
getDocumentationPageTree(page.resolvedPath) ?? {};
const description = page?.description ?? '';
return (
<div className={'container mx-auto'}>
<div className={'relative flex grow flex-col space-y-4 px-8 py-8'}>
<SitePageHeader
title={page.title}
subtitle={description}
className={'items-start'}
/>
<Mdx code={page.body.code} />
<If condition={children}>
<DocsCards pages={children ?? []} />
</If>
<div
className={
'flex flex-col justify-between space-y-4 md:flex-row md:space-x-8' +
' md:space-y-0'
}
>
<div className={'w-full'}>
<If condition={previousPage}>
{(page) => (
<DocumentationPageLink
page={page}
before={<ChevronLeftIcon className={'w-4'} />}
/>
)}
</If>
</div>
<div className={'w-full'}>
<If condition={nextPage}>
{(page) => (
<DocumentationPageLink
page={page}
after={<ChevronRightIcon className={'w-4'} />}
/>
)}
</If>
</div>
</div>
</div>
</div>
);
}
export default withI18n(DocumentationPage);

View File

@@ -0,0 +1,45 @@
import Link from 'next/link';
import { ChevronRightIcon } from 'lucide-react';
export const DocsCard: React.FC<
React.PropsWithChildren<{
label: string;
subtitle?: string | null;
link?: { url: string; label: string };
}>
> = ({ label, subtitle, children, link }) => {
return (
<div className="flex flex-col">
<div
className={`flex grow flex-col space-y-2.5 border bg-background p-6
${link ? 'rounded-t-2xl border-b-0' : 'rounded-2xl'}`}
>
<h3 className="mt-0 text-lg font-semibold dark:text-white">{label}</h3>
{subtitle && (
<div className="text-sm text-gray-500 dark:text-gray-400">
<p>{subtitle}</p>
</div>
)}
{children && <div className="text-sm">{children}</div>}
</div>
{link && (
<div className="rounded-b-2xl border bg-muted p-6 py-4 dark:bg-background">
<span className={'flex items-center space-x-2'}>
<Link
className={'text-sm font-medium hover:underline'}
href={`/docs/${link.url}`}
>
{link.label}
</Link>
<ChevronRightIcon className={'h-4'} />
</span>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,23 @@
import type { DocumentationPage } from 'contentlayer/generated';
import { DocsCard } from './docs-card';
export function DocsCards({ pages }: { pages: DocumentationPage[] }) {
return (
<div className={'grid grid-cols-1 gap-8 lg:grid-cols-2'}>
{pages.map((item) => {
return (
<DocsCard
key={item.label}
label={item.label}
subtitle={item.description}
link={{
url: item.resolvedPath,
label: item.cardCTA ?? 'Read more',
}}
/>
);
})}
</div>
);
}

View File

@@ -0,0 +1,225 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ChevronDownIcon, MenuIcon } from 'lucide-react';
import { isBrowser } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { cn } from '@kit/ui/utils';
import type { ProcessedDocumentationPage } from '../utils/build-documentation-tree';
const DocsNavLink: React.FC<{
label: string;
url: string;
level: number;
activePath: string;
collapsible: boolean;
collapsed: boolean;
toggleCollapsed: () => void;
}> = ({
label,
url,
level,
activePath,
collapsible,
collapsed,
toggleCollapsed,
}) => {
const isCurrent = url == activePath;
const isFirstLevel = level === 0;
return (
<div className={getNavLinkClassName(isCurrent, isFirstLevel)}>
<Link
className="flex h-full max-w-full grow items-center space-x-2"
href={`/docs/${url}`}
>
<span className="block max-w-full truncate">{label}</span>
</Link>
{collapsible && (
<button
aria-label="Toggle children"
onClick={toggleCollapsed}
className="mr-2 shrink-0 px-2 py-1"
>
<span
className={`block w-2.5 ${collapsed ? '-rotate-90 transform' : ''}`}
>
<ChevronDownIcon className="h-4 w-4" />
</span>
</button>
)}
</div>
);
};
const Node: React.FC<{
node: ProcessedDocumentationPage;
level: number;
activePath: string;
}> = ({ node, level, activePath }) => {
const [collapsed, setCollapsed] = useState<boolean>(node.collapsed ?? false);
const toggleCollapsed = () => setCollapsed(!collapsed);
useEffect(() => {
if (
activePath == node.resolvedPath ||
node.children.map((_) => _.resolvedPath).includes(activePath)
) {
setCollapsed(false);
}
}, [activePath, node.children, node.resolvedPath]);
return (
<>
<DocsNavLink
label={node.label}
url={node.resolvedPath}
level={level}
activePath={activePath}
collapsible={node.collapsible}
collapsed={collapsed}
toggleCollapsed={toggleCollapsed}
/>
{node.children.length > 0 && !collapsed && (
<Tree tree={node.children} level={level + 1} activePath={activePath} />
)}
</>
);
};
function Tree({
tree,
level,
activePath,
}: {
tree: ProcessedDocumentationPage[];
level: number;
activePath: string;
}) {
return (
<div className={cn('w-full space-y-2.5 pl-3', level > 0 ? 'border-l' : '')}>
{tree.map((treeNode, index) => (
<Node
key={index}
node={treeNode}
level={level}
activePath={activePath}
/>
))}
</div>
);
}
export default function DocsNavigation({
tree,
}: {
tree: ProcessedDocumentationPage[];
}) {
const activePath = usePathname().replace('/docs/', '');
return (
<>
<aside
style={{
height: `calc(100vh - 64px)`,
}}
className="sticky top-2 hidden w-80 shrink-0 border-r p-4 lg:flex"
>
<Tree tree={tree} level={0} activePath={activePath} />
</aside>
<div className={'lg:hidden'}>
<FloatingDocumentationNavigation tree={tree} activePath={activePath} />
</div>
</>
);
}
function getNavLinkClassName(isCurrent: boolean, isFirstLevel: boolean) {
return cn(
'group flex h-8 items-center justify-between space-x-2 whitespace-nowrap rounded-md px-3 text-sm leading-none transition-colors',
{
[`bg-muted`]: isCurrent,
[`hover:bg-muted`]: !isCurrent,
[`font-semibold`]: isFirstLevel,
[`font-normal`]: !isFirstLevel && isCurrent,
[`hover:text-foreground-muted`]: !isFirstLevel && !isCurrent,
},
);
}
function FloatingDocumentationNavigation({
tree,
activePath,
}: React.PropsWithChildren<{
tree: ProcessedDocumentationPage[];
activePath: string;
}>) {
const body = useMemo(() => {
return isBrowser() ? document.body : null;
}, []);
const [isVisible, setIsVisible] = useState(false);
const enableScrolling = (element: HTMLElement) =>
(element.style.overflowY = '');
const disableScrolling = (element: HTMLElement) =>
(element.style.overflowY = 'hidden');
// enable/disable body scrolling when the docs are toggled
useEffect(() => {
if (!body) {
return;
}
if (isVisible) {
disableScrolling(body);
} else {
enableScrolling(body);
}
}, [isVisible, body]);
// hide docs when navigating to another page
useEffect(() => {
setIsVisible(false);
}, [activePath]);
const onClick = () => {
setIsVisible(!isVisible);
};
return (
<>
<If condition={isVisible}>
<div
className={
'fixed left-0 top-0 z-10 h-screen w-full p-4' +
' flex flex-col space-y-4 overflow-auto bg-white dark:bg-background'
}
>
<Heading level={1}>Table of Contents</Heading>
<Tree tree={tree} level={0} activePath={activePath} />
</div>
</If>
<Button
className={'fixed bottom-5 right-5 z-10 h-16 w-16 rounded-full'}
onClick={onClick}
>
<MenuIcon className={'h-8'} />
</Button>
</>
);
}

View File

@@ -0,0 +1,47 @@
import Link from 'next/link';
import type { DocumentationPage } from 'contentlayer/generated';
import { If } from '@kit/ui/if';
import { cn } from '@kit/ui/utils';
export function DocumentationPageLink({
page,
before,
after,
}: React.PropsWithChildren<{
page: DocumentationPage;
before?: React.ReactNode;
after?: React.ReactNode;
}>) {
return (
<Link
className={cn(
`flex w-full items-center space-x-8 rounded-xl p-6 font-medium text-current ring-2 ring-muted transition-all hover:ring-primary`,
{
'justify-start': before,
'justify-end self-end': after,
},
)}
href={`/docs/${page.resolvedPath}`}
>
<If condition={before}>{(node) => <>{node}</>}</If>
<span className={'flex flex-col space-y-1.5'}>
<span
className={
'text-xs font-semibold uppercase dark:text-gray-400' +
' text-gray-500'
}
>
{before ? `Previous` : ``}
{after ? `Next` : ``}
</span>
<span>{page.title}</span>
</span>
<If condition={after}>{(node) => <>{node}</>}</If>
</Link>
);
}

View File

@@ -0,0 +1,21 @@
import type { DocumentationPage } from 'contentlayer/generated';
import { allDocumentationPages } from 'contentlayer/generated';
import DocsNavigation from './components/docs-navigation';
import { buildDocumentationTree } from './utils/build-documentation-tree';
function DocsLayout({ children }: React.PropsWithChildren) {
const tree = buildDocumentationTree(allDocumentationPages);
return (
<div className={'container mx-auto'}>
<div className={'flex'}>
<DocsNavigation tree={tree} />
<div className={'flex w-full flex-col items-center'}>{children}</div>
</div>
</div>
);
}
export default DocsLayout;

View File

@@ -0,0 +1,30 @@
import { allDocumentationPages } from 'contentlayer/generated';
import appConfig from '~/config/app.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../components/site-page-header';
import { DocsCards } from './components/docs-cards';
import { buildDocumentationTree } from './utils/build-documentation-tree';
export const metadata = {
title: `Documentation - ${appConfig.name}`,
};
function DocsPage() {
const tree = buildDocumentationTree(allDocumentationPages);
return (
<div className={'my-8 flex flex-col space-y-16'}>
<SitePageHeader
title={'Documentation'}
subtitle={'Get started with our guides and tutorials'}
/>
<div>
<DocsCards pages={tree ?? []} />
</div>
</div>
);
}
export default withI18n(DocsPage);

View File

@@ -0,0 +1,53 @@
import { cache } from 'react';
import type { DocumentationPage } from 'contentlayer/generated';
export interface ProcessedDocumentationPage extends DocumentationPage {
collapsible: boolean;
pathSegments: string[];
nextPage: ProcessedDocumentationPage | DocumentationPage | null;
previousPage: ProcessedDocumentationPage | DocumentationPage | null;
children: DocsTree;
}
export type DocsTree = ProcessedDocumentationPage[];
/**
* Build a tree of documentation pages from a flat list of pages with path segments
* @param docs
* @param parentPathNames
*/
export const buildDocumentationTree = cache(
(docs: DocumentationPage[], parentPathNames: string[] = []): DocsTree => {
const level = parentPathNames.length;
const pages = docs
.filter(
(_) =>
_.pathSegments.length === level + 1 &&
_.pathSegments
.map(({ pathName }: { pathName: string }) => pathName)
.join('/')
.startsWith(parentPathNames.join('/')),
)
.sort(
(a, b) => a.pathSegments[level].order - b.pathSegments[level].order,
);
return pages.map((doc, index) => {
const children = buildDocumentationTree(
docs,
doc.pathSegments.map(({ pathName }: { pathName: string }) => pathName),
);
return {
...doc,
pathSegments: doc.pathSegments || ([] as string[]),
collapsible: children.length > 0,
nextPage: children[0] || pages[index + 1],
previousPage: pages[index - 1],
children,
};
});
},
);

View File

@@ -0,0 +1,45 @@
import { cache } from 'react';
import type { DocumentationPage } from 'contentlayer/generated';
import { allDocumentationPages } from 'contentlayer/generated';
import { buildDocumentationTree } from './build-documentation-tree';
/**
* Retrieves a specific documentation page from the page tree by its path.
*
* @param {string} pagePath - The path of the documentation page to retrieve.
* @returns {DocumentationPageWithChildren | undefined} The documentation page found in the tree, if any.
*/
export const getDocumentationPageTree = cache((pagePath: string) => {
const tree = buildDocumentationTree(allDocumentationPages);
type DocumentationPageWithChildren = DocumentationPage & {
previousPage?: DocumentationPage | null;
nextPage?: DocumentationPage | null;
children?: DocumentationPage[];
};
const findPageInTree = (
pages: DocumentationPageWithChildren[],
path: string,
): DocumentationPageWithChildren | undefined => {
for (const page of pages) {
if (page.resolvedPath === path) {
return page;
}
const hasChildren = page.children && page.children.length > 0;
if (hasChildren) {
const foundPage = findPageInTree(page.children ?? [], path);
if (foundPage) {
return foundPage;
}
}
}
};
return findPageInTree(tree, pagePath);
});

View File

@@ -0,0 +1,123 @@
import { ChevronDownIcon } from 'lucide-react';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../components/site-page-header';
export const metadata = {
title: 'FAQ',
};
const faqItems = [
{
question: `Do you offer a free trial?`,
answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`,
},
{
question: `Can I cancel my subscription?`,
answer: `You can cancel your subscription at any time. You can do this from your account settings.`,
},
{
question: `Where can I find my invoices?`,
answer: `You can find your invoices in your account settings.`,
},
{
question: `What payment methods do you accept?`,
answer: `We accept all major credit cards and PayPal.`,
},
{
question: `Can I upgrade or downgrade my plan?`,
answer: `Yes, you can upgrade or downgrade your plan at any time. You can do this from your account settings.`,
},
{
question: `Do you offer discounts for non-profits?`,
answer: `Yes, we offer a 50% discount for non-profits. Please contact us to learn more.`,
},
];
const FAQPage = () => {
const structuredData = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems.map((item) => {
return {
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
};
}),
};
return (
<div>
<script
key={'ld:json'}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
<div className={'container mx-auto'}>
<div className={'my-8 flex flex-col space-y-16'}>
<SitePageHeader
title={'FAQ'}
subtitle={'Frequently Asked Questions'}
/>
<div
className={
'm-auto flex w-full max-w-xl items-center justify-center'
}
>
<div className="flex w-full flex-col">
{faqItems.map((item, index) => {
return <FaqItem key={index} item={item} />;
})}
</div>
</div>
</div>
</div>
</div>
);
};
export default withI18n(FAQPage);
function FaqItem({
item,
}: React.PropsWithChildren<{
item: {
question: string;
answer: string;
};
}>) {
return (
<details className={'group border-b px-2 py-4'}>
<summary
className={
'flex items-center justify-between hover:cursor-pointer hover:underline'
}
>
<h2
className={
'hover:underline-none cursor-pointer font-sans text-lg font-medium'
}
>
{item.question}
</h2>
<div>
<ChevronDownIcon
className={'h-5 transition duration-300 group-open:-rotate-180'}
/>
</div>
</summary>
<div
className={'flex flex-col space-y-2 py-1 text-muted-foreground'}
dangerouslySetInnerHTML={{ __html: item.answer }}
/>
</details>
);
}

View File

@@ -0,0 +1,18 @@
import { withI18n } from '~/lib/i18n/with-i18n';
import { SiteFooter } from './components/site-footer';
import { SiteHeader } from './components/site-header';
function SiteLayout(props: React.PropsWithChildren) {
return (
<>
<SiteHeader />
{props.children}
<SiteFooter />
</>
);
}
export default withI18n(SiteLayout);

View File

@@ -0,0 +1,3 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

View File

@@ -0,0 +1,285 @@
import Image from 'next/image';
import Link from 'next/link';
import { ChevronRightIcon } from 'lucide-react';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
function Home() {
return (
<div className={'flex flex-col space-y-16'}>
<div className={'container mx-auto'}>
<div
className={
'my-12 flex flex-col items-center md:flex-row lg:my-16' +
' mx-auto flex-1 justify-center animate-in fade-in ' +
' duration-1000 slide-in-from-top-12'
}
>
<div className={'flex w-full flex-1 flex-col items-center space-y-8'}>
<Pill>
<span>The leading SaaS Starter Kit for ambitious developers</span>
</Pill>
<HeroTitle>
<span>The SaaS Solution for</span>
<span>developers and founders</span>
</HeroTitle>
<div>
<Heading
level={3}
className={'text-center font-medium text-muted-foreground'}
>
<span>Here you can write a short description of your SaaS</span>
</Heading>
<Heading
level={3}
className={'text-center font-medium text-muted-foreground'}
>
<span>
This subheading is usually laid out on multiple lines
</span>
</Heading>
<Heading
level={3}
className={'text-center font-medium text-muted-foreground'}
>
<span>Impress your customers, straight to the point.</span>
</Heading>
</div>
<div className={'flex flex-col items-center space-y-4'}>
<MainCallToActionButton />
<span className={'text-xs text-muted-foreground'}>
Free plan. No credit card required.
</span>
</div>
</div>
</div>
<div
className={
'mx-auto flex max-w-5xl justify-center py-12 animate-in fade-in ' +
' delay-300 duration-1000 slide-in-from-top-16 fill-mode-both'
}
>
<Image
priority
className={
'rounded-2xl' +
' shadow-primary/40 animate-in fade-in' +
' delay-300 duration-1000 ease-out zoom-in-50 fill-mode-both'
}
width={2688}
height={1824}
src={`/assets/images/dashboard-dark.webp`}
alt={`App Image`}
/>
</div>
</div>
<div className={'container mx-auto'}>
<div
className={
'flex flex-col items-center justify-center space-y-24 py-16'
}
>
<div
className={
'flex max-w-3xl flex-col items-center space-y-8 text-center'
}
>
<Pill>
<span>A modern, scalable, and secure SaaS Starter Kit</span>
</Pill>
<div className={'flex flex-col space-y-2.5'}>
<Heading level={2}>The best tool in the space</Heading>
<Heading level={3} className={'text-muted-foreground'}>
Unbeatable Features and Benefits for Your SaaS Business
</Heading>
</div>
</div>
</div>
</div>
<div className={'container mx-auto'}>
<div className={'flex flex-col space-y-4'}>
<FeatureShowcaseContainer>
<LeftFeatureContainer>
<div className={'flex flex-col space-y-4'}>
<Heading level={2}>Authentication</Heading>
<Heading level={3} className={'text-muted-foreground'}>
Secure and Easy-to-Use Authentication for Your SaaS Website
and API
</Heading>
</div>
<div>
Our authentication system is built on top of the
industry-leading PaaS such as Supabase and Firebase. It is
secure, easy-to-use, and fully customizable. It supports
email/password, social logins, and more.
</div>
<div>
<Button variant={'outline'}>
<span className={'flex items-center space-x-2'}>
<span>Get Started</span>
<ChevronRightIcon className={'h-3'} />
</span>
</Button>
</div>
</LeftFeatureContainer>
<RightFeatureContainer>
<Image
className="rounded-2xl"
src={'/assets/images/sign-in.webp'}
width={'626'}
height={'683'}
alt={'Sign In'}
/>
</RightFeatureContainer>
</FeatureShowcaseContainer>
<FeatureShowcaseContainer>
<LeftFeatureContainer>
<Image
className="rounded-2xl"
src={'/assets/images/dashboard.webp'}
width={'887'}
height={'743'}
alt={'Dashboard'}
/>
</LeftFeatureContainer>
<RightFeatureContainer>
<div className={'flex flex-col space-y-4'}>
<Heading level={1}>Dashboard</Heading>
<Heading level={2} className={'text-muted-foreground'}>
A fantastic dashboard to manage your SaaS business
</Heading>
<div>
Our dashboard offers an overview of your SaaS business. It
shows at a glance all you need to know about your business. It
is fully customizable and extendable.
</div>
</div>
</RightFeatureContainer>
</FeatureShowcaseContainer>
</div>
</div>
<div className={'container mx-auto'}>
<div
className={
'flex flex-col items-center justify-center space-y-16 py-16'
}
>
<div className={'flex flex-col items-center space-y-8 text-center'}>
<Pill>
Get started for free. No credit card required. Cancel anytime.
</Pill>
<div className={'flex flex-col space-y-1'}>
<Heading level={2}>
Ready to take your SaaS business to the next level?
</Heading>
<Heading level={3} className={'text-muted-foreground'}>
Get started on our free plan and upgrade when you are ready.
</Heading>
</div>
</div>
<div className={'w-full'}></div>
</div>
</div>
</div>
);
}
export default withI18n(Home);
function HeroTitle({ children }: React.PropsWithChildren) {
return (
<h1
className={
'text-center text-4xl md:text-5xl' +
' font-heading flex flex-col font-bold xl:text-7xl'
}
>
{children}
</h1>
);
}
function Pill(props: React.PropsWithChildren) {
return (
<h2
className={
'inline-flex w-auto items-center space-x-2' +
' rounded-full bg-gradient-to-br dark:from-gray-200 dark:via-gray-400' +
' bg-clip-text px-4 py-2 text-center text-sm dark:to-gray-700' +
' border font-normal text-muted-foreground shadow-sm dark:text-transparent'
}
>
{props.children}
</h2>
);
}
function FeatureShowcaseContainer(props: React.PropsWithChildren) {
return (
<div
className={
'flex flex-col items-center justify-between lg:flex-row' +
' lg:space-x-24'
}
>
{props.children}
</div>
);
}
function LeftFeatureContainer(props: React.PropsWithChildren) {
return (
<div className={'flex w-full flex-col space-y-8 lg:w-6/12'}>
{props.children}
</div>
);
}
function RightFeatureContainer(props: React.PropsWithChildren) {
return <div className={'flex w-full lg:w-6/12'}>{props.children}</div>;
}
function MainCallToActionButton() {
return (
<Button className={'rounded-full'}>
<Link href={'/auth/sign-up'}>
<span className={'flex items-center space-x-2'}>
<span>Get Started</span>
<ChevronRightIcon
className={
'h-4 animate-in fade-in slide-in-from-left-8' +
' delay-1000 duration-1000 zoom-in fill-mode-both'
}
/>
</span>
</Link>
</Button>
);
}

View File

@@ -0,0 +1,28 @@
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import { PricingTable } from '@kit/billing/components/pricing-table';
import { SitePageHeader } from '../components/site-page-header';
export const metadata = {
title: 'Pricing',
};
function PricingPage() {
return (
<div className={'container mx-auto'}>
<div className={'my-8 flex flex-col space-y-16'}>
<SitePageHeader
title={'Pricing'}
subtitle={'Our pricing is designed to scale with your business.'}
/>
</div>
<PricingTable paths={pathsConfig.auth} config={billingConfig} />
</div>
);
}
export default withI18n(PricingPage);