commit 8538ab969af9994212b3e57c7d6d5818e822ad8b Author:  Илья Шалонин Date: Wed Oct 29 13:43:43 2025 +0500 first diff --git a/.cursor/rules/key-principles.mdc b/.cursor/rules/key-principles.mdc new file mode 100644 index 0000000..85b8b0f --- /dev/null +++ b/.cursor/rules/key-principles.mdc @@ -0,0 +1,17 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes the key principles of how to generate code for this project. + +Whenever code is generated assume you are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI and Tailwind. + +- Write concise, technical TypeScript code with accurate examples. +- Use functional and declarative programming patterns; avoid classes. +- Prefer iteration and modularization over code duplication. +- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError). +- Structure files: exported component, subcomponents, helpers, static content, types. + +Follow Next.js docs for Data Fetching, Rendering, and Routing. +Follow the documentation at supastarter.dev/docs/nextjs for supastarter specifc patterns. diff --git a/.cursor/rules/naming-coventions.mdc b/.cursor/rules/naming-coventions.mdc new file mode 100644 index 0000000..68bbd95 --- /dev/null +++ b/.cursor/rules/naming-coventions.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes what naming conventions to use for files, folders, components and methods. + +- Use lowercase with dashes for directories (e.g., components/auth-wizard). +- Use PascalCase for component names +- Use camelCase for variables and method names +- Favor named exports for components. \ No newline at end of file diff --git a/.cursor/rules/performance.mdc b/.cursor/rules/performance.mdc new file mode 100644 index 0000000..c7cca19 --- /dev/null +++ b/.cursor/rules/performance.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes how to optimize the code for performance. + +- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC). +- Wrap client components in Suspense with fallback. +- Use dynamic loading for non-critical components. +- Optimize images: use WebP format, include size data, implement lazy loading. diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 0000000..0e1a089 --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,20 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes the basic project structure of the repository and where to find or add certain parts of the application: + +- The main application is in the `packages/app` directory which is a Next.js App Router. Put all frontend-only code in this directory. +- Put all backend logic into one of the `packages` directories: + - `packages/ai` contains all AI-related code + - `packages/api` contains all API routes + - `packages/auth` contains the config for better-auth and helper functions + - `packages/database` contains the database schema and auto-generated types + - `packages/i18n` contains translations and internationalization helper functions + - `packages/logs` contains the logging config and helper functions + - `packages/mail` contains providers for sending mails and email templates + - `packages/payments` contains code for payment providers and payment processing + - `packages/storage` contains providers for storing files and images + - `packages/utils` contains utility functions + - `config` contains the application configuration \ No newline at end of file diff --git a/.cursor/rules/syntax-and-formatting.mdc b/.cursor/rules/syntax-and-formatting.mdc new file mode 100644 index 0000000..c5afa91 --- /dev/null +++ b/.cursor/rules/syntax-and-formatting.mdc @@ -0,0 +1,10 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes syntax and formatting rules used across the codebase. + +- Use the "function" keyword for pure functions. +- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. +- Use declarative JSX. \ No newline at end of file diff --git a/.cursor/rules/typescript-usage.mdc b/.cursor/rules/typescript-usage.mdc new file mode 100644 index 0000000..25884f0 --- /dev/null +++ b/.cursor/rules/typescript-usage.mdc @@ -0,0 +1,10 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes how to use typescript across the project. + +- Use TypeScript for all code; prefer interfaces over types. +- Avoid enums; use maps instead. +- Use functional components with TypeScript interfaces. diff --git a/.cursor/rules/ui-and-styling.mdc b/.cursor/rules/ui-and-styling.mdc new file mode 100644 index 0000000..bfcd309 --- /dev/null +++ b/.cursor/rules/ui-and-styling.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: +alwaysApply: true +--- +This rule describes guidelines for creating UI and styling components: + +- Use Shadcn UI, Radix, and Tailwind for components and styling. +- Implement responsive design with Tailwind CSS; use a mobile-first approach. +- Use the `cn` function for class name concatenation. +- The global theme variables and tailwind config are defined in `tooling/tailwind/theme.css`. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..92ab8bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..6480166 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,76 @@ +# Database +DATABASE_URL="YOUR_DATABASE_CONNECTION_STRING" +# ... if you use Supabase +DIRECT_URL="" + +# Site url +NEXT_PUBLIC_SITE_URL="http://localhost:3000" + +# Authentication +BETTER_AUTH_SECRET="A_RANDOM_SECRET_STRING" +# ... for Github +GITHUB_CLIENT_ID="YOUR_GITHUB_CLIENT_ID" +GITHUB_CLIENT_SECRET="YOUR_GITHUB_CLIENT_SECRET" +# ... for Google +GOOGLE_CLIENT_ID="YOUR_GOOGLE_CLIENT_ID" +GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_CLIENT_SECRET" + +# Mails +# ... with nodemailer +MAIL_HOST="" +MAIL_PORT="" +MAIL_USER="" +MAIL_PASS="" +# ... with Plunk +PLUNK_API_KEY="" +# ... with Resend +RESEND_API_KEY="" +# ... with Postmark +POSTMARK_SERVER_TOKEN="" +# ... with Mailgun +MAILGUN_API_KEY="" +MAILGUN_DOMAIN="" + +# Payments +# ... with Lemonsqueezy +LEMONSQUEEZY_API_KEY="" +LEMONSQUEEZY_WEBHOOK_SECRET="" +LEMONSQUEEZY_STORE_ID="" +# ... with Stripe +STRIPE_SECRET_KEY="" +STRIPE_WEBHOOK_SECRET="" +# ... with Chargebee +CHARGEBEE_SITE="" +CHARGEBEE_API_KEY="" +# ... with Creem +CREEM_API_KEY="" +CREEM_WEBHOOK_SECRET="" +# ... with Polar +POLAR_ACCESS_TOKEN="" +POLAR_WEBHOOK_SECRET="" + +# Product price ids +NEXT_PUBLIC_PRICE_ID_PRO_MONTHLY="asdf" +NEXT_PUBLIC_PRICE_ID_PRO_YEARLY="asdf" +NEXT_PUBLIC_PRICE_ID_LIFETIME="asdf" + +# Analytics +# ... for Pirsch +NEXT_PUBLIC_PIRSCH_CODE="" +# ... for Plausible +NEXT_PUBLIC_PLAUSIBLE_URL="" +# ... for Mixpanel +NEXT_PUBLIC_MIXPANEL_TOKEN="" +# ... for Google Analytics +NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="" + +# Storage +S3_ACCESS_KEY_ID="" +S3_SECRET_ACCESS_KEY="" +S3_ENDPOINT="" +S3_REGION="" +NEXT_PUBLIC_AVATARS_BUCKET_NAME="avatars" + +# AI +# ... with OpenAI +OPENAI_API_KEY="" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4babf98 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + open-pull-requests-limit: 2 + schedule: + interval: "daily" + ignore: + - dependency-name: "cropperjs" + versions: [">1.6.2"] + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" diff --git a/.github/workflows/validate-prs.yml b/.github/workflows/validate-prs.yml new file mode 100644 index 0000000..dc92c74 --- /dev/null +++ b/.github/workflows/validate-prs.yml @@ -0,0 +1,39 @@ +name: Validate PRs + +on: + pull_request: + branches: [main] + +env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + +jobs: + lint: + name: Lint code + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + - name: Run Biome + run: biome ci . + e2e: + name: Run e2e tests + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - uses: pnpm/action-setup@v4 + - name: Install dependencies + run: pnpm install && pnpm --filter database generate + - name: Run Playwright tests + run: pnpm --filter web e2e:ci + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: apps/web/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0efb65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build +.swc/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# ui +dist/ + +# typescript +*.tsbuildinfo + +# other +.react-email/ +.content-collections/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..56b7bb4 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/betterauth + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..2d2d036 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/main-test.iml b/.idea/main-test.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/main-test.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..bd8b072 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f0c0c3d --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=*prisma* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2f18a36 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "lokalise.i18n-ally", + "bradlc.vscode-tailwindcss", + "biomejs.biome" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7c222d7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "tailwindCSS.experimental.classRegex": [ + ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] + ], + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "typescript.preferences.importModuleSpecifier": "non-relative", + "typescript.tsdk": "node_modules/typescript/lib", + "i18n-ally.localesPaths": ["packages/i18n/translations"], + "i18n-ally.keystyle": "nested", + "i18n-ally.enabledFrameworks": ["next-intl"], + "i18n-ally.keysInUse": ["mail.organizationInvitation.headline"], + "i18n-ally.tabStyle": "tab", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + } +} diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..6415834 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,60 @@ +You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI and Tailwind. + +Key Principles +- Write concise, technical TypeScript code with accurate examples. +- Use functional and declarative programming patterns; avoid classes. +- Prefer iteration and modularization over code duplication. +- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError). +- Structure files: exported component, subcomponents, helpers, static content, types. + +Project Structure +- The main application is in the `packages/app` directory which is a Next.js App Router. Put all frontend-only code in this directory. +- Put all backend logic into one of the `packages` directories: + - `packages/ai` contains all AI-related code + - `packages/api` contains all API routes + - `packages/auth` contains the config for better-auth and helper functions + - `packages/database` contains the database schema and auto-generated types + - `packages/i18n` contains translations and internationalization helper functions + - `packages/logs` contains the logging config and helper functions + - `packages/mail` contains providers for sending mails and email templates + - `packages/payments` contains code for payment providers and payment processing + - `packages/storage` contains providers for storing files and images + - `packages/utils` contains utility functions + - `config` contains the application configuration + +Naming Conventions +- Use lowercase with dashes for directories (e.g., components/auth-wizard). +- Favor named exports for components. + +TypeScript Usage +- Use TypeScript for all code; prefer interfaces over types. +- Avoid enums; use maps instead. +- Use functional components with TypeScript interfaces. + +Syntax and Formatting +- Use the "function" keyword for pure functions. +- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. +- Use declarative JSX. + +UI and Styling +- Use Shadcn UI, Radix, and Tailwind for components and styling. +- Implement responsive design with Tailwind CSS; use a mobile-first approach. +- Use the `cn` function for class name concatenation. +- The global theme variables and tailwind config are defined in `tooling/tailwind/theme.css`. + +Performance Optimization +- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC). +- Wrap client components in Suspense with fallback. +- Use dynamic loading for non-critical components. +- Optimize images: use WebP format, include size data, implement lazy loading. + +Key Conventions +- Use 'nuqs' for URL search parameter state management. +- Optimize Web Vitals (LCP, CLS, FID). +- Limit 'use client': +- Favor server components and Next.js SSR. +- Use only for Web API access in small components. +- Avoid for data fetching or state management. + +Follow Next.js docs for Data Fetching, Rendering, and Routing. +Follow the documentation at supastarter.dev/docs/nextjs for supastarter specifc patterns. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4fed72 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# supastarter for Next.js + +supastarter is the ultimate starter kit for production-ready, scalable SaaS applications. + +## Helpful links + +- [📘 Documentation](https://supastarter.dev/docs/nextjs) +- [🚀 Demo](https://demo.supastarter.dev) \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..ebaca49 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage +/playwright-report +/test-results + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.content-collections diff --git a/apps/web/app/(marketing)/[locale]/(home)/page.tsx b/apps/web/app/(marketing)/[locale]/(home)/page.tsx new file mode 100644 index 0000000..2cd572c --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/(home)/page.tsx @@ -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 ( + <> + + + + + + + ); +} diff --git a/apps/web/app/(marketing)/[locale]/[...rest]/page.tsx b/apps/web/app/(marketing)/[locale]/[...rest]/page.tsx new file mode 100644 index 0000000..4583936 --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/[...rest]/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from "next/navigation"; + +export default function CatchAll() { + notFound(); +} diff --git a/apps/web/app/(marketing)/[locale]/blog/[...path]/page.tsx b/apps/web/app/(marketing)/[locale]/blog/[...path]/page.tsx new file mode 100644 index 0000000..d00f4bb --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/blog/[...path]/page.tsx @@ -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 }) { + 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 }) { + 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 ( +
+
+
+ + ← {t("blog.back")} + +
+ +

