first
This commit is contained in:
commit
8538ab969a
434 changed files with 44051 additions and 0 deletions
25
apps/web/app/(marketing)/[locale]/(home)/page.tsx
Normal file
25
apps/web/app/(marketing)/[locale]/(home)/page.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { FaqSection } from "@marketing/home/components/FaqSection";
|
||||
import { Features } from "@marketing/home/components/Features";
|
||||
import { Hero } from "@marketing/home/components/Hero";
|
||||
import { Newsletter } from "@marketing/home/components/Newsletter";
|
||||
import { PricingSection } from "@marketing/home/components/PricingSection";
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
|
||||
export default async function Home({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Features />
|
||||
<PricingSection />
|
||||
<FaqSection />
|
||||
<Newsletter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
5
apps/web/app/(marketing)/[locale]/[...rest]/page.tsx
Normal file
5
apps/web/app/(marketing)/[locale]/[...rest]/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { notFound } from "next/navigation";
|
||||
|
||||
export default function CatchAll() {
|
||||
notFound();
|
||||
}
|
||||
128
apps/web/app/(marketing)/[locale]/blog/[...path]/page.tsx
Normal file
128
apps/web/app/(marketing)/[locale]/blog/[...path]/page.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { LocaleLink, localeRedirect } from "@i18n/routing";
|
||||
import { PostContent } from "@marketing/blog/components/PostContent";
|
||||
import { getPostBySlug } from "@marketing/blog/utils/lib/posts";
|
||||
import { getBaseUrl } from "@repo/utils";
|
||||
import { getActivePathFromUrlParam } from "@shared/lib/content";
|
||||
import { getLocale, getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Image from "next/image";
|
||||
|
||||
type Params = {
|
||||
path: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<Params> }) {
|
||||
const params = await props.params;
|
||||
|
||||
const { path } = params;
|
||||
|
||||
const locale = await getLocale();
|
||||
const slug = getActivePathFromUrlParam(path);
|
||||
const post = await getPostBySlug(slug, { locale });
|
||||
|
||||
return {
|
||||
title: post?.title,
|
||||
description: post?.excerpt,
|
||||
openGraph: {
|
||||
title: post?.title,
|
||||
description: post?.excerpt,
|
||||
images: post?.image
|
||||
? [
|
||||
post.image.startsWith("http")
|
||||
? post.image
|
||||
: new URL(post.image, getBaseUrl()).toString(),
|
||||
]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage(props: { params: Promise<Params> }) {
|
||||
const { path, locale } = await props.params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const slug = getActivePathFromUrlParam(path);
|
||||
const post = await getPostBySlug(slug, { locale });
|
||||
|
||||
if (!post) {
|
||||
return localeRedirect({ href: "/blog", locale });
|
||||
}
|
||||
|
||||
const { title, date, authorName, authorImage, tags, image, body } = post;
|
||||
|
||||
return (
|
||||
<div className="container max-w-6xl pt-32 pb-24">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="mb-12">
|
||||
<LocaleLink href="/blog">
|
||||
← {t("blog.back")}
|
||||
</LocaleLink>
|
||||
</div>
|
||||
|
||||
<h1 className="font-bold text-4xl">{title}</h1>
|
||||
|
||||
<div className="mt-4 flex items-center justify-start gap-6">
|
||||
{authorName && (
|
||||
<div className="flex items-center">
|
||||
{authorImage && (
|
||||
<div className="relative mr-2 size-8 overflow-hidden rounded-full">
|
||||
<Image
|
||||
src={authorImage}
|
||||
alt={authorName}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-semibold text-sm opacity-50">
|
||||
{authorName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mr-0 ml-auto">
|
||||
<p className="text-sm opacity-30">
|
||||
{Intl.DateTimeFormat("en-US").format(
|
||||
new Date(date),
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{tags && (
|
||||
<div className="flex flex-1 flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="font-semibold text-primary text-xs uppercase tracking-wider"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{image && (
|
||||
<div className="relative mt-6 aspect-16/9 overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pb-8">
|
||||
<PostContent content={body} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
apps/web/app/(marketing)/[locale]/blog/page.tsx
Normal file
39
apps/web/app/(marketing)/[locale]/blog/page.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { PostListItem } from "@marketing/blog/components/PostListItem";
|
||||
import { getAllPosts } from "@marketing/blog/utils/lib/posts";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: t("blog.title"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogListPage() {
|
||||
const locale = await getLocale();
|
||||
const t = await getTranslations();
|
||||
|
||||
const posts = await getAllPosts();
|
||||
|
||||
return (
|
||||
<div className="container max-w-6xl pt-32 pb-16">
|
||||
<div className="mb-12 pt-8 text-center">
|
||||
<h1 className="mb-2 font-bold text-5xl">{t("blog.title")}</h1>
|
||||
<p className="text-lg opacity-50">{t("blog.description")}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{posts
|
||||
.filter((post) => post.published && locale === post.locale)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.date).getTime() -
|
||||
new Date(a.date).getTime(),
|
||||
)
|
||||
.map((post) => (
|
||||
<PostListItem post={post} key={post.path} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
apps/web/app/(marketing)/[locale]/changelog/page.tsx
Normal file
35
apps/web/app/(marketing)/[locale]/changelog/page.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { ChangelogSection } from "@marketing/changelog/components/ChangelogSection";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function PricingPage() {
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<div className="container max-w-3xl pt-32 pb-16">
|
||||
<div className="mb-12 text-balance pt-8 text-center">
|
||||
<h1 className="mb-2 font-bold text-5xl">
|
||||
{t("changelog.title")}
|
||||
</h1>
|
||||
<p className="text-lg opacity-50">
|
||||
{t("changelog.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ChangelogSection
|
||||
items={[
|
||||
{
|
||||
date: "2024-03-01",
|
||||
changes: ["🚀 Improved performance"],
|
||||
},
|
||||
{
|
||||
date: "2024-02-01",
|
||||
changes: ["🎨 Updated design", "🐞 Fixed a bug"],
|
||||
},
|
||||
{
|
||||
date: "2024-01-01",
|
||||
changes: ["🎉 Added new feature", "🐞 Fixed a bug"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
apps/web/app/(marketing)/[locale]/contact/page.tsx
Normal file
33
apps/web/app/(marketing)/[locale]/contact/page.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { ContactForm } from "@marketing/home/components/ContactForm";
|
||||
import { config } from "@repo/config";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: t("contact.title"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ContactPage() {
|
||||
if (!config.contactForm.enabled) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
return (
|
||||
<div className="container max-w-xl pt-32 pb-16">
|
||||
<div className="mb-12 pt-8 text-center">
|
||||
<h1 className="mb-2 font-bold text-5xl">
|
||||
{t("contact.title")}
|
||||
</h1>
|
||||
<p className="text-balance text-lg opacity-50">
|
||||
{t("contact.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ContactForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { docsSource } from "../../../../docs-source";
|
||||
|
||||
export default async function DocumentationLayout({
|
||||
children,
|
||||
params,
|
||||
}: PropsWithChildren<{
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
const t = await getTranslations();
|
||||
const { locale } = await params;
|
||||
|
||||
return (
|
||||
<div className="pt-[4.5rem]">
|
||||
<DocsLayout
|
||||
tree={docsSource.pageTree[locale]}
|
||||
disableThemeSwitch
|
||||
i18n
|
||||
nav={{
|
||||
title: <strong>{t("documentation.title")}</strong>,
|
||||
url: "/docs",
|
||||
}}
|
||||
sidebar={{
|
||||
defaultOpenLevel: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
apps/web/app/(marketing)/[locale]/docs/[[...path]]/page.tsx
Normal file
93
apps/web/app/(marketing)/[locale]/docs/[[...path]]/page.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { MDXContent } from "@content-collections/mdx/react";
|
||||
import { config } from "@repo/config";
|
||||
import { File, Files, Folder } from "fumadocs-ui/components/files";
|
||||
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
|
||||
import { Step, Steps } from "fumadocs-ui/components/steps";
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import { DocsBody, DocsPage } from "fumadocs-ui/page";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { docsSource } from "../../../../docs-source";
|
||||
|
||||
export default async function DocumentationPage(props: {
|
||||
params: Promise<{ path?: string[]; locale: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
setRequestLocale(params.locale);
|
||||
const page = docsSource.getPage(params.path, params.locale);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
full={page.data.full}
|
||||
breadcrumb={{
|
||||
enabled: true,
|
||||
includePage: true,
|
||||
includeSeparator: true,
|
||||
}}
|
||||
tableOfContent={{
|
||||
enabled: true,
|
||||
}}
|
||||
>
|
||||
<DocsBody>
|
||||
<h1 className="text-foreground">{page.data.title}</h1>
|
||||
{page.data.description && (
|
||||
<p className="-mt-6 text-foreground/50 text-lg lg:text-xl">
|
||||
{page.data.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="prose dark:prose-invert max-w-full prose-a:text-foreground prose-p:text-foreground/80">
|
||||
<MDXContent
|
||||
code={page.data.body}
|
||||
// @ts-expect-error
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
Tabs,
|
||||
Tab,
|
||||
Steps,
|
||||
Step,
|
||||
File,
|
||||
Folder,
|
||||
Files,
|
||||
img: (props) => (
|
||||
<ImageZoom
|
||||
{...(props as any)}
|
||||
className="rounded-lg border-4 border-secondary/10"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return docsSource.getPages().flatMap((page) => ({
|
||||
path: page.slugs,
|
||||
locale: page.locale ?? config.i18n.defaultLocale,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ path?: string[]; locale: string }>;
|
||||
}) {
|
||||
const t = await getTranslations();
|
||||
const params = await props.params;
|
||||
const page = docsSource.getPage(params.path, params.locale);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${page.data.title} | ${t("documentation.title")}`,
|
||||
description: page.data.description,
|
||||
};
|
||||
}
|
||||
55
apps/web/app/(marketing)/[locale]/layout.tsx
Normal file
55
apps/web/app/(marketing)/[locale]/layout.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Footer } from "@marketing/shared/components/Footer";
|
||||
import { NavBar } from "@marketing/shared/components/NavBar";
|
||||
import { config } from "@repo/config";
|
||||
import { SessionProvider } from "@saas/auth/components/SessionProvider";
|
||||
import { Document } from "@shared/components/Document";
|
||||
import { I18nProvider as FumadocsI18nProvider } from "fumadocs-ui/i18n";
|
||||
import { RootProvider as FumadocsRootProvider } from "fumadocs-ui/provider";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
const locales = Object.keys(config.i18n.locales);
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default async function MarketingLayout({
|
||||
children,
|
||||
params,
|
||||
}: PropsWithChildren<{ params: Promise<{ locale: string }> }>) {
|
||||
const { locale } = await params;
|
||||
|
||||
setRequestLocale(locale);
|
||||
|
||||
if (!locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<FumadocsI18nProvider locale={locale}>
|
||||
<FumadocsRootProvider
|
||||
search={{
|
||||
enabled: true,
|
||||
options: {
|
||||
api: "/api/docs-search",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<SessionProvider>
|
||||
<NavBar />
|
||||
<main className="min-h-screen">{children}</main>
|
||||
<Footer />
|
||||
</SessionProvider>
|
||||
</NextIntlClientProvider>
|
||||
</FumadocsRootProvider>
|
||||
</FumadocsI18nProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
68
apps/web/app/(marketing)/[locale]/legal/[...path]/page.tsx
Normal file
68
apps/web/app/(marketing)/[locale]/legal/[...path]/page.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { localeRedirect } from "@i18n/routing";
|
||||
import { PostContent } from "@marketing/blog/components/PostContent";
|
||||
import {
|
||||
getActivePathFromUrlParam,
|
||||
getLocalizedDocumentWithFallback,
|
||||
} from "@shared/lib/content";
|
||||
import { allLegalPages } from "content-collections";
|
||||
import { getLocale } from "next-intl/server";
|
||||
|
||||
type Params = {
|
||||
path: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<Params>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const { path } = params;
|
||||
|
||||
const locale = await getLocale();
|
||||
const activePath = getActivePathFromUrlParam(path);
|
||||
const page = getLocalizedDocumentWithFallback(
|
||||
allLegalPages,
|
||||
activePath,
|
||||
locale,
|
||||
);
|
||||
|
||||
return {
|
||||
title: page?.title,
|
||||
openGraph: {
|
||||
title: page?.title,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage(props: {
|
||||
params: Promise<Params>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const { path } = params;
|
||||
|
||||
const locale = await getLocale();
|
||||
const activePath = getActivePathFromUrlParam(path);
|
||||
const page = getLocalizedDocumentWithFallback(
|
||||
allLegalPages,
|
||||
activePath,
|
||||
locale,
|
||||
);
|
||||
|
||||
if (!page) {
|
||||
localeRedirect({ href: "/", locale });
|
||||
}
|
||||
|
||||
const { title, body } = page;
|
||||
|
||||
return (
|
||||
<div className="container max-w-6xl pt-32 pb-24">
|
||||
<div className="mx-auto mb-12 max-w-2xl">
|
||||
<h1 className="text-center font-bold text-4xl">{title}</h1>
|
||||
</div>
|
||||
|
||||
<PostContent content={body} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
apps/web/app/(marketing)/[locale]/not-found.tsx
Normal file
5
apps/web/app/(marketing)/[locale]/not-found.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { NotFound } from "@marketing/shared/components/NotFound";
|
||||
|
||||
export default async function NotFoundPage() {
|
||||
return <NotFound />;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue