frontend.porwoll.de/src/app/layout.tsx
CCS Admin b906cbb2b1 feat: implement CMS-driven navigation with dropdown submenus
Bypass broken contracts client navigation filter (queries non-existent
'type' field) with direct fetch to /api/navigations. Transform CMS
mainMenu structure (page/custom/submenu types) into NavItem format.
Replace JS-state dropdown (AnimatePresence) with pure CSS group-hover
to fix SSR hydration issues where both dropdowns opened simultaneously.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:22:04 +00:00

120 lines
3.7 KiB
TypeScript

import type { Metadata } from 'next'
import { Montserrat, Open_Sans } from 'next/font/google'
import { Navigation } from '@/components/Navigation'
import { Footer } from '@/components/Footer'
import { getNavigation, getSiteSettings, getSocialLinks } from '@/lib/api'
import './globals.css'
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSiteSettings() as Record<string, unknown> | null
const favicon = settings?.favicon as Record<string, unknown> | undefined
const faviconUrl = favicon?.url as string | undefined
return {
title: 'Martin Porwoll',
description: 'Whistleblower. Unternehmer. Mensch.',
icons: faviconUrl ? { icon: faviconUrl } : undefined,
}
}
const montserrat = Montserrat({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-montserrat',
display: 'swap',
})
const openSans = Open_Sans({
subsets: ['latin'],
weight: ['300', '400', '600', '700', '800'],
variable: '--font-open-sans',
display: 'swap',
})
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [navigation, siteSettings, socialLinks] = await Promise.all([
getNavigation('header'),
getSiteSettings(),
getSocialLinks(),
])
// Transform CMS navigation into NavItem format
const nav = navigation as unknown as Record<string, unknown> | null
const mainMenu = (nav?.mainMenu || nav?.items) as Array<Record<string, unknown>> | undefined
function pageSlugToHref(page: Record<string, unknown> | null): string {
if (!page?.slug) return '#'
return page.slug === 'startseite' ? '/' : `/${page.slug}`
}
function buildSubmenuItems(items: Array<Record<string, unknown>>): { label: string; href: string }[] {
return items.map((sub) => {
const subPage = sub.page as Record<string, unknown> | null
const subUrl = sub.url as string | null
const linkType = sub.linkType as string
return {
label: (sub.label as string) || '',
href: linkType === 'page' ? pageSlugToHref(subPage) : (subUrl || '#'),
}
})
}
const navItems = mainMenu?.length
? mainMenu.map((item) => {
const type = item.type as string
const page = item.page as Record<string, unknown> | null
const submenu = item.submenu as Array<Record<string, unknown>> | undefined
const children = submenu?.length ? buildSubmenuItems(submenu) : undefined
let href = '#'
if (type === 'page') href = pageSlugToHref(page)
else if (type === 'custom') href = (item.url as string) || '#'
return {
label: (item.label as string) || '',
href,
children,
}
})
: [
{ label: 'Home', href: '/' },
{ label: 'Der Mensch', href: '/mensch' },
{ label: 'Whistleblowing', href: '/whistleblowing' },
{ label: 'Kontakt', href: '/kontakt' },
]
const settings = siteSettings as unknown as Record<string, unknown> | null
const logoMedia = settings?.logo as Record<string, unknown> | undefined
const logoUrl = logoMedia?.url as string | undefined
const contactInfo = settings?.contactInfo as Record<string, string> | undefined
return (
<html lang="de" className={`${montserrat.variable} ${openSans.variable}`}>
<body className="font-body">
<Navigation
items={navItems}
logo={logoUrl}
transparent={true}
/>
<main id="top">
{children}
</main>
<Footer
socialLinks={socialLinks}
contact={{
email: contactInfo?.email,
phone: contactInfo?.phone,
address: contactInfo?.address,
}}
/>
</body>
</html>
)
}