'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}
);
},
);
}