mirror of
https://github.com/complexcaresolutions/frontend.porwoll.de.git
synced 2026-03-17 15:13:42 +00:00
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>
This commit is contained in:
parent
3f962c6668
commit
b906cbb2b1
3 changed files with 63 additions and 28 deletions
|
|
@ -42,15 +42,44 @@ export default async function RootLayout({
|
|||
getSocialLinks(),
|
||||
])
|
||||
|
||||
// Transform CMS navigation items into NavItem format
|
||||
// Transform CMS navigation into NavItem format
|
||||
const nav = navigation as unknown as Record<string, unknown> | null
|
||||
const navItems = nav?.items
|
||||
? (nav.items as Array<Record<string, unknown>>).map(
|
||||
(item) => ({
|
||||
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: (item.link as string) || (item.url as string) || '#',
|
||||
})
|
||||
)
|
||||
href,
|
||||
children,
|
||||
}
|
||||
})
|
||||
: [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Der Mensch', href: '/mensch' },
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ interface NavigationProps {
|
|||
export function Navigation({ items, logo, transparent = false }: NavigationProps) {
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -75,10 +74,8 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
|||
<div className="hidden lg:flex items-center gap-8">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.href}
|
||||
className="relative"
|
||||
onMouseEnter={() => item.children && setActiveDropdown(item.href)}
|
||||
onMouseLeave={() => setActiveDropdown(null)}
|
||||
key={item.label}
|
||||
className="group relative"
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
|
|
@ -93,16 +90,17 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
|||
{item.label}
|
||||
</Link>
|
||||
|
||||
{/* Dropdown */}
|
||||
<AnimatePresence>
|
||||
{item.children && activeDropdown === item.href && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute top-full left-0 mt-3 py-5 pb-2.5 min-w-[200px] bg-white/95 border border-light"
|
||||
>
|
||||
{/* Dropdown — hidden by default, shown on group hover via CSS */}
|
||||
{item.children && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-full left-0 pt-3 min-w-[200px]',
|
||||
'opacity-0 invisible translate-y-2',
|
||||
'group-hover:opacity-100 group-hover:visible group-hover:translate-y-0',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<div className="py-5 pb-2.5 bg-white/95 border border-light">
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
|
|
@ -119,9 +117,9 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
|||
{child.label}
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -149,7 +147,7 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
|||
>
|
||||
<div className="py-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.href}>
|
||||
<div key={item.label}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="block px-4 py-3 font-heading uppercase text-[0.85em] tracking-[2px] text-gray-light hover:text-dark border-none"
|
||||
|
|
|
|||
|
|
@ -50,9 +50,17 @@ export async function getPosts(options: {
|
|||
}
|
||||
|
||||
// Navigation
|
||||
export async function getNavigation(type: "header" | "footer" | "mobile" = "header") {
|
||||
// Note: cms.navigation.getNavigation filters by a non-existent 'type' field,
|
||||
// causing API errors. Query the collection directly instead.
|
||||
export async function getNavigation(_type: "header" | "footer" | "mobile" = "header") {
|
||||
try {
|
||||
return await cms.navigation.getNavigation(type, { depth: 2 })
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://cms.c2sgmbh.de"
|
||||
const tenantId = process.env.NEXT_PUBLIC_TENANT_ID || "4"
|
||||
const url = `${baseUrl}/api/navigations?where[tenant][equals]=${tenantId}&limit=1&depth=2&locale=de`
|
||||
const res = await fetch(url, { next: { revalidate: 60, tags: ["navigations"] } })
|
||||
if (!res.ok) return null
|
||||
const data = await res.json() as { docs?: Record<string, unknown>[] }
|
||||
return data?.docs?.[0] || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue