mirror of
https://github.com/complexcaresolutions/frontend.porwoll.de.git
synced 2026-03-17 18:43: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(),
|
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 nav = navigation as unknown as Record<string, unknown> | null
|
||||||
const navItems = nav?.items
|
const mainMenu = (nav?.mainMenu || nav?.items) as Array<Record<string, unknown>> | undefined
|
||||||
? (nav.items as Array<Record<string, unknown>>).map(
|
|
||||||
(item) => ({
|
function pageSlugToHref(page: Record<string, unknown> | null): string {
|
||||||
label: (item.label as string) || '',
|
if (!page?.slug) return '#'
|
||||||
href: (item.link as string) || (item.url as string) || '#',
|
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: 'Home', href: '/' },
|
||||||
{ label: 'Der Mensch', href: '/mensch' },
|
{ label: 'Der Mensch', href: '/mensch' },
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ interface NavigationProps {
|
||||||
export function Navigation({ items, logo, transparent = false }: NavigationProps) {
|
export function Navigation({ items, logo, transparent = false }: NavigationProps) {
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
|
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -75,10 +74,8 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
||||||
<div className="hidden lg:flex items-center gap-8">
|
<div className="hidden lg:flex items-center gap-8">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.href}
|
key={item.label}
|
||||||
className="relative"
|
className="group relative"
|
||||||
onMouseEnter={() => item.children && setActiveDropdown(item.href)}
|
|
||||||
onMouseLeave={() => setActiveDropdown(null)}
|
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
|
@ -93,16 +90,17 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown — hidden by default, shown on group hover via CSS */}
|
||||||
<AnimatePresence>
|
{item.children && (
|
||||||
{item.children && activeDropdown === item.href && (
|
<div
|
||||||
<motion.div
|
className={cn(
|
||||||
initial={{ opacity: 0, y: 10 }}
|
'absolute top-full left-0 pt-3 min-w-[200px]',
|
||||||
animate={{ opacity: 1, y: 0 }}
|
'opacity-0 invisible translate-y-2',
|
||||||
exit={{ opacity: 0, y: 10 }}
|
'group-hover:opacity-100 group-hover:visible group-hover:translate-y-0',
|
||||||
transition={{ duration: 0.2 }}
|
'transition-all duration-200'
|
||||||
className="absolute top-full left-0 mt-3 py-5 pb-2.5 min-w-[200px] bg-white/95 border border-light"
|
)}
|
||||||
>
|
>
|
||||||
|
<div className="py-5 pb-2.5 bg-white/95 border border-light">
|
||||||
{item.children.map((child) => (
|
{item.children.map((child) => (
|
||||||
<Link
|
<Link
|
||||||
key={child.href}
|
key={child.href}
|
||||||
|
|
@ -119,9 +117,9 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
||||||
{child.label}
|
{child.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,7 +147,7 @@ export function Navigation({ items, logo, transparent = false }: NavigationProps
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.href}>
|
<div key={item.label}>
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
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"
|
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
|
// 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 {
|
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 {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue