From 3a8693289f95426b06e93ae53fd3a20c12c2aad4 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Fri, 20 Feb 2026 15:25:16 +0000 Subject: [PATCH] feat: migrate all types from local bridge pattern to @c2s/payload-contracts Complete type migration removing all 33+ local interfaces and as-unknown-as casts. All components now use contracts types directly with type-safe relationship resolution via payload-helpers.ts. Key changes: - New payload-helpers.ts: resolveRelation, getMediaUrl, getMediaAlt, socialLinksToMap - types.ts: thin re-export layer from contracts (backward-compatible aliases) - api.ts: direct contracts types, no bridge casts, typed getSeoSettings - All 17 block components: correct CMS field names (headline, subline, cta group, etc.) - All route files: page.seo.metaTitle (not page.meta.title), getMediaUrl for unions - structuredData.ts: proper types for all schema generators - Footer: social links from separate collection via socialLinksToMap() - Header/Footer: resolveMedia for logo Co-Authored-By: Claude Opus 4.6 --- pnpm-lock.yaml | 8 +- src/app/[slug]/page.tsx | 12 +- src/app/aktuelles/[slug]/page.tsx | 21 +- src/app/blog/[slug]/page.tsx | 22 +- src/app/blog/page.tsx | 110 +--- src/app/coming-soon/page.tsx | 8 +- src/app/favoriten/page.tsx | 151 ++---- src/app/layout.tsx | 18 +- src/app/news/[slug]/page.tsx | 21 +- src/app/page.tsx | 11 +- src/app/presse/[slug]/page.tsx | 21 +- src/app/serien/[slug]/page.tsx | 22 +- src/app/serien/page.tsx | 77 +-- src/components/blocks/CTABlock.tsx | 73 ++- src/components/blocks/CardGridBlock.tsx | 71 ++- src/components/blocks/ContactFormBlock.tsx | 139 +----- src/components/blocks/DividerBlock.tsx | 35 +- src/components/blocks/FAQBlock.tsx | 114 ++--- src/components/blocks/FavoritesBlock.tsx | 151 ++---- src/components/blocks/HeroBlock.tsx | 46 +- src/components/blocks/ImageTextBlock.tsx | 43 +- src/components/blocks/NewsletterBlock.tsx | 118 +---- src/components/blocks/PostsListBlock.tsx | 321 ++++-------- src/components/blocks/SeriesBlock.tsx | 256 ++-------- src/components/blocks/SeriesDetailBlock.tsx | 114 ++--- src/components/blocks/StatsBlock.tsx | 119 +---- src/components/blocks/TestimonialsBlock.tsx | 158 ++---- src/components/blocks/TextBlock.tsx | 22 +- src/components/blocks/VideoBlock.tsx | 70 +-- src/components/blocks/VideoEmbedBlock.tsx | 119 ++--- src/components/blocks/index.tsx | 2 +- src/components/layout/Footer.tsx | 65 ++- src/components/layout/Header.tsx | 13 +- src/lib/api.ts | 84 ++-- src/lib/payload-helpers.ts | 74 +++ src/lib/structuredData.ts | 42 +- src/lib/types.ts | 523 ++------------------ 37 files changed, 896 insertions(+), 2378 deletions(-) create mode 100644 src/lib/payload-helpers.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b8c795..ea6a3fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@c2s/payload-contracts': specifier: github:complexcaresolutions/payload-contracts - version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#a0eea9649d35ec2a4554632554d53799a36b7f4b(react@19.2.1) + version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#d8e16db5357e5fbac9f701171894a4e641db3df8(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#a0eea9649d35ec2a4554632554d53799a36b7f4b': - resolution: {commit: a0eea9649d35ec2a4554632554d53799a36b7f4b, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git} + '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#d8e16db5357e5fbac9f701171894a4e641db3df8': + resolution: {commit: d8e16db5357e5fbac9f701171894a4e641db3df8, 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#a0eea9649d35ec2a4554632554d53799a36b7f4b(react@19.2.1)': + '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#d8e16db5357e5fbac9f701171894a4e641db3df8(react@19.2.1)': optionalDependencies: react: 19.2.1 diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 62b0518..b05ca98 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPage, getPages, getSeoSettings } from '@/lib/api' import { BlockRenderer } from '@/components/blocks' +import { getMediaUrl } from '@/lib/payload-helpers' import { generateBreadcrumbSchema } from '@/lib/structuredData' interface PageProps { @@ -17,7 +18,6 @@ export async function generateStaticParams() { slug: page.slug, })) } catch { - // Return empty array if API is unavailable during build return [] } } @@ -34,12 +34,12 @@ export async function generateMetadata({ params }: PageProps): Promise } const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || '' - const titleBase = page.meta?.title || page.title + const titleBase = page.seo?.metaTitle || page.title const title = titleSuffix ? `${titleBase} ${titleSuffix}` : titleBase const description = - page.meta?.description || seoSettings?.metaDefaults?.defaultDescription + page.seo?.metaDescription || seoSettings?.metaDefaults?.defaultDescription const image = - page.meta?.image?.url || seoSettings?.metaDefaults?.defaultImage?.url + getMediaUrl(page.seo?.ogImage) || getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage) return { title, @@ -56,10 +56,6 @@ export async function generateMetadata({ params }: PageProps): Promise description: description || undefined, images: image ? [image] : undefined, }, - robots: { - index: !page.meta?.noIndex, - follow: !page.meta?.noFollow, - }, } } diff --git a/src/app/aktuelles/[slug]/page.tsx b/src/app/aktuelles/[slug]/page.tsx index 308971b..ec8e921 100644 --- a/src/app/aktuelles/[slug]/page.tsx +++ b/src/app/aktuelles/[slug]/page.tsx @@ -2,7 +2,8 @@ import Image from 'next/image' import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPost, getSeoSettings, getSiteSettings } from '@/lib/api' -import { formatDate, getImageUrl } from '@/lib/utils' +import { formatDate } from '@/lib/utils' +import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers' import { RichTextRenderer } from '@/components/blocks' import { generateBlogPostingSchema, generateBreadcrumbSchema } from '@/lib/structuredData' @@ -20,14 +21,14 @@ export async function generateMetadata({ params }: AnnouncementPostPageProps): P if (!post) return {} const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || '' - const titleBase = post.meta?.title || post.title + const titleBase = post.seo?.metaTitle || post.title const title = titleSuffix ? `${titleBase} ${titleSuffix}` : titleBase const description = - post.meta?.description || post.excerpt || seoSettings?.metaDefaults?.defaultDescription + post.seo?.metaDescription || post.excerpt || seoSettings?.metaDefaults?.defaultDescription const image = - post.meta?.image?.url || - post.featuredImage?.url || - seoSettings?.metaDefaults?.defaultImage?.url + getMediaUrl(post.seo?.ogImage) || + getMediaUrl(post.featuredImage) || + getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage) return { title, @@ -44,10 +45,6 @@ export async function generateMetadata({ params }: AnnouncementPostPageProps): P description: description || undefined, images: image ? [image] : undefined, }, - robots: { - index: !post.meta?.noIndex, - follow: !post.meta?.noFollow, - }, } } @@ -62,7 +59,7 @@ export default async function AnnouncementPostPage({ params }: AnnouncementPostP notFound() } - const imageUrl = getImageUrl(post.featuredImage) + const imageUrl = getMediaUrl(post.featuredImage) const blogSchema = generateBlogPostingSchema(post, settings) const breadcrumbSchema = generateBreadcrumbSchema([ { name: 'Startseite', url: '/' }, @@ -94,7 +91,7 @@ export default async function AnnouncementPostPage({ params }: AnnouncementPostP
{post.featuredImage?.alt @@ -20,14 +22,14 @@ export async function generateMetadata({ params }: BlogPostPageProps): Promise {post.featuredImage?.alt

Blog

-

- Noch keine Artikel vorhanden. -

+

Noch keine Artikel vorhanden.

) @@ -32,24 +30,17 @@ export default async function BlogPage() { return ( <> - {/* Hero / Featured Post */}

Blog

- - {/* Featured Post */}
- - {/* Posts Grid */}

Alle Artikel

- {rest.map((post) => ( - - ))} + {rest.map((post) => )}
@@ -58,64 +49,32 @@ export default async function BlogPage() { } function FeaturedPostCard({ post }: { post: Post }) { - const imageUrl = getImageUrl(post.featuredImage) - const series = post.series as Series | undefined + const imageUrl = getMediaUrl(post.featuredImage) + const categories = resolveRelationArray(post.categories) return (
- {/* Image */}
{imageUrl && ( - {post.featuredImage?.alt + {getMediaAlt(post.featuredImage, )}
- - {/* Content */}
- {series && ( - {series.title} + {categories[0] && ( + {categories[0].name} )} -
- -

- {post.title} -

- - {post.excerpt && ( -

- {post.excerpt} -

- )} - +

{post.title}

+ {post.excerpt &&

{post.excerpt}

} Weiterlesen - - + +
@@ -125,49 +84,24 @@ function FeaturedPostCard({ post }: { post: Post }) { } function PostCard({ post }: { post: Post }) { - const imageUrl = getImageUrl(post.featuredImage) - const series = post.series as Series | undefined + const imageUrl = getMediaUrl(post.featuredImage) + const categories = resolveRelationArray(post.categories) return (
- {/* Image */}
- {imageUrl && ( - {post.featuredImage?.alt - )} + {imageUrl && {getMediaAlt(post.featuredImage,}
- - {/* Content */}
- {series && ( - - {series.title} - - )} -
- -

- {post.title} -

- - {post.excerpt && ( -

- {post.excerpt} -

- )} +

{post.title}

+ {post.excerpt &&

{post.excerpt}

}
diff --git a/src/app/coming-soon/page.tsx b/src/app/coming-soon/page.tsx index 9d1f8b8..1d756be 100644 --- a/src/app/coming-soon/page.tsx +++ b/src/app/coming-soon/page.tsx @@ -1,6 +1,7 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPage, getSiteSettings } from '@/lib/api' +import { getMediaUrl } from '@/lib/payload-helpers' import { BlockRenderer } from '@/components/blocks' import { ComingSoonWrapper } from './ComingSoonWrapper' @@ -16,10 +17,11 @@ export async function generateMetadata(): Promise { } } - const title = page.meta?.title || page.title || 'Kommt bald' + const title = page.seo?.metaTitle || page.title || 'Kommt bald' const description = - page.meta?.description || + page.seo?.metaDescription || `${settings?.siteName || 'BlogWoman'} - Kommt bald. Sei dabei, wenn es losgeht.` + const ogImage = getMediaUrl(page.seo?.ogImage) return { title, @@ -30,7 +32,7 @@ export async function generateMetadata(): Promise { type: 'website', locale: 'de_DE', siteName: settings?.siteName || 'BlogWoman', - images: page.meta?.image?.url ? [{ url: page.meta.image.url }] : undefined, + images: ogImage ? [{ url: ogImage }] : undefined, }, twitter: { card: 'summary_large_image', diff --git a/src/app/favoriten/page.tsx b/src/app/favoriten/page.tsx index 95cde72..17577b1 100644 --- a/src/app/favoriten/page.tsx +++ b/src/app/favoriten/page.tsx @@ -1,9 +1,9 @@ import { Metadata } from 'next' import Image from 'next/image' import { getFavorites } from '@/lib/api' -import { getImageUrl } from '@/lib/utils' +import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers' import { Badge } from '@/components/ui' -import type { Favorite } from '@/lib/types' +import type { Favorite } from '@c2s/payload-contracts/types' export const metadata: Metadata = { title: 'Favoriten', @@ -21,28 +21,21 @@ const badgeLabels: Record = { const categoryLabels: Record = { fashion: 'Mode', beauty: 'Beauty', - lifestyle: 'Lifestyle', + travel: 'Reisen', home: 'Zuhause', tech: 'Tech', - books: 'Bücher', } export default async function FavoritenPage() { const favoritesData = await getFavorites({ limit: 50 }) const favorites = favoritesData.docs - // Group by category - const groupedFavorites = favorites.reduce>( - (acc, fav) => { - const category = fav.category || 'other' - if (!acc[category]) { - acc[category] = [] - } - acc[category].push(fav) - return acc - }, - {} - ) + const groupedFavorites = favorites.reduce>((acc, fav) => { + const category = fav.category || 'other' + if (!acc[category]) acc[category] = [] + acc[category].push(fav) + return acc + }, {}) const categories = Object.keys(groupedFavorites) @@ -51,9 +44,7 @@ export default async function FavoritenPage() {

Favoriten

-

- Noch keine Favoriten vorhanden. -

+

Noch keine Favoriten vorhanden.

) @@ -61,28 +52,21 @@ export default async function FavoritenPage() { return ( <> - {/* Hero */}

Meine Favoriten

- Produkte, die ich liebe und guten Gewissens empfehlen kann. Von - Fashion-Klassikern bis zu Lifestyle-Essentials. + Produkte, die ich liebe und guten Gewissens empfehlen kann. Von Fashion-Klassikern bis zu Lifestyle-Essentials.

- {/* Category Navigation */} {categories.length > 1 && ( )} - {/* Favorites by Category */} {categories.map((category) => ( -
+
-

- {categoryLabels[category] || category} -

- +

{categoryLabels[category] || category}

- {groupedFavorites[category].map((favorite) => ( - - ))} + {groupedFavorites[category].map((favorite) => )}
))} - {/* Affiliate Disclosure */}
- - + +

Hinweis zu Affiliate-Links

- Einige Links auf dieser Seite sind Affiliate-Links. Das bedeutet, - dass ich eine kleine Provision erhalte, wenn du über diese Links - einkaufst - ohne Mehrkosten für dich. So kannst du meine Arbeit - unterstützen, während du Produkte entdeckst, die ich wirklich - liebe. + Einige Links auf dieser Seite sind Affiliate-Links. Das bedeutet, dass ich eine kleine Provision erhalte, wenn du über diese Links einkaufst - ohne Mehrkosten für dich.

@@ -148,86 +106,35 @@ export default async function FavoritenPage() { } function FavoriteCard({ favorite }: { favorite: Favorite }) { - const imageUrl = getImageUrl(favorite.image) + const imageUrl = getMediaUrl(favorite.image) return ( - +
- {/* Image */}
- {imageUrl && ( - {favorite.image?.alt - )} - - {/* Badge */} + {imageUrl && {getMediaAlt(favorite.image,} {favorite.badge && (
- + {badgeLabels[favorite.badge]}
)}
- - {/* Content */}
- {favorite.category && ( -

- {categoryLabels[favorite.category] || favorite.category} -

- )} - -

- {favorite.title} -

- - {favorite.description && ( -

- {favorite.description} -

- )} - - {/* Footer */} + {favorite.category &&

{categoryLabels[favorite.category] || favorite.category}

} +

{favorite.title}

+ {favorite.description &&

{favorite.description}

}
- {favorite.price && ( + {favorite.price != null && ( - {favorite.price} + {typeof favorite.price === 'number' ? `${favorite.price.toFixed(2)} €` : favorite.price} )} - Ansehen - - + +
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0bf6475..b2b5bbc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,8 @@ import type { Metadata } from 'next' import { Playfair_Display, Inter } from 'next/font/google' import { Header, Footer } from '@/components/layout' import { UmamiScript } from '@/components/analytics' -import { getSeoSettings, getSiteSettings, getNavigation } from '@/lib/api' +import { getSeoSettings, getSiteSettings, getNavigation, getSocialLinks } from '@/lib/api' +import { getMediaUrl } from '@/lib/payload-helpers' import './globals.css' const playfair = Playfair_Display({ @@ -25,10 +26,10 @@ export async function generateMetadata(): Promise { const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || '' const defaultDescription = seoSettings?.metaDefaults?.defaultDescription || - settings?.siteDescription || - 'Lifestyle-Blog für moderne Frauen' - const defaultImage = seoSettings?.metaDefaults?.defaultImage?.url - const canIndex = seoSettings?.robots?.indexing !== false + settings?.seo?.defaultMetaDescription || + 'Lifestyle-Blog fuer moderne Frauen' + const defaultImage = getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage) || getMediaUrl(settings?.seo?.defaultOgImage) + const canIndex = seoSettings?.robots?.allowIndexing !== false const verificationOther: Record = {} if (seoSettings?.verification?.bing) { @@ -71,16 +72,15 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { - // Fetch navigation (one doc per tenant) and settings in parallel - const [navigation, settings] = await Promise.all([ + const [navigation, settings, socialLinks] = await Promise.all([ getNavigation(), getSiteSettings(), + getSocialLinks(), ]) return ( - {/* Skip to main content link for accessibility */}
{children}
-
+
diff --git a/src/app/news/[slug]/page.tsx b/src/app/news/[slug]/page.tsx index 2863778..3437b31 100644 --- a/src/app/news/[slug]/page.tsx +++ b/src/app/news/[slug]/page.tsx @@ -2,7 +2,8 @@ import Image from 'next/image' import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPost, getSeoSettings, getSiteSettings } from '@/lib/api' -import { formatDate, getImageUrl } from '@/lib/utils' +import { formatDate } from '@/lib/utils' +import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers' import { RichTextRenderer } from '@/components/blocks' import { generateBlogPostingSchema, generateBreadcrumbSchema } from '@/lib/structuredData' @@ -20,14 +21,14 @@ export async function generateMetadata({ params }: NewsPostPageProps): Promise {post.featuredImage?.alt { @@ -15,12 +16,12 @@ export async function generateMetadata(): Promise { } const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || '' - const titleBase = page.meta?.title || page.title + const titleBase = page.seo?.metaTitle || page.title const title = titleSuffix ? `${titleBase} ${titleSuffix}` : titleBase const description = - page.meta?.description || seoSettings?.metaDefaults?.defaultDescription + page.seo?.metaDescription || seoSettings?.metaDefaults?.defaultDescription const image = - page.meta?.image?.url || seoSettings?.metaDefaults?.defaultImage?.url + getMediaUrl(page.seo?.ogImage) || getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage) return { title, @@ -37,10 +38,6 @@ export async function generateMetadata(): Promise { description: description || undefined, images: image ? [image] : undefined, }, - robots: { - index: !page.meta?.noIndex, - follow: !page.meta?.noFollow, - }, } } diff --git a/src/app/presse/[slug]/page.tsx b/src/app/presse/[slug]/page.tsx index 8400dbb..1fd3ee8 100644 --- a/src/app/presse/[slug]/page.tsx +++ b/src/app/presse/[slug]/page.tsx @@ -2,7 +2,8 @@ import Image from 'next/image' import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { getPost, getSeoSettings, getSiteSettings } from '@/lib/api' -import { formatDate, getImageUrl } from '@/lib/utils' +import { formatDate } from '@/lib/utils' +import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers' import { RichTextRenderer } from '@/components/blocks' import { generateBlogPostingSchema, generateBreadcrumbSchema } from '@/lib/structuredData' @@ -20,14 +21,14 @@ export async function generateMetadata({ params }: PressPostPageProps): Promise< if (!post) return {} const titleSuffix = seoSettings?.metaDefaults?.titleSuffix || '' - const titleBase = post.meta?.title || post.title + const titleBase = post.seo?.metaTitle || post.title const title = titleSuffix ? `${titleBase} ${titleSuffix}` : titleBase const description = - post.meta?.description || post.excerpt || seoSettings?.metaDefaults?.defaultDescription + post.seo?.metaDescription || post.excerpt || seoSettings?.metaDefaults?.defaultDescription const image = - post.meta?.image?.url || - post.featuredImage?.url || - seoSettings?.metaDefaults?.defaultImage?.url + getMediaUrl(post.seo?.ogImage) || + getMediaUrl(post.featuredImage) || + getMediaUrl(seoSettings?.metaDefaults?.defaultOgImage) return { title, @@ -44,10 +45,6 @@ export async function generateMetadata({ params }: PressPostPageProps): Promise< description: description || undefined, images: image ? [image] : undefined, }, - robots: { - index: !post.meta?.noIndex, - follow: !post.meta?.noFollow, - }, } } @@ -62,7 +59,7 @@ export default async function PressPostPage({ params }: PressPostPageProps) { notFound() } - const imageUrl = getImageUrl(post.featuredImage) + const imageUrl = getMediaUrl(post.featuredImage) const blogSchema = generateBlogPostingSchema(post, settings) const breadcrumbSchema = generateBreadcrumbSchema([ { name: 'Startseite', url: '/' }, @@ -94,7 +91,7 @@ export default async function PressPostPage({ params }: PressPostPageProps) {
{post.featuredImage?.alt

Serien

-

- Noch keine Serien vorhanden. -

+

Noch keine Serien vorhanden.

) @@ -30,24 +28,18 @@ export default async function SerienPage() { return ( <> - {/* Hero */}

Meine Serien

- Entdecke meine YouTube-Serien zu verschiedenen Themen rund um - Lifestyle, Mode und mehr. + Entdecke meine YouTube-Serien zu verschiedenen Themen rund um Lifestyle, Mode und mehr.

- - {/* Series Grid */}
- {allSeries.map((series) => ( - - ))} + {allSeries.map((series) => )}
@@ -56,66 +48,27 @@ export default async function SerienPage() { } function SeriesCard({ series }: { series: Series }) { - const _imageUrl = getImageUrl(series.coverImage) || getImageUrl(series.logo) + const coverUrl = getMediaUrl(series.coverImage) + const logoUrl = getMediaUrl(series.logo) return ( -
- {/* Background Image */} - {series.coverImage && ( - - )} - - {/* Content */} +
+ {coverUrl && }
- {/* Logo */} - {series.logo && ( + {logoUrl && (
- + {getMediaAlt(series.logo)}
)} - - {/* Title */} -

- {series.title} -

- - {/* Description */} +

{series.title}

{series.description && ( -
- -
+
)} - - {/* Link */} Zur Serie - - + +
diff --git a/src/components/blocks/CTABlock.tsx b/src/components/blocks/CTABlock.tsx index 53a3854..200fb65 100644 --- a/src/components/blocks/CTABlock.tsx +++ b/src/components/blocks/CTABlock.tsx @@ -1,56 +1,53 @@ -import Image from 'next/image' import { Button } from '@/components/ui' import { cn } from '@/lib/utils' -import type { CTABlock as CTABlockType } from '@/lib/types' +import type { BlockByType } from '@c2s/payload-contracts/types' -type CTABlockProps = Omit +type CTABlockProps = Omit, 'blockType' | 'blockName'> export function CTABlock({ - heading, - subheading, - buttonText, - buttonLink, - backgroundColor = 'brass', - backgroundImage, + headline, + description, + buttons, + backgroundColor = 'dark', }: CTABlockProps) { - const bgClasses = { - brass: 'bg-brass', - espresso: 'bg-espresso', - bordeaux: 'bg-bordeaux', + const bgClasses: Record = { + dark: 'bg-espresso', + light: 'bg-ivory', + accent: 'bg-brass', } - return ( -
- {/* Background Image */} - {backgroundImage?.url && ( -
- -
- )} + const firstButton = buttons?.[0] + return ( +
-

{heading}

+

+ {headline} +

- {subheading && ( -

- {subheading} + {description && ( +

+ {description}

)} - + {firstButton && ( + + )}
diff --git a/src/components/blocks/CardGridBlock.tsx b/src/components/blocks/CardGridBlock.tsx index c7c50ef..14b48e1 100644 --- a/src/components/blocks/CardGridBlock.tsx +++ b/src/components/blocks/CardGridBlock.tsx @@ -1,68 +1,63 @@ import Image from 'next/image' import Link from 'next/link' import { cn } from '@/lib/utils' -import type { CardGridBlock as CardGridBlockType } from '@/lib/types' +import { getMediaUrl, getMediaAlt } from '@/lib/payload-helpers' +import type { BlockByType } from '@c2s/payload-contracts/types' -type CardGridBlockProps = Omit +type CardGridBlockProps = Omit, 'blockType' | 'blockName'> export function CardGridBlock({ - title, - subtitle, + headline, cards, - columns = 3, + columns = '3', }: CardGridBlockProps) { - const columnClasses = { - 2: 'md:grid-cols-2', - 3: 'md:grid-cols-2 lg:grid-cols-3', - 4: 'md:grid-cols-2 lg:grid-cols-4', + const columnClasses: Record = { + '2': 'md:grid-cols-2', + '3': 'md:grid-cols-2 lg:grid-cols-3', + '4': 'md:grid-cols-2 lg:grid-cols-4', } return (
{/* Section Header */} - {(title || subtitle) && ( + {headline && (
- {title &&

{title}

} - {subtitle && ( -

{subtitle}

- )} +

{headline}

)} {/* Card Grid */} -
- {cards.map((card, index) => ( - - ))} -
+ {cards && cards.length > 0 && ( +
+ {cards.map((card, index) => ( + + ))} +
+ )}
) } -interface CardItemProps { - title: string - description?: string - image?: { url: string; alt?: string } - link?: string - icon?: string -} +type CardData = NonNullable[number] + +function CardItem({ card }: { card: CardData }) { + const imgUrl = getMediaUrl(card.image) -function CardItem({ title, description, image, link, icon }: CardItemProps) { const content = (
- {image?.url && ( + {imgUrl && (
{image.alt @@ -70,24 +65,24 @@ function CardItem({ title, description, image, link, icon }: CardItemProps) { )}
- {icon && ( + {card.icon && (
- {icon} + {card.icon}
)} -

{title}

+

{card.title}

- {description && ( -

{description}

+ {card.description && ( +

{card.description}

)}
) - if (link) { + if (card.link) { return ( - + {content} ) diff --git a/src/components/blocks/ContactFormBlock.tsx b/src/components/blocks/ContactFormBlock.tsx index d3eed6c..062d625 100644 --- a/src/components/blocks/ContactFormBlock.tsx +++ b/src/components/blocks/ContactFormBlock.tsx @@ -1,21 +1,21 @@ 'use client' import { useState } from 'react' -import { Button, Input, Textarea } from '@/components/ui' +import { Button, Input } from '@/components/ui' import { submitContactForm } from '@/lib/api' -import type { ContactFormBlock as ContactFormBlockType } from '@/lib/types' +import type { BlockByType } from '@c2s/payload-contracts/types' -type ContactFormBlockProps = Omit +type ContactFormBlockProps = Omit, 'blockType' | 'blockName'> export function ContactFormBlock({ - title, - subtitle, - formId, - showName = true, + headline, + description, + form: formRef, showPhone = false, - showSubject = true, successMessage = 'Vielen Dank für Ihre Nachricht! Wir melden uns zeitnah bei Ihnen.', }: ContactFormBlockProps) { + const formId = typeof formRef === 'object' && formRef !== null ? formRef.id : (formRef as number | undefined) + const [formData, setFormData] = useState({ name: '', email: '', @@ -26,26 +26,16 @@ export function ContactFormBlock({ const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') const [errorMessage, setErrorMessage] = useState('') - function handleChange( - e: React.ChangeEvent - ) { - setFormData((prev) => ({ - ...prev, - [e.target.name]: e.target.value, - })) + function handleChange(e: React.ChangeEvent) { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })) } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setStatus('loading') setErrorMessage('') - try { - const result = await submitContactForm({ - ...formData, - formId, - }) - + const result = await submitContactForm({ ...formData, formId }) if (result.success) { setStatus('success') setFormData({ name: '', email: '', phone: '', subject: '', message: '' }) @@ -63,111 +53,36 @@ export function ContactFormBlock({
- {/* Header */} - {(title || subtitle) && ( + {(headline || description) && (
- {title &&

{title}

} - {subtitle && ( -

{subtitle}

- )} + {headline &&

{headline}

} + {description &&

{description}

}
)} - - {/* Success Message */} {status === 'success' ? (
- - + +

{successMessage}

) : (
- {/* Name & Email Row */}
- {showName && ( - - )} - + +
- - {/* Phone & Subject Row */} - {(showPhone || showSubject) && ( -
- {showPhone && ( - - )} - {showSubject && ( - - )} -
+ {showPhone && ( + )} - - {/* Message */} -