From 116d41a284a7c8088955f93c0b10b3679885a79f Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Sat, 1 Nov 2025 11:59:52 +0700 Subject: [PATCH] Changelog (#399) * feat: add changelog feature and update site navigation - Introduced a new Changelog page with pagination and detailed entry views. - Added components for displaying changelog entries, pagination, and entry details. - Updated site navigation to include a link to the new Changelog page. - Enhanced localization for changelog-related texts in marketing.json. * refactor: enhance Changelog page layout and entry display - Increased the number of changelog entries displayed per page from 2 to 20 for better visibility. - Improved the layout of the Changelog page by adjusting the container styles and removing unnecessary divs. - Updated the ChangelogEntry component to enhance the visual presentation of entries, including a new date badge with an icon. - Refined the CSS styles for Markdoc headings to improve typography and spacing. * refactor: enhance Changelog page functionality and layout - Increased the number of changelog entries displayed per page from 20 to 50 for improved user experience. - Updated ChangelogEntry component to make the highlight prop optional and refined the layout for better visual clarity. - Adjusted styles in ChangelogHeader and ChangelogPagination components for a more cohesive design. - Removed unnecessary order fields from changelog markdown files to streamline content management. * feat: enhance Changelog entry navigation and data loading - Refactored ChangelogEntry page to load previous and next entries for improved navigation. - Introduced ChangelogNavigation component to facilitate navigation between changelog entries. - Updated ChangelogDetail component to display navigation links and entry details. - Enhanced data fetching logic to retrieve all changelog entries alongside the current entry. - Added localization keys for navigation text in marketing.json. * Update package dependencies and enhance documentation layout - Upgraded various packages including @turbo/gen and turbo to version 2.6.0, and react-hook-form to version 7.66.0. - Updated lucide-react to version 0.552.0 across multiple packages. - Refactored documentation layout components for improved styling and structure. - Removed deprecated loading components and adjusted navigation elements for better user experience. - Added placeholder notes in changelog entries for clarity. --- apps/dev-tool/package.json | 8 +- .../_components/site-navigation.tsx | 8 +- .../app/(marketing)/changelog/[slug]/page.tsx | 110 +++++ .../_components/changelog-detail.tsx | 36 ++ .../changelog/_components/changelog-entry.tsx | 59 +++ .../_components/changelog-header.tsx | 65 +++ .../_components/changelog-navigation.tsx | 79 ++++ .../_components/changelog-pagination.tsx | 47 ++ .../changelog/_components/date-badge.tsx | 17 + apps/web/app/(marketing)/changelog/page.tsx | 117 +++++ .../app/(marketing)/docs/[...slug]/page.tsx | 54 +-- .../docs/_components/docs-card.tsx | 2 +- .../docs/_components/docs-cards.tsx | 2 +- .../docs/_components/docs-nav-link.tsx | 11 +- .../docs-navigation-collapsible.tsx | 6 +- .../docs/_components/docs-navigation.tsx | 6 +- .../docs/_components/docs-page-link.tsx | 45 -- .../_components/docs-table-of-contents.tsx | 51 -- apps/web/app/(marketing)/docs/_lib/utils.ts | 113 ----- apps/web/app/(marketing)/docs/layout.tsx | 32 +- apps/web/app/(marketing)/docs/loading.tsx | 3 - apps/web/app/(marketing)/docs/page.tsx | 8 +- apps/web/app/(marketing)/loading.tsx | 3 - apps/web/app/layout.tsx | 2 +- .../advanced-analytics-dashboard.mdoc | 35 ++ .../content/changelog/api-improvements.mdoc | 38 ++ .../content/changelog/custom-workflows.mdoc | 39 ++ .../changelog/mobile-app-redesign.mdoc | 27 ++ .../changelog/security-enhancements.mdoc | 45 ++ .../team-collaboration-features.mdoc | 31 ++ .../authentication/authentication.mdoc | 2 +- .../authentication/email-password.mdoc | 382 +++++++++++++++ .../authentication/magic-links.mdoc | 392 +++++++++++++++ .../authentication/oauth-providers.mdoc | 395 ++++++++++++++++ .../documentation/billing/billing.mdoc | 15 + .../documentation/billing/pricing-plans.mdoc | 186 ++++++++ .../documentation/billing/subscriptions.mdoc | 143 ++++++ .../documentation/billing/webhooks.mdoc | 194 ++++++++ .../documentation/database/database.mdoc | 16 + .../database/functions-triggers.mdoc | 446 ++++++++++++++++++ .../documentation/database/migrations.mdoc | 68 +++ .../documentation/database/querying-data.mdoc | 430 +++++++++++++++++ .../database/row-level-security.mdoc | 88 ++++ .../documentation/database/schema.mdoc | 43 ++ .../content/documentation/features/email.mdoc | 279 +++++++++++ .../documentation/features/features.mdoc | 16 + .../documentation/features/file-uploads.mdoc | 398 ++++++++++++++++ .../features/team-collaboration.mdoc | 276 +++++++++++ .../getting-started/configuration.mdoc | 350 ++++++++++++++ .../getting-started/getting-started.mdoc | 4 +- .../installing-dependencies.mdoc | 4 +- .../getting-started/project-structure.mdoc | 247 ++++++++++ .../getting-started/quick-start.mdoc | 133 ++++++ .../content/posts/brainstorming-ideas.mdoc | 2 + .../web/content/posts/must-have-features.mdoc | 2 +- .../web/content/posts/saas-starter-guide.mdoc | 2 + apps/web/package.json | 4 +- apps/web/public/locales/en/marketing.json | 7 + apps/web/styles/globals.css | 23 +- apps/web/styles/markdoc.css | 26 +- package.json | 6 +- packages/billing/gateway/package.json | 4 +- .../cms/keystatic/src/keystatic-client.ts | 4 +- .../cms/keystatic/src/keystatic.config.ts | 72 ++- packages/features/accounts/package.json | 4 +- packages/features/admin/package.json | 4 +- packages/features/auth/package.json | 4 +- packages/features/notifications/package.json | 2 +- packages/features/team-accounts/package.json | 4 +- packages/otp/package.json | 2 +- packages/ui/package.json | 6 +- pnpm-lock.yaml | 408 ++++++++-------- tooling/eslint/package.json | 4 +- 73 files changed, 5638 insertions(+), 558 deletions(-) create mode 100644 apps/web/app/(marketing)/changelog/[slug]/page.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/changelog-detail.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/changelog-entry.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/changelog-header.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/changelog-navigation.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/changelog-pagination.tsx create mode 100644 apps/web/app/(marketing)/changelog/_components/date-badge.tsx create mode 100644 apps/web/app/(marketing)/changelog/page.tsx delete mode 100644 apps/web/app/(marketing)/docs/_components/docs-page-link.tsx delete mode 100644 apps/web/app/(marketing)/docs/_components/docs-table-of-contents.tsx delete mode 100644 apps/web/app/(marketing)/docs/loading.tsx delete mode 100644 apps/web/app/(marketing)/loading.tsx create mode 100644 apps/web/content/changelog/advanced-analytics-dashboard.mdoc create mode 100644 apps/web/content/changelog/api-improvements.mdoc create mode 100644 apps/web/content/changelog/custom-workflows.mdoc create mode 100644 apps/web/content/changelog/mobile-app-redesign.mdoc create mode 100644 apps/web/content/changelog/security-enhancements.mdoc create mode 100644 apps/web/content/changelog/team-collaboration-features.mdoc create mode 100644 apps/web/content/documentation/authentication/email-password.mdoc create mode 100644 apps/web/content/documentation/authentication/magic-links.mdoc create mode 100644 apps/web/content/documentation/authentication/oauth-providers.mdoc create mode 100644 apps/web/content/documentation/billing/billing.mdoc create mode 100644 apps/web/content/documentation/billing/pricing-plans.mdoc create mode 100644 apps/web/content/documentation/billing/subscriptions.mdoc create mode 100644 apps/web/content/documentation/billing/webhooks.mdoc create mode 100644 apps/web/content/documentation/database/database.mdoc create mode 100644 apps/web/content/documentation/database/functions-triggers.mdoc create mode 100644 apps/web/content/documentation/database/migrations.mdoc create mode 100644 apps/web/content/documentation/database/querying-data.mdoc create mode 100644 apps/web/content/documentation/database/row-level-security.mdoc create mode 100644 apps/web/content/documentation/database/schema.mdoc create mode 100644 apps/web/content/documentation/features/email.mdoc create mode 100644 apps/web/content/documentation/features/features.mdoc create mode 100644 apps/web/content/documentation/features/file-uploads.mdoc create mode 100644 apps/web/content/documentation/features/team-collaboration.mdoc create mode 100644 apps/web/content/documentation/getting-started/configuration.mdoc create mode 100644 apps/web/content/documentation/getting-started/project-structure.mdoc create mode 100644 apps/web/content/documentation/getting-started/quick-start.mdoc diff --git a/apps/dev-tool/package.json b/apps/dev-tool/package.json index c927d41ea..bffe494e6 100644 --- a/apps/dev-tool/package.json +++ b/apps/dev-tool/package.json @@ -8,12 +8,12 @@ "format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\"" }, "dependencies": { - "@ai-sdk/openai": "^2.0.58", + "@ai-sdk/openai": "^2.0.59", "@faker-js/faker": "^10.1.0", "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "5.90.5", - "ai": "5.0.83", - "lucide-react": "^0.548.0", + "ai": "5.0.86", + "lucide-react": "^0.552.0", "next": "catalog:", "nodemailer": "^7.0.10", "react": "19.2.0", @@ -36,7 +36,7 @@ "@types/react-dom": "19.2.2", "babel-plugin-react-compiler": "1.0.0", "pino-pretty": "13.0.0", - "react-hook-form": "^7.65.0", + "react-hook-form": "^7.66.0", "recharts": "2.15.3", "tailwindcss": "4.1.16", "tailwindcss-animate": "^1.0.7", diff --git a/apps/web/app/(marketing)/_components/site-navigation.tsx b/apps/web/app/(marketing)/_components/site-navigation.tsx index f4e235cdd..c8f055de9 100644 --- a/apps/web/app/(marketing)/_components/site-navigation.tsx +++ b/apps/web/app/(marketing)/_components/site-navigation.tsx @@ -18,6 +18,10 @@ const links = { label: 'marketing:blog', path: '/blog', }, + Changelog: { + label: 'marketing:changelog', + path: '/changelog', + }, Docs: { label: 'marketing:documentation', path: '/docs', @@ -30,10 +34,6 @@ const links = { label: 'marketing:faq', path: '/faq', }, - Contact: { - label: 'marketing:contact', - path: '/contact', - }, }; export function SiteNavigation() { diff --git a/apps/web/app/(marketing)/changelog/[slug]/page.tsx b/apps/web/app/(marketing)/changelog/[slug]/page.tsx new file mode 100644 index 000000000..15f3d35c4 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/[slug]/page.tsx @@ -0,0 +1,110 @@ +import { cache } from 'react'; + +import type { Metadata } from 'next'; + +import { notFound } from 'next/navigation'; + +import { createCmsClient } from '@kit/cms'; + +import { withI18n } from '~/lib/i18n/with-i18n'; + +import { ChangelogDetail } from '../_components/changelog-detail'; + +interface ChangelogEntryPageProps { + params: Promise<{ slug: string }>; +} + +const getChangelogData = cache(changelogEntryLoader); + +async function changelogEntryLoader(slug: string) { + const client = await createCmsClient(); + + const [entry, allEntries] = await Promise.all([ + client.getContentItemBySlug({ slug, collection: 'changelog' }), + client.getContentItems({ + collection: 'changelog', + sortBy: 'publishedAt', + sortDirection: 'desc', + content: false, + }), + ]); + + if (!entry) { + return null; + } + + // Find previous and next entries in the timeline + const currentIndex = allEntries.items.findIndex((item) => item.slug === slug); + const previousEntry = + currentIndex > 0 ? allEntries.items[currentIndex - 1] : null; + const nextEntry = + currentIndex < allEntries.items.length - 1 + ? allEntries.items[currentIndex + 1] + : null; + + return { + entry, + previousEntry, + nextEntry, + }; +} + +export async function generateMetadata({ + params, +}: ChangelogEntryPageProps): Promise { + const slug = (await params).slug; + const data = await getChangelogData(slug); + + if (!data) { + notFound(); + } + + const { title, publishedAt, description, image } = data.entry; + + return Promise.resolve({ + title, + description, + openGraph: { + title, + description, + type: 'article', + publishedTime: publishedAt, + url: data.entry.url, + images: image + ? [ + { + url: image, + }, + ] + : [], + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: image ? [image] : [], + }, + }); +} + +async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) { + const slug = (await params).slug; + const data = await getChangelogData(slug); + + if (!data) { + notFound(); + } + + return ( +
+ +
+ ); +} + +export default withI18n(ChangelogEntryPage); diff --git a/apps/web/app/(marketing)/changelog/_components/changelog-detail.tsx b/apps/web/app/(marketing)/changelog/_components/changelog-detail.tsx new file mode 100644 index 000000000..2eda089a6 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/changelog-detail.tsx @@ -0,0 +1,36 @@ +import type { Cms } from '@kit/cms'; +import { ContentRenderer } from '@kit/cms'; + +import { ChangelogHeader } from './changelog-header'; +import { ChangelogNavigation } from './changelog-navigation'; + +interface ChangelogDetailProps { + entry: Cms.ContentItem; + content: unknown; + previousEntry: Cms.ContentItem | null; + nextEntry: Cms.ContentItem | null; +} + +export function ChangelogDetail({ + entry, + content, + previousEntry, + nextEntry, +}: ChangelogDetailProps) { + return ( +
+ + +
+
+ +
+
+ + +
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/_components/changelog-entry.tsx b/apps/web/app/(marketing)/changelog/_components/changelog-entry.tsx new file mode 100644 index 000000000..d045a6e1d --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/changelog-entry.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link'; + +import { type Cms } from '@kit/cms'; +import { If } from '@kit/ui/if'; +import { cn } from '@kit/ui/utils'; + +import { DateBadge } from './date-badge'; + +interface ChangelogEntryProps { + entry: Cms.ContentItem; + highlight?: boolean; +} + +export function ChangelogEntry({ + entry, + highlight = false, +}: ChangelogEntryProps) { + const { title, slug, publishedAt, description } = entry; + const entryUrl = `/changelog/${slug}`; + + return ( +
+
+ {highlight ? ( + + + + + ) : ( + +
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/_components/changelog-header.tsx b/apps/web/app/(marketing)/changelog/_components/changelog-header.tsx new file mode 100644 index 000000000..3e689a317 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/changelog-header.tsx @@ -0,0 +1,65 @@ +import Link from 'next/link'; + +import { ChevronLeft } from 'lucide-react'; + +import { Cms } from '@kit/cms'; +import { If } from '@kit/ui/if'; +import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; + +import { CoverImage } from '../../blog/_components/cover-image'; +import { DateFormatter } from '../../blog/_components/date-formatter'; + +export function ChangelogHeader({ entry }: { entry: Cms.ContentItem }) { + const { title, publishedAt, description, image } = entry; + + return ( +
+
+
+ + + + +
+
+ +
+
+
+ + + +
+ +

+ {title} +

+ + {description && ( +

+ )} +

+
+ + + {(imageUrl) => ( +
+ +
+ )} +
+
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/_components/changelog-navigation.tsx b/apps/web/app/(marketing)/changelog/_components/changelog-navigation.tsx new file mode 100644 index 000000000..3cb115ed2 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/changelog-navigation.tsx @@ -0,0 +1,79 @@ +import Link from 'next/link'; + +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +import type { Cms } from '@kit/cms'; +import { If } from '@kit/ui/if'; +import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; + +import { DateFormatter } from '../../blog/_components/date-formatter'; + +interface ChangelogNavigationProps { + previousEntry: Cms.ContentItem | null; + nextEntry: Cms.ContentItem | null; +} + +interface NavLinkProps { + entry: Cms.ContentItem; + direction: 'previous' | 'next'; +} + +function NavLink({ entry, direction }: NavLinkProps) { + const isPrevious = direction === 'previous'; + + const Icon = isPrevious ? ChevronLeft : ChevronRight; + const i18nKey = isPrevious + ? 'marketing:changelogNavigationPrevious' + : 'marketing:changelogNavigationNext'; + + return ( + +
+ {isPrevious && } + + + + + {!isPrevious && } +
+ +
+

+ {entry.title} +

+ +
+ +
+
+ + ); +} + +export function ChangelogNavigation({ + previousEntry, + nextEntry, +}: ChangelogNavigationProps) { + return ( +
+
+
+ }> + {(prev) => } + + + }> + {(next) => } + +
+
+
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/_components/changelog-pagination.tsx b/apps/web/app/(marketing)/changelog/_components/changelog-pagination.tsx new file mode 100644 index 000000000..700684a38 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/changelog-pagination.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import { ArrowLeft, ArrowRight } from 'lucide-react'; + +import { Button } from '@kit/ui/button'; +import { Trans } from '@kit/ui/trans'; + +interface ChangelogPaginationProps { + currentPage: number; + canGoToNextPage: boolean; + canGoToPreviousPage: boolean; +} + +export function ChangelogPagination({ + currentPage, + canGoToNextPage, + canGoToPreviousPage, +}: ChangelogPaginationProps) { + const nextPage = currentPage + 1; + const previousPage = currentPage - 1; + + return ( +
+ {canGoToPreviousPage && ( + + )} + + {canGoToNextPage && ( + + )} +
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/_components/date-badge.tsx b/apps/web/app/(marketing)/changelog/_components/date-badge.tsx new file mode 100644 index 000000000..8c3ccdaf3 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/_components/date-badge.tsx @@ -0,0 +1,17 @@ +import { format } from 'date-fns'; +import { CalendarIcon } from 'lucide-react'; + +interface DateBadgeProps { + date: string; +} + +export function DateBadge({ date }: DateBadgeProps) { + const formattedDate = format(new Date(date), 'MMMM d, yyyy'); + + return ( +
+ + {formattedDate} +
+ ); +} diff --git a/apps/web/app/(marketing)/changelog/page.tsx b/apps/web/app/(marketing)/changelog/page.tsx new file mode 100644 index 000000000..024f830f2 --- /dev/null +++ b/apps/web/app/(marketing)/changelog/page.tsx @@ -0,0 +1,117 @@ +import { cache } from 'react'; + +import type { Metadata } from 'next'; + +import { createCmsClient } from '@kit/cms'; +import { getLogger } from '@kit/shared/logger'; +import { If } from '@kit/ui/if'; +import { Trans } from '@kit/ui/trans'; + +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +import { SitePageHeader } from '../_components/site-page-header'; +import { ChangelogEntry } from './_components/changelog-entry'; +import { ChangelogPagination } from './_components/changelog-pagination'; + +interface ChangelogPageProps { + searchParams: Promise<{ page?: string }>; +} + +const CHANGELOG_ENTRIES_PER_PAGE = 50; + +export const generateMetadata = async ( + props: ChangelogPageProps, +): Promise => { + const { t, resolvedLanguage } = await createI18nServerInstance(); + const searchParams = await props.searchParams; + const limit = CHANGELOG_ENTRIES_PER_PAGE; + + const page = searchParams.page ? parseInt(searchParams.page) : 0; + const offset = page * limit; + + const { total } = await getContentItems(resolvedLanguage, limit, offset); + + return { + title: t('marketing:changelog'), + description: t('marketing:changelogSubtitle'), + pagination: { + previous: page > 0 ? `/changelog?page=${page - 1}` : undefined, + next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined, + }, + }; +}; + +const getContentItems = cache( + async (language: string | undefined, limit: number, offset: number) => { + const client = await createCmsClient(); + const logger = await getLogger(); + + try { + return await client.getContentItems({ + collection: 'changelog', + limit, + offset, + content: false, + language, + sortBy: 'publishedAt', + sortDirection: 'desc', + }); + } catch (error) { + logger.error({ error }, 'Failed to load changelog entries'); + + return { total: 0, items: [] }; + } + }, +); + +async function ChangelogPage(props: ChangelogPageProps) { + const { t, resolvedLanguage: language } = await createI18nServerInstance(); + const searchParams = await props.searchParams; + + const limit = CHANGELOG_ENTRIES_PER_PAGE; + const page = searchParams.page ? parseInt(searchParams.page) : 0; + const offset = page * limit; + + const { total, items: entries } = await getContentItems( + language, + limit, + offset, + ); + + return ( + <> + + +
+ 0} + fallback={} + > +
+ {entries.map((entry, index) => { + return ( + + ); + })} +
+ + 0} + /> +
+
+ + ); +} + +export default withI18n(ChangelogPage); diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index 3fa54c2fa..3b030aba3 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -11,8 +11,6 @@ import { withI18n } from '~/lib/i18n/with-i18n'; // local imports import { DocsCards } from '../_components/docs-cards'; -import { DocsTableOfContents } from '../_components/docs-table-of-contents'; -import { extractHeadingsFromJSX } from '../_lib/utils'; const getPageBySlug = cache(pageLoader); @@ -52,38 +50,36 @@ async function DocumentationPage({ params }: DocumentationPageProps) { const description = page?.description ?? ''; - const headings = extractHeadingsFromJSX( - page.content as { - props: { children: React.ReactElement[] }; - }, - ); - return ( -
-
-
-
+
+
+
-

- {page.title} -

+

+ {page.title} +

-

- {description} -

-
+

+ {description} +

+ -
- -
-
- - +
+ +
+ +
0}> diff --git a/apps/web/app/(marketing)/docs/_components/docs-card.tsx b/apps/web/app/(marketing)/docs/_components/docs-card.tsx index 4b30eeeac..0a64598e5 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-card.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-card.tsx @@ -13,7 +13,7 @@ export function DocsCard({ return (

{title} diff --git a/apps/web/app/(marketing)/docs/_components/docs-cards.tsx b/apps/web/app/(marketing)/docs/_components/docs-cards.tsx index 54c9765c9..068118a08 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-cards.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-cards.tsx @@ -6,7 +6,7 @@ export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) { const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order); return ( -
+
{cardsSortedByOrder.map((item) => { return ( ) { const currentPath = usePathname(); - const ref = useRef(null); const isCurrent = isRouteActive(url, currentPath, true); return ( @@ -22,14 +19,10 @@ export function DocsNavLink({ - - {label} - + {label} {children} diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation-collapsible.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation-collapsible.tsx index 5a36aa498..9bd35f05f 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-navigation-collapsible.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-navigation-collapsible.tsx @@ -4,7 +4,7 @@ import { usePathname } from 'next/navigation'; import { Cms } from '@kit/cms'; import { Collapsible } from '@kit/ui/collapsible'; -import { isRouteActive } from '@kit/ui/utils'; +import { cn, isRouteActive } from '@kit/ui/utils'; export function DocsNavigationCollapsible( props: React.PropsWithChildren<{ @@ -21,7 +21,9 @@ export function DocsNavigationCollapsible( return ( {props.children} diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx index e565fd263..d07eb6dd9 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx @@ -139,12 +139,12 @@ export function DocsNavigation({ - + - + diff --git a/apps/web/app/(marketing)/docs/_components/docs-page-link.tsx b/apps/web/app/(marketing)/docs/_components/docs-page-link.tsx deleted file mode 100644 index 52cdad12c..000000000 --- a/apps/web/app/(marketing)/docs/_components/docs-page-link.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import Link from 'next/link'; - -import { If } from '@kit/ui/if'; -import { cn } from '@kit/ui/utils'; - -export function DocsPageLink({ - page, - before, - after, -}: React.PropsWithChildren<{ - page: { - url: string; - title: string; - }; - before?: React.ReactNode; - after?: React.ReactNode; -}>) { - return ( - - {(node) => <>{node}} - - - - {before ? `Previous` : ``} - {after ? `Next` : ``} - - - {page.title} - - - {(node) => <>{node}} - - ); -} diff --git a/apps/web/app/(marketing)/docs/_components/docs-table-of-contents.tsx b/apps/web/app/(marketing)/docs/_components/docs-table-of-contents.tsx deleted file mode 100644 index 0d30d0417..000000000 --- a/apps/web/app/(marketing)/docs/_components/docs-table-of-contents.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -interface NavItem { - text: string; - level: number; - href: string; - children: NavItem[]; -} - -export function DocsTableOfContents(props: { data: NavItem[] }) { - const navData = props.data; - - return ( -
-
    - {navData.map((item) => ( -
  1. - - {item.text} - - {item.children && ( -
      - {item.children.map((child) => ( -
    1. - - {child.text} - -
    2. - ))} -
    - )} -
  2. - ))} -
-
- ); -} diff --git a/apps/web/app/(marketing)/docs/_lib/utils.ts b/apps/web/app/(marketing)/docs/_lib/utils.ts index eac429814..033ee4919 100644 --- a/apps/web/app/(marketing)/docs/_lib/utils.ts +++ b/apps/web/app/(marketing)/docs/_lib/utils.ts @@ -1,12 +1,5 @@ import { Cms } from '@kit/cms'; -interface HeadingNode { - text: string; - level: number; - href: string; - children: HeadingNode[]; -} - /** * @name buildDocumentationTree * @description Build a tree structure for the documentation pages. @@ -38,109 +31,3 @@ export function buildDocumentationTree(pages: Cms.ContentItem[]) { return tree.sort((a, b) => a.order - b.order); } - -/** - * @name extractHeadingsFromJSX - * @description Extract headings from JSX. This is used to generate the table of contents for the documentation pages. - * @param jsx - */ -export function extractHeadingsFromJSX(jsx: { - props: { children: React.ReactElement[] }; -}) { - const headings: HeadingNode[] = []; - let currentH2: HeadingNode | null = null; - - function getTextContent( - children: React.ReactElement[] | string | React.ReactElement, - ): string { - try { - if (typeof children === 'string') { - return children; - } - - if (Array.isArray(children)) { - return children.map((child) => getTextContent(child)).join(''); - } - - if ( - ( - children.props as { - children: React.ReactElement; - } - ).children - ) { - return getTextContent( - (children.props as { children: React.ReactElement }).children, - ); - } - - return ''; - } catch { - return ''; - } - } - - try { - jsx.props.children.forEach((node) => { - if (!node || typeof node !== 'object' || !('type' in node)) { - return; - } - - const nodeType = node.type as string; - - const text = getTextContent( - ( - node.props as { - children: React.ReactElement[]; - } - ).children, - ); - - if (nodeType === 'h1') { - const slug = generateSlug(text); - - headings.push({ - text, - level: 1, - href: `#${slug}`, - children: [], - }); - } else if (nodeType === 'h2') { - const slug = generateSlug(text); - - currentH2 = { - text, - level: 2, - href: `#${slug}`, - children: [], - }; - - if (headings.length > 0) { - headings[headings.length - 1]!.children.push(currentH2); - } else { - headings.push(currentH2); - } - } else if (nodeType === 'h3' && currentH2) { - const slug = generateSlug(text); - - currentH2.children.push({ - text, - level: 3, - href: `#${slug}`, - children: [], - }); - } - }); - - return headings; - } catch { - return []; - } -} - -function generateSlug(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); -} diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx index bba657dfb..2a5e3b914 100644 --- a/apps/web/app/(marketing)/docs/layout.tsx +++ b/apps/web/app/(marketing)/docs/layout.tsx @@ -13,14 +13,32 @@ async function DocsLayout({ children }: React.PropsWithChildren) { const tree = buildDocumentationTree(docs); return ( - - +
+ + - {children} - + + + {children} + +
+ ); +} + +function HideFooterStyles() { + return ( +