Files
vue-menu-layer/app/utils/client.ts
Jakub Zych d980f4cf2f feat: MenuNav layer for the SummerCMS menu API
- 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
2026-10-12 18:19:53 +02:00

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 : [] }
},
}
}