frontend.blogwoman.de/src/components/blocks/NewsletterBlock.tsx
CCS Admin 3a8693289f 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 <noreply@anthropic.com>
2026-02-20 15:25:16 +00:00

104 lines
4.1 KiB
TypeScript

'use client'
import { useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Button, Input } from '@/components/ui'
import { cn } from '@/lib/utils'
import { getMediaUrl } from '@/lib/payload-helpers'
import { subscribeNewsletter } from '@/lib/api'
import type { BlockByType } from '@c2s/payload-contracts/types'
type NewsletterBlockProps = Omit<BlockByType<'newsletter-block'>, 'blockType' | 'blockName'>
export function NewsletterBlock({
title = 'Newsletter',
subtitle,
buttonText = 'Anmelden',
layout = 'card',
image,
collectName = false,
source = 'newsletter-block',
}: NewsletterBlockProps) {
const [email, setEmail] = useState('')
const [firstName, setFirstName] = useState('')
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const [errorMessage, setErrorMessage] = useState('')
const bgImageUrl = getMediaUrl(image)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setStatus('loading')
setErrorMessage('')
try {
const result = await subscribeNewsletter(
email,
collectName && firstName ? firstName : undefined,
source || 'newsletter-block'
)
if (result.success) {
setStatus('success')
setEmail('')
} else {
setStatus('error')
setErrorMessage(result.message || 'Ein Fehler ist aufgetreten.')
}
} catch {
setStatus('error')
setErrorMessage('Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.')
}
}
const formContent = (
<>
{status === 'success' ? (
<div className="p-4 bg-success/10 text-success rounded-lg text-center">
<p className="font-medium">Vielen Dank für Ihre Anmeldung!</p>
<p className="text-sm mt-1">Bitte bestätigen Sie Ihre E-Mail-Adresse.</p>
</div>
) : (
<form onSubmit={handleSubmit} className={cn('flex flex-col gap-4', (layout === 'inline' || layout === 'minimal') && !collectName && 'sm:flex-row')}>
{collectName && (
<Input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} placeholder="Dein Vorname (optional)" disabled={status === 'loading'} />
)}
<div className={cn('flex flex-col gap-4', (layout === 'inline' || layout === 'minimal') && 'sm:flex-row')}>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Deine E-Mail-Adresse" required disabled={status === 'loading'} error={status === 'error' ? errorMessage : undefined} className={cn((layout === 'inline' || layout === 'minimal') && 'sm:flex-1')} />
<Button type="submit" disabled={status === 'loading'} className="whitespace-nowrap">
{status === 'loading' ? 'Wird gesendet...' : buttonText || 'Anmelden'}
</Button>
</div>
</form>
)}
{status !== 'success' && (
<p className="text-sm text-warm-gray-dark mt-4 text-center">
Mit der Anmeldung akzeptieren Sie unsere{' '}
<Link href="/datenschutz" className="underline hover:text-espresso">Datenschutzerklärung</Link>.
</p>
)}
</>
)
if (layout === 'minimal') {
return <section className="py-8"><div className="container max-w-xl">{formContent}</div></section>
}
return (
<section className="py-16 md:py-20">
<div className="container">
<div className={cn('relative bg-soft-white border border-warm-gray rounded-2xl p-8 md:p-10 overflow-hidden', bgImageUrl && 'text-soft-white')}>
{bgImageUrl && (
<div className="absolute inset-0">
<Image src={bgImageUrl} alt="" fill className="object-cover" />
<div className="absolute inset-0 bg-espresso/60" />
</div>
)}
<div className="relative z-10 max-w-xl mx-auto text-center">
{title && <h2 className={cn('mb-3', bgImageUrl && 'text-soft-white')}>{title}</h2>}
{subtitle && <p className={cn('text-lg mb-6', bgImageUrl ? 'text-soft-white/80' : 'text-espresso/80')}>{subtitle}</p>}
{formContent}
</div>
</div>
</div>
</section>
)
}