fix: navigation migration to payload-contracts

- Fix getNavigation() to use contracts API (no broken type filter)
- Single nav fetch in layout, pass mainMenu/footerMenu to components
- Header, Navigation, MobileMenu use CMS mainMenu schema
- Footer uses CMS footerMenu schema with linkType field
- Add pnpm-workspace.yaml for onlyBuiltDependencies allowlist
- Update payload-contracts to latest (navigation fix a0eea96)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-20 13:43:05 +00:00
parent a8e8d2861b
commit 7235d8b910
9 changed files with 153 additions and 208 deletions

View file

@ -10,7 +10,7 @@ importers:
dependencies: dependencies:
'@c2s/payload-contracts': '@c2s/payload-contracts':
specifier: github:complexcaresolutions/payload-contracts specifier: github:complexcaresolutions/payload-contracts
version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7(react@19.2.1) version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#a0eea9649d35ec2a4554632554d53799a36b7f4b(react@19.2.1)
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@ -125,8 +125,8 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7': '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#a0eea9649d35ec2a4554632554d53799a36b7f4b':
resolution: {commit: 64847594b2150bfdce09a7bd7f54ad2f52d6f2b7, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git} resolution: {commit: a0eea9649d35ec2a4554632554d53799a36b7f4b, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git}
version: 1.0.0 version: 1.0.0
peerDependencies: peerDependencies:
react: ^19.0.0 react: ^19.0.0
@ -2048,7 +2048,7 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7(react@19.2.1)': '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#a0eea9649d35ec2a4554632554d53799a36b7f4b(react@19.2.1)':
optionalDependencies: optionalDependencies:
react: 19.2.1 react: 19.2.1

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- "@c2s/payload-contracts"

View file

