mirror of
https://github.com/complexcaresolutions/frontend.blogwoman.de.git
synced 2026-03-17 15:04:01 +00:00
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:
parent
a8e8d2861b
commit
7235d8b910
9 changed files with 153 additions and 208 deletions
|
|
@ -10,7 +10,7 @@ importers:
|
|||
dependencies:
|
||||
'@c2s/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:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
|
|
@ -125,8 +125,8 @@ packages:
|
|||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7':
|
||||
resolution: {commit: 64847594b2150bfdce09a7bd7f54ad2f52d6f2b7, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git}
|
||||
'@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#a0eea9649d35ec2a4554632554d53799a36b7f4b':
|
||||
resolution: {commit: a0eea9649d35ec2a4554632554d53799a36b7f4b, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git}
|
||||
version: 1.0.0
|
||||
peerDependencies:
|
||||
react: ^19.0.0
|
||||
|
|
@ -2048,7 +2048,7 @@ snapshots:
|
|||
'@babel/helper-string-parser': 7.27.1
|
||||
'@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:
|
||||
react: 19.2.1
|
||||
|
||||
|
|
|
|||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
onlyBuiltDependencies:
|
||||
- "@c2s/payload-contracts"
|
||||
|
|
@ -71,10 +71,9 @@ export default async function RootLayout({
|
|||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
// Fetch navigation and settings in parallel
|
||||
const [headerNav, footerNav, settings] = await Promise.all([
|
||||
getNavigation('header'),
|
||||
getNavigation('footer'),
|
||||
// Fetch navigation (one doc per tenant) and settings in parallel
|
||||
const [navigation, settings] = await Promise.all([
|
||||
getNavigation(),
|
||||
getSiteSettings(),
|
||||
])
|
||||
|
||||
|
|
@ -89,9 +88,9 @@ export default async function RootLayout({
|
|||
Zum Hauptinhalt springen
|
||||
</a>
|
||||
<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>
|
||||
<Footer navigation={footerNav} settings={settings} />
|
||||
<Footer footerMenu={navigation?.footerMenu ?? null} settings={settings} />
|
||||
</div>
|
||||
<UmamiScript />
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export function BlockRenderer({ blocks }: BlockRendererProps) {
|
|||
return null
|
||||
}
|
||||
|
||||
// Extract the block data, excluding blockType for the component props
|
||||
const { blockType, ...blockProps } = block
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,29 +1,21 @@
|
|||
import Link from 'next/link'
|
||||
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
|
||||
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
|
||||
}
|
||||
type FooterMenuItem = NonNullable<NavigationType['footerMenu']>[number]
|
||||
|
||||
interface FooterProps {
|
||||
navigation: Navigation | null
|
||||
footerMenu: FooterMenuItem[] | 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()
|
||||
|
||||
return (
|
||||
|
|
@ -56,84 +48,38 @@ export function Footer({ navigation, settings }: FooterProps) {
|
|||
{settings?.socialLinks && (
|
||||
<div className="flex gap-4 mt-6">
|
||||
{settings.socialLinks.instagram && (
|
||||
<a
|
||||
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>
|
||||
<SocialAnchor href={settings.socialLinks.instagram} label="Instagram" platform="instagram" />
|
||||
)}
|
||||
{settings.socialLinks.youtube && (
|
||||
<a
|
||||
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>
|
||||
<SocialAnchor href={settings.socialLinks.youtube} label="YouTube" platform="youtube" />
|
||||
)}
|
||||
{settings.socialLinks.pinterest && (
|
||||
<a
|
||||
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>
|
||||
<SocialAnchor href={settings.socialLinks.pinterest} label="Pinterest" platform="pinterest" />
|
||||
)}
|
||||
{settings.socialLinks.tiktok && (
|
||||
<a
|
||||
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>
|
||||
<SocialAnchor href={settings.socialLinks.tiktok} label="TikTok" platform="tiktok" />
|
||||
)}
|
||||
{settings.socialLinks.facebook && (
|
||||
<a
|
||||
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>
|
||||
<SocialAnchor href={settings.socialLinks.facebook} label="Facebook" platform="facebook" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Columns */}
|
||||
{navigation?.items && navigation.items.length > 0 && (
|
||||
<>
|
||||
{groupNavigationItems(navigation.items).map((group, index) => (
|
||||
<div key={index}>
|
||||
{group.title && (
|
||||
{/* Footer Navigation */}
|
||||
{footerMenu && footerMenu.length > 0 && (
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-xs font-bold tracking-[0.1em] uppercase text-brass mb-4">
|
||||
{group.title}
|
||||
Links
|
||||
</h3>
|
||||
)}
|
||||
<ul className="space-y-2">
|
||||
{group.items.map((item) => (
|
||||
<ul className="grid grid-cols-2 gap-2">
|
||||
{footerMenu.map((item) => (
|
||||
<li key={item.id}>
|
||||
<FooterLink item={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Contact Column */}
|
||||
|
|
@ -163,11 +109,6 @@ export function Footer({ navigation, settings }: FooterProps) {
|
|||
</a>
|
||||
</p>
|
||||
)}
|
||||
{formatAddress(settings.address) && (
|
||||
<p className="text-warm-gray whitespace-pre-line">
|
||||
{formatAddress(settings.address)}
|
||||
</p>
|
||||
)}
|
||||
</address>
|
||||
</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 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 (
|
||||
<a
|
||||
href={item.url}
|
||||
target={item.openInNewTab ? '_blank' : undefined}
|
||||
rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
|
||||
className={className}
|
||||
>
|
||||
<a href={item.url!} target="_blank" rel="noopener noreferrer" className={className}>
|
||||
{item.label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
const href = item.page?.slug ? `/${item.page.slug}` : item.url || '/'
|
||||
|
||||
return (
|
||||
<Link href={href} className={className}>
|
||||
{item.label}
|
||||
|
|
@ -210,32 +147,20 @@ function FooterLink({ item }: { item: Navigation['items'][0] }) {
|
|||
)
|
||||
}
|
||||
|
||||
// Helper to group navigation items for footer columns
|
||||
function groupNavigationItems(items: Navigation['items']) {
|
||||
// Simple grouping - you can customize based on your needs
|
||||
const groups: { title?: string; items: Navigation['items'] }[] = []
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.type === 'submenu' && item.children?.length) {
|
||||
groups.push({
|
||||
title: item.label,
|
||||
items: item.children,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add remaining items as a group
|
||||
const topLevelItems = items.filter(
|
||||
(item) => item.type !== 'submenu' || !item.children?.length
|
||||
function SocialAnchor({ href, label, platform }: { href: string; label: string; platform: string }) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-warm-gray hover:text-soft-white transition-colors"
|
||||
aria-label={label}
|
||||
>
|
||||
<SocialIcon platform={platform} />
|
||||
</a>
|
||||
)
|
||||
if (topLevelItems.length > 0) {
|
||||
groups.unshift({ items: topLevelItems })
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// Social Media Icons
|
||||
function SocialIcon({ platform }: { platform: string }) {
|
||||
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" />
|
||||
</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':
|
||||
return (
|
||||
<svg className={iconClass} fill="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -279,12 +198,7 @@ function SocialIcon({ platform }: { platform: string }) {
|
|||
default:
|
||||
return (
|
||||
<svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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"
|
||||
/>
|
||||
<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" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ import Link from 'next/link'
|
|||
import Image from 'next/image'
|
||||
import { Navigation } from './Navigation'
|
||||
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 {
|
||||
navigation: NavigationType | null
|
||||
mainMenu: MainMenuItem[] | null
|
||||
settings: SiteSettings | null
|
||||
}
|
||||
|
||||
export function Header({ navigation, settings }: HeaderProps) {
|
||||
export function Header({ mainMenu, settings }: HeaderProps) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
|
|
@ -39,7 +42,7 @@ export function Header({ navigation, settings }: HeaderProps) {
|
|||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<Navigation items={navigation?.items || []} />
|
||||
<Navigation items={mainMenu || []} />
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
|
|
@ -69,7 +72,7 @@ export function Header({ navigation, settings }: HeaderProps) {
|
|||
|
||||
{/* Mobile Menu */}
|
||||
<MobileMenu
|
||||
items={navigation?.items || []}
|
||||
items={mainMenu || []}
|
||||
isOpen={isMobileMenuOpen}
|
||||
onClose={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
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 {
|
||||
items: NavigationItem[]
|
||||
items: MainMenuItem[]
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
|
@ -33,7 +35,6 @@ export function MobileMenu({ items, isOpen, onClose }: MobileMenuProps) {
|
|||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
previousActiveElement.current = document.activeElement as HTMLElement
|
||||
// Small delay to allow animation to start
|
||||
setTimeout(() => {
|
||||
closeButtonRef.current?.focus()
|
||||
}, 100)
|
||||
|
|
@ -142,46 +143,39 @@ export function MobileMenu({ items, isOpen, onClose }: MobileMenuProps) {
|
|||
)
|
||||
}
|
||||
|
||||
interface MobileNavItemProps {
|
||||
item: NavigationItem
|
||||
onClose: () => void
|
||||
depth?: number
|
||||
}
|
||||
|
||||
function MobileNavItem({ item, onClose, depth = 0 }: MobileNavItemProps) {
|
||||
function MobileNavItem({ item, onClose, depth = 0 }: { item: MainMenuItem; onClose: () => void; depth?: number }) {
|
||||
const linkClasses = cn(
|
||||
'block py-3 text-base font-medium text-espresso',
|
||||
'hover:text-brass transition-colors',
|
||||
depth > 0 && 'pl-4 text-sm'
|
||||
)
|
||||
|
||||
// With children
|
||||
if (item.type === 'submenu' && item.children?.length) {
|
||||
// With submenu
|
||||
if (item.type === 'submenu' && item.submenu?.length) {
|
||||
return (
|
||||
<li>
|
||||
<span className="block py-3 text-base font-semibold text-espresso">
|
||||
{item.label}
|
||||
</span>
|
||||
<ul className="border-l-2 border-warm-gray ml-2">
|
||||
{item.children.map((child) => (
|
||||
<MobileNavItem
|
||||
key={child.id}
|
||||
item={child}
|
||||
onClose={onClose}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{item.submenu.map((child) => (
|
||||
<MobileSubItem key={child.id} item={child} onClose={onClose} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
// External link
|
||||
if (item.type === 'external' && item.url) {
|
||||
// Link
|
||||
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 (
|
||||
<li>
|
||||
<a
|
||||
href={item.url}
|
||||
href={item.url!}
|
||||
target={item.openInNewTab ? '_blank' : undefined}
|
||||
rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
|
||||
className={linkClasses}
|
||||
|
|
@ -193,8 +187,21 @@ function MobileNavItem({ item, onClose, depth = 0 }: MobileNavItemProps) {
|
|||
)
|
||||
}
|
||||
|
||||
// Internal link
|
||||
const href = item.page?.slug ? `/${item.page.slug}` : item.url || '/'
|
||||
return (
|
||||
<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 (
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@
|
|||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
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 {
|
||||
items: NavigationItem[]
|
||||
items: MainMenuItem[]
|
||||
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) {
|
||||
return (
|
||||
<nav className={cn('flex items-center gap-8', className)}>
|
||||
|
|
@ -20,11 +27,7 @@ export function Navigation({ items, className }: NavigationProps) {
|
|||
)
|
||||
}
|
||||
|
||||
interface NavItemProps {
|
||||
item: NavigationItem
|
||||
}
|
||||
|
||||
function NavItem({ item }: NavItemProps) {
|
||||
function NavItem({ item }: { item: MainMenuItem }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const linkClasses = cn(
|
||||
|
|
@ -34,7 +37,7 @@ function NavItem({ item }: NavItemProps) {
|
|||
)
|
||||
|
||||
// Submenu
|
||||
if (item.type === 'submenu' && item.children?.length) {
|
||||
if (item.type === 'submenu' && item.submenu?.length) {
|
||||
return (
|
||||
<div
|
||||
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]">
|
||||
{item.children.map((child) => (
|
||||
<NavLink
|
||||
{item.submenu.map((child) => (
|
||||
<SubNavLink
|
||||
key={child.id}
|
||||
item={child}
|
||||
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
|
||||
return <NavLink item={item} className={linkClasses} />
|
||||
}
|
||||
const slug = getPageSlug(item.page)
|
||||
const href = item.type === 'page' && slug ? `/${slug}` : item.url || '/'
|
||||
|
||||
interface NavLinkProps {
|
||||
item: NavigationItem
|
||||
className?: string
|
||||
}
|
||||
|
||||
function NavLink({ item, className }: NavLinkProps) {
|
||||
if (item.type === 'external' && item.url) {
|
||||
if (item.type === 'custom' && item.url) {
|
||||
const isExternal = item.url.startsWith('http')
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
href={item.url}
|
||||
target={item.openInNewTab ? '_blank' : undefined}
|
||||
rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
|
||||
className={className}
|
||||
className={linkClasses}
|
||||
>
|
||||
{item.label}
|
||||
</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 (
|
||||
<Link href={href} className={className}>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
/**
|
||||
* Payload CMS API functions — powered by @c2s/payload-contracts
|
||||
*
|
||||
* Uses the shared API client for transport (tenant isolation, fetch caching)
|
||||
* but returns data typed with local interfaces for component compatibility.
|
||||
* Uses the shared API client for transport (tenant isolation, fetch caching).
|
||||
* Returns data typed with local interfaces for component compatibility.
|
||||
*
|
||||
* The local types use simplified interfaces (e.g. id: string, meta instead of seo)
|
||||
* while the contracts use the real CMS types. We use 'as unknown as' to bridge.
|
||||
* Navigation uses contracts types directly (schema matches).
|
||||
* 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 type { Navigation } from "@c2s/payload-contracts/types"
|
||||
import type {
|
||||
Page,
|
||||
Post,
|
||||
Navigation,
|
||||
Favorite,
|
||||
Series,
|
||||
Testimonial,
|
||||
|
|
@ -21,7 +22,8 @@ import type {
|
|||
SiteSettings,
|
||||
} 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"
|
||||
|
||||
// Pages
|
||||
|
|
@ -84,11 +86,11 @@ export async function getPosts(options: {
|
|||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
export async function getNavigation(type: "header" | "footer" | "mobile"): Promise<Navigation | null> {
|
||||
// Navigation — single document per tenant with mainMenu + footerMenu
|
||||
// Uses contracts Navigation type directly (no bridge needed)
|
||||
export async function getNavigation(): Promise<Navigation | null> {
|
||||
try {
|
||||
const result = await cms.navigation.getNavigation(type, { depth: 2 })
|
||||
return result as unknown as Navigation | null
|
||||
return await cms.navigation.getNavigation({ depth: 2 })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue