- typed client and useMenu/useMenuApi (SSR, menu:<code>:<locale> key, server and public bases, x-forwarded-for forwarded also on cached renders) - MenuNav and recursive MenuNavItem: WAI disclosure navigation, URL allowlist with a text fallback, rel on _blank, active link and trail, mobile toggle, Esc / focus-out / outside pointer / route change closing - menu tokens and base CSS, README - node tests (link rules, client, server-rendered NavItem) and a playground SSR smoke against a fixture API
44 lines
1.7 KiB
TypeScript
44 lines
1.7 KiB
TypeScript
// Typed client for the anonymous menu API. It wraps an injected fetcher, so
|
|
// the Nuxt composable decides the base URL and headers and `node --test` can
|
|
// drive it with a stub.
|
|
|
|
import type { Menu, MenuEnvelope } from './types.ts'
|
|
|
|
/** Issues one GET for `path` (e.g. "/menus/main") with `query`, resolving the parsed JSON. */
|
|
export type MenuFetcher = (path: string, query?: Record<string, string>) => Promise<unknown>
|
|
|
|
/** True for a fetch error that carries HTTP status 404 (ofetch, h3 or plain). */
|
|
export function isNotFound(err: unknown): boolean {
|
|
if (!err || typeof err !== 'object') return false
|
|
const e = err as { status?: unknown; statusCode?: unknown; response?: { status?: unknown } }
|
|
return e.status === 404 || e.statusCode === 404 || e.response?.status === 404
|
|
}
|
|
|
|
/** The API path of a menu: /menus/<code>, the code URL-encoded. */
|
|
export function menuPath(code: string): string {
|
|
return `/menus/${encodeURIComponent(code)}`
|
|
}
|
|
|
|
export interface MenuClient {
|
|
/** The menu with `code`, or null when the API answers 404 (unknown or inactive). */
|
|
getMenu(code: string, locale?: string): Promise<Menu | null>
|
|
}
|
|
|
|
export function createMenuClient(fetcher: MenuFetcher): MenuClient {
|
|
return {
|
|
async getMenu(code, locale) {
|
|
const query = locale ? { locale } : undefined
|
|
let res: MenuEnvelope
|
|
try {
|
|
res = (await (query ? fetcher(menuPath(code), query) : fetcher(menuPath(code)))) as MenuEnvelope
|
|
} catch (err) {
|
|
if (isNotFound(err)) return null
|
|
throw err
|
|
}
|
|
const menu = res?.data
|
|
if (!menu || typeof menu !== 'object') return null
|
|
return { ...menu, items: Array.isArray(menu.items) ? menu.items : [] }
|
|
},
|
|
}
|
|
}
|