From 85dacd5354fef40a47282f7f6709cc2a0aec907b Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Tue, 2 Apr 2024 13:59:09 -0400 Subject: [PATCH] refactor --- .../api-page/endpoints/EndpointContent.tsx | 299 ++++++------------ .../docs-context/DocsContextProvider.tsx | 209 ++++++++---- packages/ui/app/src/hooks/useViewportSize.ts | 9 +- packages/ui/app/src/next-app/DocsPage.tsx | 131 +------- 4 files changed, 261 insertions(+), 387 deletions(-) diff --git a/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx b/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx index 316551e453..282e2d71bf 100644 --- a/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx +++ b/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx @@ -2,11 +2,11 @@ import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; import cn from "clsx"; import { useAtom } from "jotai"; import dynamic from "next/dynamic"; -import { FC, forwardRef, memo, useCallback, useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/router"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { withStream } from "../../commons/withStream"; import { useDocsContext } from "../../contexts/docs-context/useDocsContext"; -import { useSlugListener } from "../../contexts/useSlugListener"; import { useViewportContext } from "../../contexts/viewport-context/useViewportContext"; import { useViewportSize } from "../../hooks/useViewportSize"; import { FERN_LANGUAGE_ATOM } from "../../sidebar/atom"; @@ -14,7 +14,7 @@ import { ResolvedEndpointDefinition, ResolvedError, ResolvedTypeDefinition } fro import { ApiPageDescription } from "../ApiPageDescription"; import { Breadcrumbs } from "../Breadcrumbs"; import { JsonPropertyPath } from "../examples/JsonPropertyPath"; -import { CodeExample, CodeExampleGroup, generateCodeExamples } from "../examples/code-example"; +import { CodeExample, generateCodeExamples } from "../examples/code-example"; import { EndpointAvailabilityTag } from "./EndpointAvailabilityTag"; import { EndpointContentLeft, convertNameToAnchorPart } from "./EndpointContentLeft"; import { EndpointUrlWithOverflow } from "./EndpointUrlWithOverflow"; @@ -64,7 +64,7 @@ function maybeGetErrorStatusCodeOrNameFromAnchor(anchor: string | undefined): nu return undefined; } -const UnmemoizedEndpointContent: FC = ({ +export const EndpointContent: React.FC = ({ api, showErrors, endpoint, @@ -74,11 +74,11 @@ const UnmemoizedEndpointContent: FC = ({ isInViewport: initiallyInViewport, types, }) => { + const router = useRouter(); const { layout } = useDocsContext(); const { layoutBreakpoint } = useViewportContext(); const viewportSize = useViewportSize(); const [isInViewport, setIsInViewport] = useState(initiallyInViewport); - const { ref: containerRef } = useInView({ onChange: setIsInViewport, rootMargin: "100%", @@ -100,8 +100,8 @@ const UnmemoizedEndpointContent: FC = ({ const [selectedError, setSelectedError] = useState(); - useSlugListener(endpoint.slug.join("/"), (anchor) => { - const statusCodeOrName = maybeGetErrorStatusCodeOrNameFromAnchor(anchor); + useEffect(() => { + const statusCodeOrName = maybeGetErrorStatusCodeOrNameFromAnchor(router.asPath.split("#")[1]); if (statusCodeOrName != null) { const error = endpoint.errors.find((e) => typeof statusCodeOrName === "number" @@ -112,7 +112,8 @@ const UnmemoizedEndpointContent: FC = ({ setSelectedError(error); } } - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const examples = useMemo(() => { if (selectedError == null) { @@ -208,203 +209,105 @@ const UnmemoizedEndpointContent: FC = ({ requestHeight + responseHeight + (responseHeight > 0 && requestHeight > 0 ? GAP_6 : 0) + padding; return ( - setSelectedError(undefined)} ref={containerRef} - api={api} - showErrors={showErrors} - endpoint={endpoint} - breadcrumbs={breadcrumbs} - hideBottomSeparator={hideBottomSeparator} - setContainerRef={setContainerRef} - isInViewport={isInViewport} - types={types} - setSelectedError={setSelectedError} - hoveredRequestPropertyPath={hoveredRequestPropertyPath} - hoveredResponsePropertyPath={hoveredResponsePropertyPath} - onHoverRequestProperty={onHoverRequestProperty} - onHoverResponseProperty={onHoverResponseProperty} - selectedError={selectedError} - contentType={contentType} - setContentType={setContentType} - clients={clients} - selectedClient={selectedClient} - setSelectedExampleClientAndScrollToTop={setSelectedExampleClientAndScrollToTop} - requestJson={requestJson} - responseJson={responseJson} - responseCodeSnippet={responseCodeSnippet} - requestHeight={requestHeight} - responseHeight={responseHeight} - exampleHeight={exampleHeight} - /> - ); -}; - -export const EndpointContent = memo(UnmemoizedEndpointContent); - -interface EndpointContentMemoizedProps { - api: string; - showErrors: boolean; - endpoint: ResolvedEndpointDefinition; - breadcrumbs: readonly string[]; - hideBottomSeparator?: boolean; - setContainerRef: (ref: HTMLElement | null) => void; - isInViewport: boolean; - types: Record; - setSelectedError: (error: ResolvedError | undefined) => void; - hoveredRequestPropertyPath?: JsonPropertyPath; - hoveredResponsePropertyPath?: JsonPropertyPath; - onHoverRequestProperty: (jsonPropertyPath: JsonPropertyPath, { isHovering }: { isHovering: boolean }) => void; - onHoverResponseProperty: (jsonPropertyPath: JsonPropertyPath, { isHovering }: { isHovering: boolean }) => void; - selectedError?: ResolvedError; - contentType: string | undefined; - setContentType: (contentType: string | undefined) => void; - clients: CodeExampleGroup[]; - selectedClient: CodeExample; - setSelectedExampleClientAndScrollToTop: (nextClient: CodeExample) => void; - requestJson?: unknown; - responseJson?: unknown; - responseCodeSnippet: string; - requestHeight: number; - responseHeight: number; - exampleHeight: number; -} - -const EndpointContentMemoized = memo( - forwardRef((props, ref) => { - const { layoutBreakpoint } = useViewportContext(); - const { - api, - showErrors, - endpoint, - breadcrumbs, - hideBottomSeparator, - setContainerRef, - isInViewport, - types, - setSelectedError, - hoveredRequestPropertyPath, - hoveredResponsePropertyPath, - onHoverRequestProperty, - onHoverResponseProperty, - selectedError, - contentType, - setContentType, - clients, - selectedClient, - setSelectedExampleClientAndScrollToTop, - requestJson, - responseJson, - responseCodeSnippet, - requestHeight, - responseHeight, - exampleHeight, - } = props; - - return ( + >
setSelectedError(undefined)} - ref={ref} + className={cn("scroll-mt-header-height max-w-content-width md:max-w-endpoint-width mx-auto", { + "border-default border-b mb-px pb-20": !hideBottomSeparator, + })} + ref={setContainerRef} + data-route={`/${endpoint.slug.join("/")}`} > -
-
- -
- {endpoint.responseBody?.shape.type === "stream" ? ( - withStream(

{endpoint.title}

) - ) : ( -

{endpoint.title}

- )} - {endpoint.availability != null && ( - - - - )} -
- +
+ +
+ {endpoint.responseBody?.shape.type === "stream" ? ( + withStream(

{endpoint.title}

) + ) : ( +

{endpoint.title}

+ )} + {endpoint.availability != null && ( + + + + )}
-
-
- + +
+
+
+ -
- -
+
+
+
-
- {isInViewport && ( - - )} -
+
+ {isInViewport && ( + + )}
- ); - }), -); - -EndpointContentMemoized.displayName = "EndpointContentMemoized"; +
+ ); +}; diff --git a/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx b/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx index 43e419836f..77be102109 100644 --- a/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx +++ b/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx @@ -1,35 +1,39 @@ import { DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk"; -import { ColorsConfig, SidebarNavigation } from "@fern-ui/fdr-utils"; -import { PropsWithChildren, useCallback, useMemo } from "react"; +import { useDeepCompareMemoize } from "@fern-ui/react-commons"; +import { useTheme } from "next-themes"; +import Head from "next/head"; +import Script from "next/script"; +import { PropsWithChildren, ReactNode, useCallback, useMemo } from "react"; +import { DocsPage } from "../../next-app/DocsPage"; +import { getThemeColor } from "../../next-app/utils/getColorVariables"; +import { getFontExtension } from "../../next-app/utils/getFontVariables"; +import { renderThemeStylesheet } from "../../next-app/utils/renderThemeStylesheet"; import { DocsContext } from "./DocsContext"; export declare namespace DocsContextProvider { - export type Props = PropsWithChildren< - { - files: Record; - layout: DocsV1Read.DocsLayoutConfig | undefined; - typography: DocsV1Read.DocsTypographyConfigV2 | undefined; - css: DocsV1Read.CssConfig | undefined; - colors: ColorsConfig; - baseUrl: DocsV2Read.BaseUrl; - } & SidebarNavigation - >; + export type Props = PropsWithChildren; } -export const DocsContextProvider: React.FC = ({ - baseUrl, - files, - layout, - typography, - css, - colors, - currentTabIndex, - tabs, - currentVersionIndex, - versions, - sidebarNodes, - children, -}) => { +export const DocsContextProvider: React.FC = ({ children, ...pageProps }) => { + const files = useDeepCompareMemoize(pageProps.files); + const layout = useDeepCompareMemoize(pageProps.layout); + const colors = useDeepCompareMemoize(pageProps.colors); + const typography = useDeepCompareMemoize(pageProps.typography); + const css = useDeepCompareMemoize(pageProps.css); + const js = useDeepCompareMemoize(pageProps.js); + const sidebarNodes = useDeepCompareMemoize(pageProps.navigation.sidebarNodes); + const tabs = useDeepCompareMemoize(pageProps.navigation.tabs); + const versions = useDeepCompareMemoize(pageProps.navigation.versions); + const { resolvedTheme: theme } = useTheme(); + + const { baseUrl, title, favicon } = pageProps; + const { currentTabIndex, currentVersionIndex } = pageProps.navigation; + + const stylesheet = useMemo( + () => renderThemeStylesheet(colors, typography, layout, css, files, tabs.length > 0), + [colors, css, files, layout, tabs.length, typography], + ); + const resolveFile = useCallback( (fileId: DocsV1Read.FileId): DocsV1Read.File_ | undefined => { const file = files[fileId]; @@ -41,42 +45,127 @@ export const DocsContextProvider: React.FC = ({ }, [files], ); + + const value = useMemo( + () => ({ + domain: baseUrl.domain, + basePath: baseUrl.basePath, + layout, + colors, + typography, + css, + files, + resolveFile, + currentTabIndex, + tabs, + currentVersionIndex, + versions, + sidebarNodes, + }), + [ + baseUrl.basePath, + baseUrl.domain, + colors, + css, + currentTabIndex, + currentVersionIndex, + files, + layout, + resolveFile, + sidebarNodes, + tabs, + typography, + versions, + ], + ); + return ( - ({ - domain: baseUrl.domain, - basePath: baseUrl.basePath, - layout, - colors, - typography, - css, - files, - resolveFile, - currentTabIndex, - tabs, - currentVersionIndex, - versions, - sidebarNodes, - }), - [ - baseUrl.basePath, - baseUrl.domain, - colors, - css, - currentTabIndex, - currentVersionIndex, - files, - layout, - resolveFile, - sidebarNodes, - tabs, - typography, - versions, - ], - )} - > + + + + {title != null && {title}} + {favicon != null && } + {typography?.bodyFont?.variants.map((v) => getPreloadedFont(v, files))} + {typography?.headingsFont?.variants.map((v) => getPreloadedFont(v, files))} + {typography?.codeFont?.variants.map((v) => getPreloadedFont(v, files))} + {theme === "light" && colors.light != null && ( + + )} + {theme === "dark" && colors.dark != null && ( + + )} + {maybeRenderNoIndex(baseUrl)} + + {/* + We concatenate all global styles into a single instance, + as styled JSX will only create one instance of global styles + for each component. + */} + {/* eslint-disable-next-line react/no-unknown-property */} + + {children} + + {js?.inline?.map((inline, idx) => ( + + ))} + {js?.files.map((file) => ( + - ))} - {js?.files.map((file) => ( -