import { createNavigationItem, createNavigationMenu, deleteNavigationItem, deleteNavigationMenu, listNavigationMenus, listPages, updateNavigationItem, updateNavigationMenu, upsertNavigationItemTranslation, } from "@cms/db" import { Button } from "@cms/ui/button" import { revalidatePath } from "next/cache" import { redirect } from "next/navigation" import { AdminShell } from "@/components/admin-shell" import { CreateMenuForm } from "@/components/navigation/create-menu-form" import { CreateNavigationItemForm } from "@/components/navigation/create-navigation-item-form" import { requirePermissionForRoute } from "@/lib/route-guards" export const dynamic = "force-dynamic" type SearchParamsInput = Record const SUPPORTED_LOCALES = ["de", "en", "es", "fr"] as const type SupportedLocale = (typeof SUPPORTED_LOCALES)[number] function readFirstValue(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null } return value ?? null } function readInputString(formData: FormData, field: string): string { const value = formData.get(field) return typeof value === "string" ? value.trim() : "" } function readNullableString(formData: FormData, field: string): string | null { const value = readInputString(formData, field) return value.length > 0 ? value : null } function readInt(formData: FormData, field: string, fallback = 0): number { const value = readInputString(formData, field) if (!value) { return fallback } const parsed = Number.parseInt(value, 10) if (!Number.isFinite(parsed)) { return fallback } return parsed } function normalizeLocale(input: string | null): SupportedLocale { if (input && SUPPORTED_LOCALES.includes(input as SupportedLocale)) { return input as SupportedLocale } return "en" } function redirectWithState(params: { notice?: string; error?: string }) { const query = new URLSearchParams() if (params.notice) { query.set("notice", params.notice) } if (params.error) { query.set("error", params.error) } const value = query.toString() redirect(value ? `/navigation?${value}` : "/navigation") } async function createMenuAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await createNavigationMenu({ name: readInputString(formData, "name"), slug: readInputString(formData, "slug"), location: readInputString(formData, "location"), isVisible: readInputString(formData, "isVisible") === "true", }) } catch { redirectWithState({ error: "Failed to create navigation menu." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation menu created." }) } async function createItemAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await createNavigationItem({ menuId: readInputString(formData, "menuId"), label: readInputString(formData, "label"), href: readNullableString(formData, "href"), pageId: readNullableString(formData, "pageId"), parentId: readNullableString(formData, "parentId"), sortOrder: readInt(formData, "sortOrder", 0), isVisible: readInputString(formData, "isVisible") === "true", }) } catch { redirectWithState({ error: "Failed to create navigation item." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation item created." }) } async function updateMenuAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await updateNavigationMenu({ id: readInputString(formData, "id"), name: readInputString(formData, "name"), slug: readInputString(formData, "slug"), location: readInputString(formData, "location"), isVisible: readInputString(formData, "isVisible") === "true", }) } catch { redirectWithState({ error: "Failed to update navigation menu." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation menu updated." }) } async function deleteMenuAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await deleteNavigationMenu(readInputString(formData, "id")) } catch { redirectWithState({ error: "Failed to delete navigation menu." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation menu deleted." }) } async function updateItemAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await updateNavigationItem({ id: readInputString(formData, "id"), label: readInputString(formData, "label"), href: readNullableString(formData, "href"), pageId: readNullableString(formData, "pageId"), parentId: readNullableString(formData, "parentId"), sortOrder: readInt(formData, "sortOrder", 0), isVisible: readInputString(formData, "isVisible") === "true", }) } catch { redirectWithState({ error: "Failed to update navigation item." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation item updated." }) } async function deleteItemAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) try { await deleteNavigationItem(readInputString(formData, "id")) } catch { redirectWithState({ error: "Failed to delete navigation item." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation item deleted." }) } async function upsertItemTranslationAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:write", scope: "team", }) const locale = normalizeLocale(readInputString(formData, "locale")) try { await upsertNavigationItemTranslation({ navigationItemId: readInputString(formData, "navigationItemId"), locale, label: readInputString(formData, "label"), }) } catch { redirectWithState({ error: "Failed to save item translation." }) } revalidatePath("/navigation") redirectWithState({ notice: "Navigation item translation saved." }) } export default async function NavigationManagementPage({ searchParams, }: { searchParams: Promise }) { const role = await requirePermissionForRoute({ nextPath: "/navigation", permission: "navigation:read", scope: "team", }) const [resolvedSearchParams, menus, pages] = await Promise.all([ searchParams, listNavigationMenus(), listPages(200), ]) const notice = readFirstValue(resolvedSearchParams.notice) const error = readFirstValue(resolvedSearchParams.error) const selectedLocale = normalizeLocale(readFirstValue(resolvedSearchParams.locale)) return ( {notice ? (
{notice}
) : null} {error ? (
{error}
) : null}

Create Menu

Create Navigation Item

{SUPPORTED_LOCALES.map((locale) => ( {locale.toUpperCase()} ))}
{menus.length === 0 ? (
No navigation menus yet.
) : ( menus.map((menu) => (
{menu.items.length === 0 ? (

No items in this menu.

) : ( menu.items.map((item) => { const translation = item.translations.find( (entry) => entry.locale === selectedLocale, ) return (

Translation ({selectedLocale.toUpperCase()}) - saved locales:{" "} {item.translations.length > 0 ? item.translations .map((entry) => entry.locale.toUpperCase()) .join(", ") : "none"}

) }) )}
)) )}
) }