From 575efb07f4533105200e75bef8562c0894b6f85d Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 30 Jul 2024 02:40:33 -0700 Subject: [PATCH] NextJS page router layout function - faster, lower flicker. --- pages/_app.tsx | 13 ++++++--- pages/call.tsx | 6 ++--- pages/dev/beam.tsx | 6 ++--- pages/diff.tsx | 6 ++--- pages/draw.tsx | 6 ++--- pages/index.tsx | 11 +++++--- pages/info/debug.tsx | 6 ++--- pages/link/callback_openrouter.tsx | 9 ++++--- pages/link/chat/[chatLinkId].tsx | 9 ++++--- pages/link/share_target.tsx | 6 ++--- pages/news.tsx | 9 ++++--- pages/personas.tsx | 6 ++--- pages/tokens.tsx | 6 ++--- pages/workspace.tsx | 10 +++---- src/common/layout/withLayout.tsx | 42 +++++++++++++++++++++++++----- src/common/types/next.page.d.ts | 15 ++++++----- 16 files changed, 95 insertions(+), 71 deletions(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index 78e36f7fa..e7bcfd312 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -24,8 +24,14 @@ import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components import { isVercelFromFrontend } from '~/common/util/pwaUtils'; -const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => - <> +const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => { + + // We are using a nextjs per-page layout pattern to bring the (Optima) layout creation to a shared place + // This reduces the flicker and the time switching between apps, and seems to not have impact on + // the build. This is a good trade-off for now. + const getLayout = Component.getLayout ?? ((page: any) => page); + + return <> {Brand.Title.Common} @@ -39,7 +45,7 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => {/* ^ SSR boundary */} - + {getLayout()} @@ -52,6 +58,7 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => {hasGoogleAnalytics && } ; +}; // enables the React Query API invocation export default apiQuery.withTRPC(MyApp); \ No newline at end of file diff --git a/pages/call.tsx b/pages/call.tsx index 2e56d3ac1..47880c561 100644 --- a/pages/call.tsx +++ b/pages/call.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppCall } from '../src/apps/call/AppCall'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function CallPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/dev/beam.tsx b/pages/dev/beam.tsx index dab3ee05c..f9798464f 100644 --- a/pages/dev/beam.tsx +++ b/pages/dev/beam.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppBeam } from '../../src/apps/beam/AppBeam'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function BeamPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/diff.tsx b/pages/diff.tsx index a406e2b76..bd86c316f 100644 --- a/pages/diff.tsx +++ b/pages/diff.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppDiff } from '../src/apps/diff/AppDiff'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function DiffPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/draw.tsx b/pages/draw.tsx index 8f6642f9e..601dbd3cb 100644 --- a/pages/draw.tsx +++ b/pages/draw.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppDraw } from '../src/apps/draw/AppDraw'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function DrawPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/index.tsx b/pages/index.tsx index 68a906ca1..fc8ccb9cb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -2,13 +2,16 @@ import * as React from 'react'; import { AppChat } from '../src/apps/chat/AppChat'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; +import { useDebugHook } from '~/common/components/useDebugHook'; -export default function IndexPage() { +export default withNextJSPerPageLayout({ type: 'optima' }, () => { + + useDebugHook('IndexPage'); // TODO: This Index page will point to the Dashboard (or a landing page) // For now it offers the chat experience, but this will change. #299 - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file + return ; +}); diff --git a/pages/info/debug.tsx b/pages/info/debug.tsx index a822e7b1f..a9959ece6 100644 --- a/pages/info/debug.tsx +++ b/pages/info/debug.tsx @@ -9,7 +9,7 @@ import { AppPlaceholder } from '../../src/apps/AppPlaceholder'; import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; import { getPlantUmlServerUrl } from '~/modules/blocks/code/RenderCode'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; // app config @@ -164,6 +164,4 @@ function AppDebug() { } -export default function DebugPage() { - return withLayout({ type: 'plain' }, ); -}; \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'plain' }, () => ); diff --git a/pages/link/callback_openrouter.tsx b/pages/link/callback_openrouter.tsx index 08706a90e..a73e351ca 100644 --- a/pages/link/callback_openrouter.tsx +++ b/pages/link/callback_openrouter.tsx @@ -7,7 +7,7 @@ import { useModelsStore } from '~/modules/llms/store-llms'; import { InlineError } from '~/common/components/InlineError'; import { apiQuery } from '~/common/util/trpc.client'; import { navigateToIndex, useRouterQuery } from '~/common/app.routes'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) { @@ -81,10 +81,11 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) { * Docs: https://openrouter.ai/docs#oauth * Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode */ -export default function CallbackPage() { +export default withNextJSPerPageLayout({ type: 'plain' }, () => { // external state - get the 'code=...' from the URL const { code } = useRouterQuery<{ code: string | undefined }>(); - return withLayout({ type: 'plain' }, ); -} \ No newline at end of file + return ; + +}); diff --git a/pages/link/chat/[chatLinkId].tsx b/pages/link/chat/[chatLinkId].tsx index dfaa50e60..f086bfc22 100644 --- a/pages/link/chat/[chatLinkId].tsx +++ b/pages/link/chat/[chatLinkId].tsx @@ -3,13 +3,14 @@ import * as React from 'react'; import { AppLinkChat } from '../../../src/apps/link-chat/AppLinkChat'; import { useRouterQuery } from '~/common/app.routes'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function ChatLinkPage() { +export default withNextJSPerPageLayout({ type: 'optima', suspendAutoModelsSetup: true }, () => { // external state const { chatLinkId } = useRouterQuery<{ chatLinkId: string | undefined }>(); - return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, ); -} \ No newline at end of file + return ; + +}); \ No newline at end of file diff --git a/pages/link/share_target.tsx b/pages/link/share_target.tsx index be29c7e65..6543024f5 100644 --- a/pages/link/share_target.tsx +++ b/pages/link/share_target.tsx @@ -10,7 +10,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client'; import { LogoProgress } from '~/common/components/LogoProgress'; import { asValidURL } from '~/common/util/urlUtils'; import { navigateToIndex, useRouterQuery } from '~/common/app.routes'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; /** @@ -135,6 +135,4 @@ function AppShareTarget() { * This page will be invoked on mobile when sharing Text/URLs/Files from other APPs * Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath */ -export default function ShareTargetPage() { - return withLayout({ type: 'plain' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'plain' }, () => ); diff --git a/pages/news.tsx b/pages/news.tsx index e64df9359..96b08dfee 100644 --- a/pages/news.tsx +++ b/pages/news.tsx @@ -3,12 +3,13 @@ import * as React from 'react'; import { AppNews } from '../src/apps/news/AppNews'; import { markNewsAsSeen } from '../src/apps/news/news.version'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function NewsPage() { +export default withNextJSPerPageLayout({ type: 'optima', suspendAutoModelsSetup: true }, () => { + // 'touch' the last seen news version React.useEffect(() => markNewsAsSeen(), []); - return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, ); -} \ No newline at end of file + return ; +}); \ No newline at end of file diff --git a/pages/personas.tsx b/pages/personas.tsx index 551e73ed7..eb1d76f23 100644 --- a/pages/personas.tsx +++ b/pages/personas.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppPersonas } from '../src/apps/personas/AppPersonas'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function PersonasPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/tokens.tsx b/pages/tokens.tsx index 8d7d20a13..3ecf25efc 100644 --- a/pages/tokens.tsx +++ b/pages/tokens.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { AppTokens } from '../src/apps/tokens/AppTokens'; -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function TokenizerPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/pages/workspace.tsx b/pages/workspace.tsx index dc0cde643..7ecef59eb 100644 --- a/pages/workspace.tsx +++ b/pages/workspace.tsx @@ -1,12 +1,8 @@ import * as React from 'react'; -import { Box } from '@mui/joy'; +import { AppPlaceholder } from '../src/apps/AppPlaceholder'; -// import { AppWorkspace } from '../src/apps/personas/AppWorkspace'; - -import { withLayout } from '~/common/layout/withLayout'; +import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; -export default function PersonasPage() { - return withLayout({ type: 'optima' }, ); -} \ No newline at end of file +export default withNextJSPerPageLayout({ type: 'optima' }, () => ); diff --git a/src/common/layout/withLayout.tsx b/src/common/layout/withLayout.tsx index 8dd133595..959fc82c5 100644 --- a/src/common/layout/withLayout.tsx +++ b/src/common/layout/withLayout.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; +import type { NextPageWithLayout } from '~/common/types/next.page'; + import { OptimaLayout } from './optima/OptimaLayout'; import { PlainLayout } from './plain/PlainLayout'; -type WithLayout = { +type PerPageLayoutOptions = { type: 'optima'; suspendAutoModelsSetup?: boolean; } | { @@ -13,23 +15,49 @@ type WithLayout = { /** - * Dynamic page-level layouting: a wrapper that adds the layout around the children. + * Next.js page-level layouting: a wrapper that adds the layout around the page as a layout function. */ -export function withLayout(layoutOptions: WithLayout, children: React.ReactNode): React.ReactElement { +export function withNextJSPerPageLayout(options: PerPageLayoutOptions, page: NextPageWithLayout): NextPageWithLayout { - const { type, ...rest } = layoutOptions; + const { type, ...rest } = options; switch (type) { case 'optima': - return {children}; + page.getLayout = (page: React.ReactElement) => {page}; + return page; case 'plain': - return {children}; + page.getLayout = (page: React.ReactElement) => {page}; + return page; default: console.error('No layout specified for this top-level page'); - return <>{children}; + return page; } } + + +// /** +// * Dynamic page-level layouting: a wrapper that adds the layout around the children. +// */ +// export function withLayout(layoutOptions: LayoutOptions, children: React.ReactNode): React.ReactElement { +// +// const { type, ...rest } = layoutOptions; +// +// switch (type) { +// +// case 'optima': +// return {children}; +// +// case 'plain': +// return {children}; +// +// default: +// console.error('No layout specified for this top-level page'); +// return <>{children}; +// +// } +// } +// diff --git a/src/common/types/next.page.d.ts b/src/common/types/next.page.d.ts index 5d21920ab..e7fac3e1e 100644 --- a/src/common/types/next.page.d.ts +++ b/src/common/types/next.page.d.ts @@ -1,17 +1,20 @@ +import type { ReactElement, ReactNode } from 'react'; +import type { NextPage } from 'next'; import type { EmotionCache } from '@emotion/react'; -// export type NextPageWithLayout

= NextPage & { -// // require .layoutOptions on the page component -// layoutOptions: LayoutOptions; -// }; +export type NextPageWithLayout

= NextPage & { + // definition of the per-page layout function, as per: + // https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#per-page-layouts + getLayout?: (page: ReactElement) => ReactNode; +} // Extend the AppProps type with the custom page component type declare module 'next/app' { import { AppProps } from 'next/app'; type MyAppProps = AppProps & { - // Component: NextPageWithLayout; + Component: NextPageWithLayout emotionCache?: EmotionCache; }; -} \ No newline at end of file +}