'use client'; /** * Next.js adapter — bridges our framework-agnostic router hooks * to `next/navigation` so server components, route loaders, and * `prefetch` all fire correctly. * * USAGE: * Mount once near the root of the App Router tree, e.g. inside * the locale layout's client provider stack: * * ```tsx * import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs'; * * * * {children} * * * ``` * * Without this provider, our hooks fall back to the History API * adapter — which works in plain SPAs (Wails, Electron, Vite, CRA) * but in Next.js means: no server component refetch on navigation, * no route loader, no prefetch. So always mount it in Next apps. * * PEER DEPENDENCY: * `next` is an OPTIONAL peer of @djangocfg/ui-core. The base package * never imports from `next/*`. This sub-path entry does — so it only * loads when the consumer explicitly imports `/adapters/nextjs`. * Wails / Electron consumers: don't import this file and `next` is * never resolved. */ import { forwardRef, useMemo, type AnchorHTMLAttributes, type ComponentType, type ReactNode, type Ref, } from 'react'; // Use the toploader-wrapped router so that EVERY programmatic // navigation through ui-core hooks (`useNavigate`, `useQueryState`, // `useRouter` from `@djangocfg/ui-core/hooks`) starts the // `nextjs-toploader` progress bar. // // Why not `next/navigation` directly: only attaches a // document-level click listener for tags and instruments // `history.pushState` to call `nprogress.done()` (NOT `.start()`). So // `router.push()` from `next/navigation` mutates history but never // starts the bar — only `` clicks do. The `nextjs-toploader/app` // entry exposes a thin wrapper around the same `useRouter` whose // `push`/`replace` call `nprogress.start()` first. Drop-in shape. import { useRouter as useNextRouter } from 'nextjs-toploader/app'; import NextLink from 'next/link'; import { RouterAdapterProvider, type RouterAdapter, type RouterLocation, } from '../adapter'; import { LinkProvider, type LinkComponent, type LinkComponentProps, } from '../../../components/navigation/link'; const SSR_LOCATION: RouterLocation = Object.freeze({ pathname: '/', search: '', hash: '', }); export interface NextRouterAdapterProps { children: ReactNode; /** * Pass `scroll: false` to every Next router call by default. * Useful for filter/pagination apps where the page should never * jump on URL changes. Default: undefined (use Next's own default). */ defaultScroll?: boolean; } /** * Wraps a subtree so all `@djangocfg/ui-core/hooks` router calls go * through Next's App Router. Server components, loaders, and prefetch * keep working as if you used `next/navigation` directly. */ export function NextRouterAdapter({ children, defaultScroll, }: NextRouterAdapterProps) { const next = useNextRouter(); const adapter = useMemo( () => ({ push(url) { next.push(url, defaultScroll === undefined ? undefined : { scroll: defaultScroll }); }, replace(url) { next.replace(url, defaultScroll === undefined ? undefined : { scroll: defaultScroll }); }, back() { next.back(); }, forward() { next.forward(); }, getLocation() { if (typeof window === 'undefined') return SSR_LOCATION; return { pathname: window.location.pathname, search: window.location.search, hash: window.location.hash, }; }, }), [next, defaultScroll] ); return ( {children} ); } /** * Maps our agnostic Link API → next/link props. Lives at module scope so * the component identity is stable (avoids tree remounts on every render). */ const NextLinkAdapter: LinkComponent = forwardRef( function NextLinkAdapter({ href, replace, scroll, prefetch, children, ...rest }, ref) { return ( {children} ); } ); export interface NextLinkProviderProps { children: ReactNode; } /** * Wires `` from `@djangocfg/ui-core/components` to `next/link`. * Mount alongside `NextRouterAdapter` near the root of a Next app so * every Link picks up Next's prefetch / RSC handling automatically. */ export function NextLinkProvider({ children }: NextLinkProviderProps) { return {children}; } /** * Minimal shape of the `Link` returned by next-intl's `createNavigation()`. * Typed structurally so we don't take a peer dep on `next-intl`. */ type NextIntlLinkLike = ComponentType< AnchorHTMLAttributes & { href: string; locale?: string; replace?: boolean; scroll?: boolean; prefetch?: boolean | null; ref?: Ref; } >; /** * Wraps next-intl's `Link` (from `createNavigation()`) into our generic * `LinkComponent`, so apps using `next-intl` get locale-prefixed hrefs on * every `` rendered through `@djangocfg/ui-core`. * * Pass the result as `baseApp.linkAdapter` to `AppLayout` / `BaseApp` — * it overrides the default `next/link` adapter for the whole subtree. * * @example * import { createNextIntlLinkAdapter } from '@djangocfg/ui-core/adapters/nextjs'; * import { Link as IntlLink } from '@/i18n/navigation'; * * const linkAdapter = useMemo(() => createNextIntlLinkAdapter(IntlLink), []); * */ export function createNextIntlLinkAdapter( NextIntlLink: NextIntlLinkLike, ): LinkComponent { return forwardRef( function NextIntlLinkAdapter({ href, replace, scroll, prefetch, children, ...rest }, ref) { return ( {children} ); }, ); }