mirror of
https://github.com/complexcaresolutions/frontend.blogwoman.de.git
synced 2026-03-17 16:14:00 +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:
|
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
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- "@c2s/payload-contracts"
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
<h3 className="text-xs font-bold tracking-[0.1em] uppercase text-brass mb-4">
|
||||||
<div key={index}>
|
Links
|
||||||
{group.title && (
|
</h3>
|
||||||
<h3 className="text-xs font-bold tracking-[0.1em] uppercase text-brass mb-4">
|
<ul className="grid grid-cols-2 gap-2">
|
||||||
{group.title}
|
{footerMenu.map((item) => (
|
||||||
</h3>
|
<li key={item.id}>
|
||||||
)}
|
<FooterLink item={item} />
|
||||||
<ul className="space-y-2">
|
</li>
|
||||||
{group.items.map((item) => (
|
))}
|
||||||
<li key={item.id}>
|
</ul>
|
||||||
<FooterLink item={item} />
|
</div>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,30 +93,46 @@ 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 || '/'
|
||||||
|
|
||||||
|
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={linkClasses}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href} className={linkClasses}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavLinkProps {
|
type SubMenuItem = NonNullable<MainMenuItem['submenu']>[number]
|
||||||
item: NavigationItem
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavLink({ item, className }: NavLinkProps) {
|
function SubNavLink({ item, className }: { item: SubMenuItem; className?: string }) {
|
||||||
if (item.type === 'external' && item.url) {
|
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 (
|
return (
|
||||||
<a
|
<a href={item.url} className={className} target="_blank" rel="noopener noreferrer">
|
||||||
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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue