mirror of
https://github.com/complexcaresolutions/frontend.blogwoman.de.git
synced 2026-03-17 16:14:00 +00:00
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>
104 lines
4.1 KiB
TypeScript
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>
|
|
)
|
|
}
|