@ -71,10 +71,9 @@ export default async function RootLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode children: React.ReactNode
}>) { }>) {
// Fetch navigation and settings in parallel // Fetch navigation (one doc per tenant) and settings in parallel
const [headerNav, footerNav, settings] = await Promise.all([ const [navigation, settings] = await Promise.all([
getNavigation('header'), getNavigation(),
getNavigation('footer'),
getSiteSettings(), getSiteSettings(),
]) ])
@ -89,9 +88,9 @@ export default async function RootLayout({
Zum Hauptinhalt springen Zum Hauptinhalt springen
</a> </a>
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
<Header navigation={headerNav} settings={settings} /> <Header mainMenu={navigation?.mainMenu ?? null} settings={settings} />
<main id="main-content" className="flex-1">{children}</main> <main id="main-content" className="flex-1">{children}</main>
<Footer navigation={footerNav} settings={settings} /> <Footer footerMenu={navigation?.footerMenu ?? null} settings={settings} />
</div> </div>
<UmamiScript /> <UmamiScript />
</body> </body>

View file

@ -59,7 +59,6 @@ export function BlockRenderer({ blocks }: BlockRendererProps) {
return null return null
} }
// Extract the block data, excluding blockType for the component props
const { blockType, ...blockProps } = block const { blockType, ...blockProps } = block
return ( return (

View file

@ -1,29 +1,21 @@
import Link from 'next/link' import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import type { Navigation, SiteSettings, Address } from '@/lib/types' import type { Navigation as NavigationType } from '@c2s/payload-contracts/types'
import type { SiteSettings } from '@/lib/types'
// Helper to format address type FooterMenuItem = NonNullable<NavigationType['footerMenu']>[number]
function formatAddress(address: Address | string | undefined): string | null {
if (!address) return null
if (typeof address === 'string') return address
const parts: string[] = []
if (address.street) parts.push(address.street)
if (address.additionalLine) parts.push(address.additionalLine)
if (address.zip || address.city) {
parts.push([address.zip, address.city].filter(Boolean).join(' '))
}
if (address.country) parts.push(address.country)
return parts.length > 0 ? parts.join('\n') : null
}
interface FooterProps { interface FooterProps {
navigation: Navigation | null footerMenu: FooterMenuItem[] | null
settings: SiteSettings | null settings: SiteSettings | null
} }
export function Footer({ navigation, settings }: FooterProps) { function getPageSlug(page: FooterMenuItem['page']): string | undefined {
if (typeof page === 'object' && page !== null) return (page as { slug: string }).slug
return undefined
}
export function Footer({ footerMenu, settings }: FooterProps) {
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
return ( return (
@ -56,84 +48,38 @@ export function Footer({ navigation, settings }: FooterProps) {
{settings?.socialLinks && ( {settings?.socialLinks && (
<div className="flex gap-4 mt-6"> <div className="flex gap-4 mt-6">
{settings.socialLinks.instagram && ( {settings.socialLinks.instagram && (
<a <SocialAnchor href={settings.socialLinks.instagram} label="Instagram" platform="instagram" />
href={settings.socialLinks.instagram}
target="_blank"
rel="noopener noreferrer"
className="text-warm-gray hover:text-soft-white transition-colors"
aria-label="Instagram"
>
<SocialIcon platform="instagram" />
</a>
)} )}
{settings.socialLinks.youtube && ( {settings.socialLinks.youtube && (
<a <SocialAnchor href={settings.socialLinks.youtube} label="YouTube" platform="youtube" />
href={settings.socialLinks.youtube}
target="_blank"
rel="noopener noreferrer"
className="text-warm-gray hover:text-soft-white transition-colors"
aria-label="YouTube"
>
<SocialIcon platform="youtube" />
</a>
)} )}
{settings.socialLinks.pinterest && ( {settings.socialLinks.pinterest && (
<a <SocialAnchor href={settings.socialLinks.pinterest} label="Pinterest" platform="pinterest" />
href={settings.socialLinks.pinterest}
target="_blank"
rel="noopener noreferrer"
className="text-warm-gray hover:text-soft-white transition-colors"
aria-label="Pinterest"
>
<SocialIcon platform="pinterest" />
</a>
)} )}
{settings.socialLinks.tiktok && ( {settings.socialLinks.tiktok && (
<a <SocialAnchor href={settings.socialLinks.tiktok} label="TikTok" platform="tiktok" />
href={settings.socialLinks.tiktok}
target="_blank"
rel="noopener noreferrer"
className="text-warm-gray hover:text-soft-white transition-colors"
aria-label="TikTok"
>
<SocialIcon platform="tiktok" />
</a>
)} )}
{settings.socialLinks.facebook && ( {settings.socialLinks.facebook && (
<a <SocialAnchor href={settings.socialLinks.facebook} label="Facebook" platform="facebook" />
href={settings.socialLinks.facebook}
target="_blank"
rel="noopener noreferrer"
className="text-warm-gray hover:text-soft-white transition-colors"
aria-label="Facebook"
>
<SocialIcon platform="facebook" />
</a>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Navigation Columns */} {/* Footer Navigation */}
{navigation?.items && navigation.items.length > 0 && ( {footerMenu && footerMenu.length > 0 && (
<> <div className="lg:col-span-2">
{groupNavigationItems(navigation.items).map((group, index) => (
<div key={index}>
{group.title && (
<h3 className="text-xs font-bold tracking-[0.1em] uppercase text-brass mb-4"> <h3 className="text-xs font-bold tracking-[0.1em] uppercase text-brass mb-4">
{group.title} Links
</h3> </h3>
)} <ul className="grid grid-cols-2 gap-2">
<ul className="space-y-2"> {footerMenu.map((item) => (
{group.items.map((item) => (
<li key={item.id}> <li key={item.id}>
<FooterLink item={item} /> <FooterLink item={item} />
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
))}
</>
)} )}
{/* Contact Column */} {/* Contact Column */}
@ -163,11 +109,6 @@ export function Footer({ navigation, settings }: FooterProps) {
</a> </a>
</p> </p>
)} )}
{formatAddress(settings.address) && (
<p className="text-warm-gray whitespace-pre-line">
{formatAddress(settings.address)}
</p>
)}
</address> </address>
</div> </div>
)} )}
@ -185,24 +126,20 @@ export function Footer({ navigation, settings }: FooterProps) {
) )
} }
function FooterLink({ item }: { item: Navigation['items'][0] }) { function FooterLink({ item }: { item: FooterMenuItem }) {
const className = 'text-sm text-soft-white hover:text-sand transition-colors' const className = 'text-sm text-soft-white hover:text-sand transition-colors'
const slug = getPageSlug(item.page)
const href = item.linkType === 'page' && slug ? `/${slug}` : item.url || '/'
const isExternal = item.linkType === 'custom' && item.url?.startsWith('http')
if (item.type === 'external' && item.url) { if (isExternal) {
return ( return (
<a <a href={item.url!} target="_blank" rel="noopener noreferrer" className={className}>
href={item.url}
target={item.openInNewTab ? '_blank' : undefined}
rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
className={className}
>
{item.label} {item.label}
</a> </a>
) )
} }
const href = item.page?.slug ? `/${item.page.slug}` : item.url || '/'
return ( return (
<Link href={href} className={className}> <Link href={href} className={className}>
{item.label} {item.label}
@ -210,32 +147,20 @@ function FooterLink({ item }: { item: Navigation['items'][0] }) {
) )
} }
// Helper to group navigation items for footer columns function SocialAnchor({ href, label, platform }: { href: string; label: string; platform: string }) {
function groupNavigationItems(items: Navigation['items']) { return (
// Simple grouping - you can customize based on your needs <a
const groups: { title?: string; items: Navigation['items'] }[] = [] href={href}
target="_blank"
items.forEach((item) => { rel="noopener noreferrer"
if (item.type === 'submenu' && item.children?.length) { className="text-warm-gray hover:text-soft-white transition-colors"
groups.push({ aria-label={label}
title: item.label, >
items: item.children, <SocialIcon platform={platform} />
}) </a>
}
})
// Add remaining items as a group
const topLevelItems = items.filter(
(item) => item.type !== 'submenu' || !item.children?.length
) )
if (topLevelItems.length > 0) {
groups.unshift({ items: topLevelItems })
} }
return groups
}
// Social Media Icons
function SocialIcon({ platform }: { platform: string }) { function SocialIcon({ platform }: { platform: string }) {
const iconClass = 'w-5 h-5' const iconClass = 'w-5 h-5'
@ -258,12 +183,6 @@ function SocialIcon({ platform }: { platform: string }) {
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" /> <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg> </svg>
) )
case 'linkedin':
return (
<svg className={iconClass} fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
)
case 'pinterest': case 'pinterest':
return ( return (
<svg className={iconClass} fill="currentColor" viewBox="0 0 24 24"> <svg className={iconClass} fill="currentColor" viewBox="0 0 24 24">
@ -279,12 +198,7 @@ function SocialIcon({ platform }: { platform: string }) {
default: default:
return ( return (
<svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg> </svg>
) )
} }

View file

@ -5,14 +5,17 @@ import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import { Navigation } from './Navigation' import { Navigation } from './Navigation'
import { MobileMenu } from './MobileMenu' import { MobileMenu } from './MobileMenu'
import type { Navigation as NavigationType, SiteSettings } from '@/lib/types' import type { Navigation as NavigationType } from '@c2s/payload-contracts/types'
import type { SiteSettings } from '@/lib/types'
type MainMenuItem = NonNullable<NavigationType['mainMenu']>[number]
interface HeaderProps { interface HeaderProps {
navigation: NavigationType | null mainMenu: MainMenuItem[] | null
settings: SiteSettings | null settings: SiteSettings | null
} }
export function Header({ navigation, settings }: HeaderProps) { export function Header({ mainMenu, settings }: HeaderProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
return ( return (
@ -39,7 +42,7 @@ export function Header({ navigation, settings }: HeaderProps) {
{/* Desktop Navigation */} {/* Desktop Navigation */}
<div className="hidden md:block"> <div className="hidden md:block">
<Navigation items={navigation?.items || []} /> <Navigation items={mainMenu || []} />
</div> </div>
{/* Mobile Menu Toggle */} {/* Mobile Menu Toggle */}
@ -69,7 +72,7 @@ export function Header({ navigation, settings }: HeaderProps) {
{/* Mobile Menu */} {/* Mobile Menu */}
<MobileMenu <MobileMenu
items={navigation?.items || []} items={mainMenu || []}
isOpen={isMobileMenuOpen} isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)} onClose={() => setIsMobileMenuOpen(false)}
/> />

View file

@ -3,10 +3,12 @@
import { useEffect, useRef, useCallback } from 'react' import { useEffect, useRef, useCallback } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { NavigationItem } from '@/lib/types' import type { Navigation as NavigationType, Page } from '@c2s/payload-contracts/types'
type MainMenuItem = NonNullable<NavigationType['mainMenu']>[number]
interface MobileMenuProps { interface MobileMenuProps {
items: NavigationItem[] items: MainMenuItem[]
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
} }
@ -33,7 +35,6 @@ export function MobileMenu({ items, isOpen, onClose }: MobileMenuProps) {
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
previousActiveElement.current = document.activeElement as HTMLElement previousActiveElement.current = document.activeElement as HTMLElement
// Small delay to allow animation to start
setTimeout(() => { setTimeout(() => {
closeButtonRef.current?.focus() closeButtonRef.current?.focus()
}, 100) }, 100)
@ -142,46 +143,39 @@ export function MobileMenu({ items, isOpen, onClose }: MobileMenuProps) {
) )
} }
interface MobileNavItemProps { function MobileNavItem({ item, onClose, depth = 0 }: { item: MainMenuItem; onClose: () => void; depth?: number }) {
item: NavigationItem
onClose: () => void
depth?: number
}
function MobileNavItem({ item, onClose, depth = 0 }: MobileNavItemProps) {
const linkClasses = cn( const linkClasses = cn(
'block py-3 text-base font-medium text-espresso', 'block py-3 text-base font-medium text-espresso',
'hover:text-brass transition-colors', 'hover:text-brass transition-colors',
depth > 0 && 'pl-4 text-sm' depth > 0 && 'pl-4 text-sm'
) )
// With children // With submenu
if (item.type === 'submenu' && item.children?.length) { if (item.type === 'submenu' && item.submenu?.length) {
return ( return (
<li> <li>
<span className="block py-3 text-base font-semibold text-espresso"> <span className="block py-3 text-base font-semibold text-espresso">
{item.label} {item.label}
</span> </span>
<ul className="border-l-2 border-warm-gray ml-2"> <ul className="border-l-2 border-warm-gray ml-2">
{item.children.map((child) => ( {item.submenu.map((child) => (
<MobileNavItem <MobileSubItem key={child.id} item={child} onClose={onClose} />
key={child.id}
item={child}
onClose={onClose}
depth={depth + 1}
/>
))} ))}
</ul> </ul>
</li> </li>
) )
} }
// External link // Link
if (item.type === 'external' && item.url) { const slug = typeof item.page === 'object' && item.page !== null ? (item.page as Page).slug : undefined
const href = item.type === 'page' && slug ? `/${slug}` : item.url || '/'
const isExternal = item.type === 'custom' && item.url?.startsWith('http')
if (isExternal) {
return ( return (
<li> <li>
<a <a
href={item.url} href={item.url!}
target={item.openInNewTab ? '_blank' : undefined} target={item.openInNewTab ? '_blank' : undefined}
rel={item.openInNewTab ? 'noopener noreferrer' : undefined} rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
className={linkClasses} className={linkClasses}
@ -193,8 +187,21 @@ function MobileNavItem({ item, onClose, depth = 0 }: MobileNavItemProps) {
) )
} }
// Internal link return (
const href = item.page?.slug ? `/${item.page.slug}` : item.url || '/' <li>
<Link href={href} className={linkClasses} onClick={onClose}>
{item.label}
</Link>
</li>
)
}
type SubMenuItem = NonNullable<MainMenuItem['submenu']>[number]
function MobileSubItem({ item, onClose }: { item: SubMenuItem; onClose: () => void }) {
const linkClasses = 'block py-3 pl-4 text-sm font-medium text-espresso hover:text-brass transition-colors'
const slug = typeof item.page === 'object' && item.page !== null ? (item.page as Page).slug : undefined
const href = item.linkType === 'page' && slug ? `/${slug}` : item.url || '/'
return ( return (
<li> <li>

View file

@ -3,13 +3,20 @@
import { useState } from 'react' import { useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { NavigationItem } from '@/lib/types' import type { Navigation as NavigationType, Page } from '@c2s/payload-contracts/types'
type MainMenuItem = NonNullable<NavigationType['mainMenu']>[number]
interface NavigationProps { interface NavigationProps {
items: NavigationItem[] items: MainMenuItem[]
className?: string className?: string
} }
function getPageSlug(page: MainMenuItem['page']): string | undefined {
if (typeof page === 'object' && page !== null) return (page as Page).slug
return undefined
}
export function Navigation({ items, className }: NavigationProps) { export function Navigation({ items, className }: NavigationProps) {
return ( return (
<nav className={cn('flex items-center gap-8', className)}> <nav className={cn('flex items-center gap-8', className)}>
@ -20,11 +27,7 @@ export function Navigation({ items, className }: NavigationProps) {
) )
} }
interface NavItemProps { function NavItem({ item }: { item: MainMenuItem }) {
item: NavigationItem
}
function NavItem({ item }: NavItemProps) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const linkClasses = cn( const linkClasses = cn(
@ -34,7 +37,7 @@ function NavItem({ item }: NavItemProps) {
) )
// Submenu // Submenu
if (item.type === 'submenu' && item.children?.length) { if (item.type === 'submenu' && item.submenu?.length) {
return ( return (
<div <div
className="relative" className="relative"
@ -76,8 +79,8 @@ function NavItem({ item }: NavItemProps) {
)} )}
> >
<div className="bg-soft-white border border-warm-gray rounded-lg shadow-lg py-2 min-w-[200px]"> <div className="bg-soft-white border border-warm-gray rounded-lg shadow-lg py-2 min-w-[200px]">
{item.children.map((child) => ( {item.submenu.map((child) => (
<NavLink <SubNavLink
key={child.id} key={child.id}
item={child} item={child}
className="block px-4 py-2 text-sm text-espresso hover:bg-ivory hover:text-brass transition-colors" className="block px-4 py-2 text-sm text-espresso hover:bg-ivory hover:text-brass transition-colors"
@ -90,29 +93,45 @@ function NavItem({ item }: NavItemProps) {
} }
// Regular link // Regular link
return <NavLink item={item} className={linkClasses} /> const slug = getPageSlug(item.page)
} const href = item.type === 'page' && slug ? `/${slug}` : item.url || '/'
interface NavLinkProps { if (item.type === 'custom' && item.url) {
item: NavigationItem const isExternal = item.url.startsWith('http')
className?: string if (isExternal) {
}
function NavLink({ item, className }: NavLinkProps) {
if (item.type === 'external' && item.url) {
return ( return (
<a <a
href={item.url} href={item.url}
target={item.openInNewTab ? '_blank' : undefined} target={item.openInNewTab ? '_blank' : undefined}
rel={item.openInNewTab ? 'noopener noreferrer' : undefined} rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
className={className} className={linkClasses}
> >
{item.label} {item.label}
</a> </a>
) )
} }
}
const href = item.page?.slug ? `/${item.page.slug}` : item.url || '/' return (
<Link href={href} className={linkClasses}>
{item.label}
</Link>
)
}
type SubMenuItem = NonNullable<MainMenuItem['submenu']>[number]
function SubNavLink({ item, className }: { item: SubMenuItem; className?: string }) {
const slug = typeof item.page === 'object' && item.page !== null ? (item.page as Page).slug : undefined
const href = item.linkType === 'page' && slug ? `/${slug}` : item.url || '/'
if (item.linkType === 'custom' && item.url?.startsWith('http')) {
return (
<a href={item.url} className={className} target="_blank" rel="noopener noreferrer">
{item.label}
</a>
)
}
return ( return (
<Link href={href} className={className}> <Link href={href} className={className}>

View file

@ -1,17 +1,18 @@
/** /**
* Payload CMS API functions powered by @c2s/payload-contracts * Payload CMS API functions powered by @c2s/payload-contracts
* *
* Uses the shared API client for transport (tenant isolation, fetch caching) * Uses the shared API client for transport (tenant isolation, fetch caching).
* but returns data typed with local interfaces for component compatibility. * Returns data typed with local interfaces for component compatibility.
* *
* The local types use simplified interfaces (e.g. id: string, meta instead of seo) * Navigation uses contracts types directly (schema matches).
* while the contracts use the real CMS types. We use 'as unknown as' to bridge. * Other types use 'as unknown as' bridge since local types differ
* from CMS types (meta vs seo, id string vs number, etc.).
*/ */
import { cms } from "./cms" import { cms } from "./cms"
import type { Navigation } from "@c2s/payload-contracts/types"
import type { import type {
Page, Page,
Post, Post,
Navigation,
Favorite, Favorite,
Series, Series,
Testimonial, Testimonial,
@ -21,7 +22,8 @@ import type {
SiteSettings, SiteSettings,
} from "./types" } from "./types"
// const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://cms.c2sgmbh.de" export type { Navigation }
const TENANT_ID = process.env.NEXT_PUBLIC_TENANT_ID || "9" const TENANT_ID = process.env.NEXT_PUBLIC_TENANT_ID || "9"
// Pages // Pages
@ -84,11 +86,11 @@ export async function getPosts(options: {
} }
} }
// Navigation // Navigation — single document per tenant with mainMenu + footerMenu
export async function getNavigation(type: "header" | "footer" | "mobile"): Promise<Navigation | null> { // Uses contracts Navigation type directly (no bridge needed)
export async function getNavigation(): Promise<Navigation | null> {
try { try {
const result = await cms.navigation.getNavigation(type, { depth: 2 }) return await cms.navigation.getNavigation({ depth: 2 })
return result as unknown as Navigation | null
} catch { } catch {
return null return null
} }