{title}

+ +
+ {authorName && ( +
+ {authorImage && ( +
+ {authorName} +
+ )} +
+

+ {authorName} +

+
+
+ )} + +
+

+ {Intl.DateTimeFormat("en-US").format( + new Date(date), + )} +

+
+ + {tags && ( +
+ {tags.map((tag) => ( + + #{tag} + + ))} +
+ )} +
+
+ + {image && ( +
+ {title} +
+ )} + +
+ +
+
+ ); +} diff --git a/apps/web/app/(marketing)/[locale]/blog/page.tsx b/apps/web/app/(marketing)/[locale]/blog/page.tsx new file mode 100644 index 0000000..fdc4fac --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/blog/page.tsx @@ -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 ( +
+
+

{t("blog.title")}

+

{t("blog.description")}

+
+ +
+ {posts + .filter((post) => post.published && locale === post.locale) + .sort( + (a, b) => + new Date(b.date).getTime() - + new Date(a.date).getTime(), + ) + .map((post) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/app/(marketing)/[locale]/changelog/page.tsx b/apps/web/app/(marketing)/[locale]/changelog/page.tsx new file mode 100644 index 0000000..71bf0fe --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/changelog/page.tsx @@ -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 ( +
+
+

+ {t("changelog.title")} +

+

+ {t("changelog.description")} +

+
+ +
+ ); +} diff --git a/apps/web/app/(marketing)/[locale]/contact/page.tsx b/apps/web/app/(marketing)/[locale]/contact/page.tsx new file mode 100644 index 0000000..ff01c5f --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/contact/page.tsx @@ -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 ( +
+
+

+ {t("contact.title")} +

+

+ {t("contact.description")} +

+
+ + +
+ ); +} diff --git a/apps/web/app/(marketing)/[locale]/docs/[[...path]]/layout.tsx b/apps/web/app/(marketing)/[locale]/docs/[[...path]]/layout.tsx new file mode 100644 index 0000000..40e9752 --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/docs/[[...path]]/layout.tsx @@ -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 ( +
+ {t("documentation.title")}, + url: "/docs", + }} + sidebar={{ + defaultOpenLevel: 1, + }} + > + {children} + +
+ ); +} diff --git a/apps/web/app/(marketing)/[locale]/docs/[[...path]]/page.tsx b/apps/web/app/(marketing)/[locale]/docs/[[...path]]/page.tsx new file mode 100644 index 0000000..a2b452a --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/docs/[[...path]]/page.tsx @@ -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 ( + + +

{page.data.title}

+ {page.data.description && ( +

+ {page.data.description} +

+ )} +
+ ( + + ), + }} + /> +
+
+
+ ); +} + +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, + }; +} diff --git a/apps/web/app/(marketing)/[locale]/layout.tsx b/apps/web/app/(marketing)/[locale]/layout.tsx new file mode 100644 index 0000000..0c6750a --- /dev/null +++ b/apps/web/app/(marketing)/[locale]/layout.tsx @@ -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 ( + + + + + + +
{children}
+