From d2621183e24fdd5ae6a15fd1f1bbaffdd9e34530 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:04:45 -0400 Subject: [PATCH] refactor(docs): move components out of app (#25819) --- apps/docs/features/auth/auth.client.tsx | 60 ++++ .../docs/features/data/queryClient.client.tsx | 15 + apps/docs/features/envs/staging.client.tsx | 31 +++ .../features/telemetry/telemetry.client.tsx | 52 ++++ apps/docs/features/ui/helpers.constants.ts | 3 + .../features/ui/helpers.scroll.client.tsx | 34 +++ apps/docs/features/ui/theme.client.tsx | 11 + apps/docs/layouts/MainSkeleton.tsx | 7 +- apps/docs/lib/constants.ts | 9 +- apps/docs/pages/_app.tsx | 260 ++---------------- apps/docs/pages/index.mdx | 9 +- 11 files changed, 245 insertions(+), 246 deletions(-) create mode 100644 apps/docs/features/auth/auth.client.tsx create mode 100644 apps/docs/features/data/queryClient.client.tsx create mode 100644 apps/docs/features/envs/staging.client.tsx create mode 100644 apps/docs/features/telemetry/telemetry.client.tsx create mode 100644 apps/docs/features/ui/helpers.constants.ts create mode 100644 apps/docs/features/ui/helpers.scroll.client.tsx create mode 100644 apps/docs/features/ui/theme.client.tsx diff --git a/apps/docs/features/auth/auth.client.tsx b/apps/docs/features/auth/auth.client.tsx new file mode 100644 index 00000000000..21f35db3aca --- /dev/null +++ b/apps/docs/features/auth/auth.client.tsx @@ -0,0 +1,60 @@ +'use client' + +import { SessionContextProvider } from '@supabase/auth-helpers-react' +import { createClient } from '@supabase/supabase-js' +import { useQueryClient } from '@tanstack/react-query' +import { AuthProvider, useConstant } from 'common' +import { type PropsWithChildren, useCallback } from 'react' +import { IS_PLATFORM } from '~/lib/constants' +import { LOCAL_STORAGE_KEYS, remove } from '~/lib/storage' +import { useOnLogout } from '~/lib/userAuth' + +const AuthContainerInternal = ({ children }: PropsWithChildren) => { + const supabase = useConstant(() => + IS_PLATFORM + ? createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) + : undefined + ) + + return IS_PLATFORM ? ( + + {children} + + ) : ( + {children} + ) +} + +/** + * + * !!! IMPORTANT !!! + * Ensure data is cleared on sign out. + * + */ +const SignOutHandler = ({ children }: PropsWithChildren) => { + const queryClient = useQueryClient() + + const cleanUp = useCallback(() => { + queryClient.cancelQueries() + queryClient.clear() + + Object.keys(LOCAL_STORAGE_KEYS).forEach((key) => { + remove('local', LOCAL_STORAGE_KEYS[key]) + }) + }, [queryClient]) + + useOnLogout(cleanUp) + + return <>{children} +} + +const AuthContainer = ({ children }: PropsWithChildren) => ( + + {children} + +) + +export { AuthContainer } diff --git a/apps/docs/features/data/queryClient.client.tsx b/apps/docs/features/data/queryClient.client.tsx new file mode 100644 index 00000000000..a346e9117b5 --- /dev/null +++ b/apps/docs/features/data/queryClient.client.tsx @@ -0,0 +1,15 @@ +'use client' + +import { QueryClientProvider as QueryClientProviderPrimitive } from '@tanstack/react-query' +import { type PropsWithChildren } from 'react' +import { useRootQueryClient } from '~/lib/fetch/queryClient' + +const QueryClientProvider = ({ children }: PropsWithChildren) => { + const queryClient = useRootQueryClient() + + return ( + {children} + ) +} + +export { QueryClientProvider } diff --git a/apps/docs/features/envs/staging.client.tsx b/apps/docs/features/envs/staging.client.tsx new file mode 100644 index 00000000000..43583cdbf2d --- /dev/null +++ b/apps/docs/features/envs/staging.client.tsx @@ -0,0 +1,31 @@ +'use client' + +import { type PropsWithChildren, useEffect, useState } from 'react' +import { BUILD_PREVIEW_HTML, IS_PREVIEW } from '~/lib/constants' + +/** + * Preview builds don't need to be statically generated to optimize performance. + * This (somewhat hacky) way of shortcutting preview builds cuts their build + * time and speeds up the feedback loop for previewing docs changes in Vercel. + * + * This technically breaks the Rules of Hooks to avoid an unnecessary full-app + * rerender in prod, but this is fine because IS_PREVIEW will never change on + * you within a single build. + */ +const ShortcutPreviewBuild = ({ children }: PropsWithChildren) => { + if (!BUILD_PREVIEW_HTML && IS_PREVIEW) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [isMounted, setIsMounted] = useState(false) + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setIsMounted(true) + }, []) + + return isMounted ? children : null + } + + return children +} + +export { ShortcutPreviewBuild } diff --git a/apps/docs/features/telemetry/telemetry.client.tsx b/apps/docs/features/telemetry/telemetry.client.tsx new file mode 100644 index 00000000000..527328e09cc --- /dev/null +++ b/apps/docs/features/telemetry/telemetry.client.tsx @@ -0,0 +1,52 @@ +'use client' + +import { useTelemetryProps } from 'common' +import { usePathname } from 'next/navigation' +import { useCallback, useEffect } from 'react' +import { useConsent } from 'ui-patterns/ConsentToast' +import { BASE_PATH, IS_PLATFORM } from '~/lib/constants' +import { unauthedAllowedPost } from '~/lib/fetch/fetchWrappers' + +const useSendPageTelemetryWithConsent = () => { + const { hasAcceptedConsent } = useConsent() + const telemetryProps = useTelemetryProps() + + const sendPageTelemetry = useCallback( + (route: string) => { + if (!(IS_PLATFORM && hasAcceptedConsent)) return + + unauthedAllowedPost('/platform/telemetry/page', { + body: { + referrer: document.referrer, + title: document.title, + route: `${BASE_PATH}${route}`, + ga: { + screen_resolution: telemetryProps?.screenResolution, + language: telemetryProps?.language, + session_id: '', + }, + }, + }).catch((e) => { + console.error('Problem sending telemetry:', e) + }) + }, + [telemetryProps, hasAcceptedConsent] + ) + + return sendPageTelemetry +} + +const PageTelemetry = () => { + const pathname = usePathname() + const sendPageTelemetry = useSendPageTelemetryWithConsent() + + useEffect(() => { + if (pathname) { + sendPageTelemetry(pathname) + } + }, [pathname, sendPageTelemetry]) + + return null +} + +export { PageTelemetry } diff --git a/apps/docs/features/ui/helpers.constants.ts b/apps/docs/features/ui/helpers.constants.ts new file mode 100644 index 00000000000..dbf22cc6205 --- /dev/null +++ b/apps/docs/features/ui/helpers.constants.ts @@ -0,0 +1,3 @@ +const DOCS_CONTENT_CONTAINER_ID = 'docs-content-container' + +export { DOCS_CONTENT_CONTAINER_ID } diff --git a/apps/docs/features/ui/helpers.scroll.client.tsx b/apps/docs/features/ui/helpers.scroll.client.tsx new file mode 100644 index 00000000000..410e922afb5 --- /dev/null +++ b/apps/docs/features/ui/helpers.scroll.client.tsx @@ -0,0 +1,34 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { useEffect } from 'react' +import { DOCS_CONTENT_CONTAINER_ID } from './helpers.constants' + +const useScrollTopOnPageChange = () => { + const pathname = usePathname() + + useEffect(() => { + if (document && pathname) { + // Don't scroll on reference pages + if (pathname.startsWith('/reference/')) return + + const container = document.getElementById(DOCS_CONTENT_CONTAINER_ID) + if (container) container.scrollTop = 0 + /** + * a11y works by default, so no need to specially handle it + */ + } + }, [pathname]) +} + +/** + * Scroll the docs content container to top on page change. Can't use Next.js's + * native scroll restoration, because we scroll the content container, not the + * document. + */ +const ScrollRestoration = () => { + useScrollTopOnPageChange() + return null +} + +export { ScrollRestoration } diff --git a/apps/docs/features/ui/theme.client.tsx b/apps/docs/features/ui/theme.client.tsx new file mode 100644 index 00000000000..7adf0813361 --- /dev/null +++ b/apps/docs/features/ui/theme.client.tsx @@ -0,0 +1,11 @@ +'use client' + +import { useThemeSandbox } from 'common' + +const ThemeSandbox = () => { + useThemeSandbox() + + return null +} + +export { ThemeSandbox } diff --git a/apps/docs/layouts/MainSkeleton.tsx b/apps/docs/layouts/MainSkeleton.tsx index 4b17c279fd7..18206d69af1 100644 --- a/apps/docs/layouts/MainSkeleton.tsx +++ b/apps/docs/layouts/MainSkeleton.tsx @@ -2,13 +2,12 @@ import { useTheme } from 'next-themes' import Image from 'next/legacy/image' import Link from 'next/link' import { type CSSProperties, type PropsWithChildren, memo, useEffect } from 'react' - import { cn } from 'ui' - import Footer from '~/components/Navigation/Footer' import HomeMenuIconPicker from '~/components/Navigation/NavigationMenu/HomeMenuIconPicker' import NavigationMenu, { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar' +import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants' import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState' const levelsData = { @@ -275,8 +274,8 @@ const Container = memo(function Container({ return (
{ - setIsMounted(true) - }, []) - - return isMounted ? children : null - } - - return children -} - -/** - * - * !!! IMPORTANT !!! - * Ensure data is cleared on sign out. - * - * **/ -function SignOutHandler({ children }: PropsWithChildren) { - const queryClient = useQueryClient() - - const cleanUp = useCallback(() => { - queryClient.cancelQueries() - queryClient.clear() - - Object.keys(LOCAL_STORAGE_KEYS).forEach((key) => { - remove('local', LOCAL_STORAGE_KEYS[key]) - }) - }, [queryClient]) - - useOnLogout(cleanUp) - - return <>{children} -} - -function AuthContainer({ children }: PropsWithChildren) { - const [supabase] = useState(() => - IS_PLATFORM - ? createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY - ) - : undefined - ) - - return IS_PLATFORM ? ( - - {children} - - ) : ( - {children} - ) -} +import type { AppPropsWithLayout } from '~/types' function MyApp({ Component, pageProps }: AppPropsWithLayout) { - const router = useRouter() - const telemetryProps = useTelemetryProps() - const { consentValue, hasAcceptedConsent } = useConsent() - const queryClient = useRootQueryClient() - - useThemeSandbox() - - const handlePageTelemetry = useCallback( - (route: string) => { - if (IS_PLATFORM) { - unauthedAllowedPost('/platform/telemetry/page', { - body: { - referrer: document.referrer, - title: document.title, - route, - ga: { - screen_resolution: telemetryProps?.screenResolution, - language: telemetryProps?.language, - session_id: '', - }, - }, - }).catch((e) => { - console.error('Problem sending telemetry:', e) - }) - } - }, - [telemetryProps] - ) - - useEffect(() => { - function handleRouteChange(url: string) { - /* - * handle telemetry - */ - if (hasAcceptedConsent) handlePageTelemetry(url) - /* - * handle "scroll to top" behaviour on route change - */ - if (document) { - // do not scroll to top for reference docs - if (!url.includes('reference/')) { - // scroll container div to top - const container = document.getElementById('docs-content-container') - // check container exists (only avail on new docs) - if (container) container.scrollTop = 0 - } - } - } - - // Listen for page changes after a navigation or when the query changes - router.events.on('routeChangeComplete', handleRouteChange) - return () => { - router.events.off('routeChangeComplete', handleRouteChange) - } - }, [router, handlePageTelemetry, consentValue, hasAcceptedConsent]) - - /** - * Save/restore scroll position when reloading or navigating back/forward. - * - * Required since scroll happens within a sub-container, not the page root. - */ - useEffect(() => { - const storageKey = 'scroll-position' - - const container = document.getElementById('docs-content-container') - if (!container) { - return - } - - const previousScroll = Number(sessionStorage.getItem(storageKey)) - const [entry] = window.performance.getEntriesByType('navigation') - - // Only restore scroll position on reload and back/forward events - if ( - previousScroll && - entry && - isPerformanceNavigationTiming(entry) && - ['reload', 'back_forward'].includes(entry.type) - ) { - container.scrollTop = previousScroll - } - - const handler = () => { - // Scroll stored in session storage, so only persisted per tab - sessionStorage.setItem(storageKey, container.scrollTop.toString()) - } - - window.addEventListener('beforeunload', handler) - - return () => window.removeEventListener('beforeunload', handler) - }, [router]) - - useEffect(() => { - if (!hasAcceptedConsent) return - - /** - * Send page telemetry on first page load - */ - if (router.isReady) { - handlePageTelemetry(router.basePath + router.asPath) - } - }, [router, handlePageTelemetry, consentValue, hasAcceptedConsent]) - - /** - * Reference docs use `history.pushState()` to jump to - * sub-sections without causing a re-render. - * - * We need to the below handler to manually force a re-render - * when navigating away from, then back to reference docs - */ - useEffect(() => { - function handler() { - router.replace(window.location.href) - } - - window.addEventListener('popstate', handler) - - return () => { - window.removeEventListener('popstate', handler) - } - }, [router]) - return ( - - - - - + + + + + - - - -
- - - - - -
-
-
-
+ + + + +
+ + + + + + +
+
+
) } -/** - * Type guard that checks if a performance entry is a - * `PerformanceNavigationTiming`. - */ -function isPerformanceNavigationTiming( - entry: PerformanceEntry -): entry is PerformanceNavigationTiming { - return entry.entryType === 'navigation' -} - export default MyApp diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index a26bd85cb18..5b912d11238 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -86,10 +86,9 @@ export const meta = {