-
Notifications
You must be signed in to change notification settings - Fork 5
App router migration #1295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: cursor/docs-api-references-strategy-027f
Are you sure you want to change the base?
App router migration #1295
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| export default function Loading() { | ||
| return ( | ||
| <div className="p-6"> | ||
| <div className="flex flex-col gap-6"> | ||
| <div className="h-12 w-64 bg-gray-200 rounded animate-pulse" /> | ||
| <div className="h-6 w-96 bg-gray-100 rounded animate-pulse" /> | ||
| <div className="flex flex-col gap-4 mt-8"> | ||
| {[1, 2, 3].map((i) => ( | ||
| <div | ||
| key={i} | ||
| className="h-48 w-full bg-gray-100 rounded animate-pulse" | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import { Suspense } from "react"; | ||
| import { getStainlessSpec } from "../../../../lib/openapi/loader"; | ||
| import { | ||
| buildSchemaReferences, | ||
| getSidebarContent, | ||
| } from "../../../../lib/openapi/helpers"; | ||
| import { | ||
| RESOURCE_ORDER, | ||
| API_REFERENCE_OVERVIEW_CONTENT, | ||
| } from "../../../../data/sidebars/apiOverviewSidebar"; | ||
| import { | ||
| PageShell, | ||
| ResourceSection, | ||
| PreContent, | ||
| ResourceSectionSkeleton, | ||
| } from "../components"; | ||
| import type { Metadata } from "next"; | ||
|
|
||
| export const dynamic = "force-static"; | ||
| export const revalidate = 3600; | ||
|
|
||
| export async function generateMetadata(): Promise<Metadata> { | ||
| return { | ||
| title: "API reference | Knock Docs", | ||
| description: "Complete reference documentation for the Knock API.", | ||
| alternates: { | ||
| canonical: "/api-reference", | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| // Generate all possible static paths | ||
| export async function generateStaticParams() { | ||
| const stainless = await getStainlessSpec("api"); | ||
| const paths: { slug: string[] }[] = []; | ||
|
|
||
| // Root path (no slug) | ||
| paths.push({ slug: [] }); | ||
|
|
||
| // Overview paths | ||
| API_REFERENCE_OVERVIEW_CONTENT.forEach((section) => { | ||
| if (section.pages) { | ||
| section.pages.forEach((page) => { | ||
| const slug = page.slug.replace(/^\//, "").split("/").filter(Boolean); | ||
| if (slug.length > 0) { | ||
| paths.push({ slug: ["overview", ...slug] }); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| paths.push({ slug: ["overview"] }); | ||
|
|
||
| // Resource paths | ||
| function addResourcePaths( | ||
| resourceName: string, | ||
| resource: (typeof stainless.resources)[string], | ||
| basePath: string[] = [], | ||
| ) { | ||
| const resourcePath = [...basePath, resourceName]; | ||
| paths.push({ slug: resourcePath }); | ||
|
|
||
| // Method paths | ||
| if (resource.methods) { | ||
| Object.keys(resource.methods).forEach((methodName) => { | ||
| paths.push({ slug: [...resourcePath, methodName] }); | ||
| }); | ||
| } | ||
|
|
||
| // Schema paths | ||
| if (resource.models) { | ||
| paths.push({ slug: [...resourcePath, "schemas"] }); | ||
| Object.keys(resource.models).forEach((modelName) => { | ||
| paths.push({ slug: [...resourcePath, "schemas", modelName] }); | ||
| }); | ||
| } | ||
|
|
||
| // Subresource paths | ||
| if (resource.subresources) { | ||
| Object.entries(resource.subresources).forEach( | ||
| ([subName, subResource]) => { | ||
| addResourcePaths(subName, subResource, resourcePath); | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| RESOURCE_ORDER.forEach((resourceName) => { | ||
| const resource = stainless.resources[resourceName]; | ||
| if (resource) { | ||
| addResourcePaths(resourceName, resource); | ||
| } | ||
| }); | ||
|
|
||
| return paths; | ||
| } | ||
|
|
||
| export default async function ApiReferencePage() { | ||
| const stainless = await getStainlessSpec("api"); | ||
| const schemaReferences = await buildSchemaReferences("api", "/api-reference"); | ||
| const baseUrl = stainless.environments.production; | ||
|
|
||
| // Transform sidebar content for compatibility | ||
| const sidebarContent = await getSidebarContent( | ||
| "api", | ||
| RESOURCE_ORDER, | ||
| "/api-reference", | ||
| API_REFERENCE_OVERVIEW_CONTENT.map((section) => ({ | ||
| ...section, | ||
| title: section.title || "", | ||
| })), | ||
| ); | ||
|
|
||
| // Transform to SidebarSection format | ||
| const transformedSidebar = sidebarContent.map((section) => ({ | ||
| title: section.title, | ||
| slug: section.slug, | ||
| pages: section.pages || [], | ||
| sidebarMenuDefaultOpen: | ||
| API_REFERENCE_OVERVIEW_CONTENT.find((s) => s.slug === section.slug) | ||
| ?.sidebarMenuDefaultOpen ?? false, | ||
| })); | ||
|
|
||
| return ( | ||
| <PageShell | ||
| sidebarContent={transformedSidebar} | ||
| title="API reference" | ||
| description="Complete reference documentation for the Knock API." | ||
| > | ||
| <Suspense fallback={<ResourceSectionSkeleton />}> | ||
| <PreContent specName="api" /> | ||
| </Suspense> | ||
|
|
||
| {RESOURCE_ORDER.map((resourceName) => ( | ||
| <Suspense key={resourceName} fallback={<ResourceSectionSkeleton />}> | ||
| <ResourceSection | ||
| specName="api" | ||
| resourceName={resourceName} | ||
| basePath="/api-reference" | ||
| baseUrl={baseUrl} | ||
| schemaReferences={schemaReferences} | ||
| /> | ||
| </Suspense> | ||
| ))} | ||
| </PageShell> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| "use client"; | ||
|
|
||
| import { Box, Stack } from "@telegraph/layout"; | ||
| import { Text, Heading } from "@telegraph/typography"; | ||
| import Link from "next/link"; | ||
| import { SectionHeadingAppRouter } from "./section-heading"; | ||
| import { ContentActionsAppRouter } from "./content-actions"; | ||
|
|
||
| export const Section = ({ | ||
| title, | ||
| children, | ||
| isIdempotent = false, | ||
| isRetentionSubject = false, | ||
| path, | ||
| mdPath, | ||
| }: { | ||
| title?: string; | ||
| children: React.ReactNode; | ||
| isIdempotent?: boolean; | ||
| isRetentionSubject?: boolean; | ||
| path?: string; | ||
| mdPath?: string; | ||
| }) => { | ||
| const handleRetentionClick = (e: React.MouseEvent<HTMLAnchorElement>) => { | ||
| e.preventDefault(); | ||
| const element = document.querySelector( | ||
| '[data-resource-path="/overview/data-retention"]', | ||
| ); | ||
| if (element) { | ||
| element.scrollIntoView({ behavior: "smooth" }); | ||
| } else { | ||
| window.location.href = "/api-reference/overview/data-retention"; | ||
| } | ||
| }; | ||
|
|
||
| const handleIdempotentClick = (e: React.MouseEvent<HTMLAnchorElement>) => { | ||
| e.preventDefault(); | ||
| const element = document.querySelector( | ||
| '[data-resource-path="/overview/idempotent-requests"]', | ||
| ); | ||
| if (element) { | ||
| element.scrollIntoView({ behavior: "smooth" }); | ||
| } else { | ||
| window.location.href = "/api-reference/overview/idempotent-requests"; | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Box | ||
| as="section" | ||
| borderBottom="px" | ||
| borderColor="gray-3" | ||
| py="16" | ||
| data-resource-path={path} | ||
| > | ||
| {title && ( | ||
| <> | ||
| {isIdempotent && ( | ||
| <Box mb="2"> | ||
| <Text | ||
| as="span" | ||
| size="0" | ||
| bg="yellow-3" | ||
| py="1" | ||
| px="3" | ||
| color="black" | ||
| borderRadius="2" | ||
| > | ||
| <Link | ||
| href="/api-reference/overview/idempotent-requests" | ||
| onClick={handleIdempotentClick} | ||
| style={{ color: "inherit", textDecoration: "none" }} | ||
| > | ||
| Idempotent | ||
| </Link> | ||
| </Text> | ||
| </Box> | ||
| )} | ||
| {isRetentionSubject && ( | ||
| <Box mb="2"> | ||
| <Text | ||
| as="span" | ||
| size="0" | ||
| bg="yellow-3" | ||
| py="1" | ||
| px="3" | ||
| color="black" | ||
| borderRadius="2" | ||
| > | ||
| <Link | ||
| href="/api-reference/overview/data-retention" | ||
| onClick={handleRetentionClick} | ||
| style={{ color: "inherit", textDecoration: "none" }} | ||
| > | ||
| Retention policy applied | ||
| </Link> | ||
| </Text> | ||
| </Box> | ||
| )} | ||
| <Stack | ||
| direction="row" | ||
| alignItems="center" | ||
| justifyContent="space-between" | ||
| mb="6" | ||
| > | ||
| <SectionHeadingAppRouter tag="h2" path={path}> | ||
| {title} | ||
| </SectionHeadingAppRouter> | ||
| {mdPath && <ContentActionsAppRouter mdPath={mdPath} />} | ||
| </Stack> | ||
| </> | ||
| )} | ||
| <Stack | ||
| w="full" | ||
| style={{ | ||
| display: "grid", | ||
| gridTemplateColumns: "1fr 1fr", | ||
| justifyItems: "flex-start", | ||
| }} | ||
| className="md-one-column" | ||
| overflow="hidden" | ||
| > | ||
| {children} | ||
| </Stack> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export const ContentColumn = ({ children }: { children: React.ReactNode }) => ( | ||
| <Stack pr="3" w="full"> | ||
| <Box w="full">{children}</Box> | ||
| </Stack> | ||
| ); | ||
|
|
||
| export const ExampleColumn = ({ children }: { children: React.ReactNode }) => ( | ||
| <Box position="relative" minW="0" w="full" data-example-column> | ||
| <Stack | ||
| mt="5" | ||
| pl="5" | ||
| flexDirection="column" | ||
| flexWrap="wrap" | ||
| w="full" | ||
| gap="5" | ||
| position="sticky" | ||
| style={{ top: "100px" }} | ||
| className="md-no-left-padding" | ||
| > | ||
| {children} | ||
| </Stack> | ||
| </Box> | ||
| ); | ||
|
|
||
| export const ErrorExample = ({ | ||
| title, | ||
| description, | ||
| }: { | ||
| title: string; | ||
| description: React.ReactNode; | ||
| }) => ( | ||
| <div | ||
| data-error-example | ||
| className="flex-col pt-6 mt-6 border-gray-200 border-t dark:border-gray-700" | ||
| > | ||
| <span className="bg-code-background dark:bg-gray-800 text-code rounded text-sm font-normal py-0.75 px-1.5 font-mono inline-block"> | ||
| {title} | ||
| </span> | ||
| <span className="block pt-0 mt-1 text-gray-800 dark:text-gray-200 text-sm"> | ||
| {description} | ||
| </span> | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| "use client"; | ||
|
|
||
| import { useRef, useState } from "react"; | ||
| import { Button } from "@telegraph/button"; | ||
| import { Stack } from "@telegraph/layout"; | ||
| import { Icon } from "@telegraph/icon"; | ||
| import { Check, Link2 } from "lucide-react"; | ||
|
|
||
| interface ContentActionsProps { | ||
| mdPath?: string; | ||
| showOnMobile?: boolean; | ||
| style?: React.CSSProperties; | ||
| } | ||
|
|
||
| export function ContentActionsAppRouter({ | ||
| mdPath, | ||
| showOnMobile = false, | ||
| style, | ||
| }: ContentActionsProps) { | ||
| const [copied, setCopied] = useState(false); | ||
| const timeoutRef = useRef<NodeJS.Timeout | null>(null); | ||
|
|
||
| const handleCopyLink = async () => { | ||
| const url = window.location.href; | ||
| await navigator.clipboard.writeText(url); | ||
| setCopied(true); | ||
| if (timeoutRef.current) { | ||
| clearTimeout(timeoutRef.current); | ||
| } | ||
| timeoutRef.current = setTimeout(() => { | ||
| setCopied(false); | ||
| }, 2000); | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing timeout cleanup causes memory leak on unmountLow Severity The |
||
|
|
||
| return ( | ||
| <Stack | ||
| direction="row" | ||
| gap="1" | ||
| className={showOnMobile ? "" : "hidden md:flex"} | ||
| style={style} | ||
| > | ||
| <Button.Root | ||
| variant="ghost" | ||
| size="0" | ||
| onClick={handleCopyLink} | ||
| aria-label="Copy link" | ||
| > | ||
| <Icon | ||
| icon={copied ? Check : Link2} | ||
| size="2" | ||
| color={copied ? "green" : "gray"} | ||
| aria-hidden | ||
| /> | ||
| </Button.Root> | ||
| </Stack> | ||
| ); | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused
mdPathprop inContentActionsAppRouterLow Severity
The
mdPathprop is declared in the interface and destructured in the component but never used in the function body. Either remove it or implement the intended functionality (e.g., edit on GitHub link).