336 lines
16 KiB
TypeScript
336 lines
16 KiB
TypeScript
import Link from 'next/link'
|
||
import SeoIcon, { type SeoIconName } from '@/components/seo/SeoIcon'
|
||
import type { SeoPageProfile } from '@/lib/seoPages'
|
||
import type { SeoTemplateCopy } from '@/lib/seoTemplateCopy'
|
||
|
||
interface SectionProps {
|
||
profile: SeoPageProfile
|
||
copy: SeoTemplateCopy
|
||
}
|
||
|
||
export function SeoHero({
|
||
profile,
|
||
copy,
|
||
secondaryHref = '#methodology',
|
||
}: SectionProps & { secondaryHref?: string }) {
|
||
return (
|
||
<section className="max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 lg:grid-cols-12 gap-12 items-start mt-8">
|
||
<div className="lg:col-span-6 z-10 flex flex-col justify-center pt-8">
|
||
<div className="font-body text-xs font-semibold tracking-[0.1em] uppercase text-primary mb-6 seo-fade">
|
||
{copy.hero.eyebrow}
|
||
</div>
|
||
<h1 className="font-display font-bold text-[40px] leading-[1.2] tracking-[-0.01em] md:text-[56px] md:leading-[1.1] md:tracking-[-0.02em] text-primary mb-6 seo-fade seo-fade-1">
|
||
{profile.h1}
|
||
</h1>
|
||
<p className="font-body text-lg text-on-surface-variant mb-4 leading-relaxed seo-fade seo-fade-2">{profile.tagline}</p>
|
||
<p className="font-body text-base text-on-surface-variant mb-10 leading-relaxed seo-fade seo-fade-2">{profile.directAnswer}</p>
|
||
<div className="flex flex-col sm:flex-row gap-4 seo-fade seo-fade-3">
|
||
<a
|
||
className="inline-flex justify-center items-center px-8 py-3 bg-ink text-white rounded-full font-bold shadow-lg hover:bg-ink-soft transition-colors duration-300"
|
||
href="#download"
|
||
>
|
||
{copy.hero.ctaPrimary}
|
||
</a>
|
||
<a
|
||
className="inline-flex justify-center items-center px-8 py-3 bg-transparent border-2 border-ink text-ink rounded-full font-bold hover:bg-ink hover:text-white transition-colors duration-300"
|
||
href={secondaryHref}
|
||
>
|
||
{copy.hero.ctaSecondary}
|
||
</a>
|
||
</div>
|
||
<ul className="mt-6 flex flex-wrap gap-x-5 gap-y-2 seo-fade seo-fade-3">
|
||
{profile.timeCue && (
|
||
<li className="flex items-center gap-2 text-sm text-on-surface-variant">
|
||
<SeoIcon name="check_circle" className="w-4 h-4 shrink-0 text-primary" />
|
||
<span>{profile.timeCue}</span>
|
||
</li>
|
||
)}
|
||
{copy.hero.trustChips.map((chip) => (
|
||
<li key={chip} className="flex items-center gap-2 text-sm text-on-surface-variant">
|
||
<SeoIcon name="check_circle" className="w-4 h-4 shrink-0 text-primary" />
|
||
<span>{chip}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<div className="mt-12 grid grid-cols-2 gap-6 pt-8 border-t border-surface-container-high seo-fade seo-fade-3">
|
||
<div>
|
||
<div className="font-display text-2xl font-semibold text-primary mb-1">{copy.hero.stat1Title}</div>
|
||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{copy.hero.stat1Label}</div>
|
||
</div>
|
||
<div>
|
||
<div className="font-display text-2xl font-semibold text-primary mb-1">{copy.hero.stat2Title}</div>
|
||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{copy.hero.stat2Label}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="lg:col-span-6 flex justify-center items-center seo-fade seo-fade-2">
|
||
<div className="relative w-full max-w-[320px] md:max-w-[380px] h-[550px] md:h-[650px] rounded-3xl overflow-hidden shadow-2xl border-[8px] border-surface-container">
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
alt={profile.heroImageAlt ?? copy.hero.imageAlt}
|
||
className="w-full h-full object-cover"
|
||
src={profile.heroImage ?? '/hero.jpg'}
|
||
/>
|
||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 w-[90%] bg-surface/95 backdrop-blur p-4 rounded-xl border border-surface-container-highest flex items-center gap-3 shadow-xl">
|
||
<div className="w-1.5 h-8 bg-primary rounded-full"></div>
|
||
<div>
|
||
<div className="font-display font-semibold text-sm text-primary mb-0.5">{profile.heroImageBadgeTitle ?? copy.hero.floatTitle}</div>
|
||
<div className="text-[10px] font-semibold tracking-[0.1em] uppercase text-on-surface-variant">{profile.heroImageBadgeLabel ?? copy.hero.floatLabel}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoAdvantage({
|
||
profile,
|
||
copy,
|
||
showInsights = false,
|
||
}: SectionProps & { showInsights?: boolean }) {
|
||
// Echtes Prüfdatum aus dem Profil. Kein globales Datum — eine Seite, die seit
|
||
// April unverändert ist, darf kein aktuelleres Datum behaupten.
|
||
const reviewedAt = profile.lastUpdated
|
||
|
||
return (
|
||
<section className={`max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 ${showInsights ? 'lg:grid-cols-3' : ''} gap-12 border-t border-surface-container-high`} id="advantage">
|
||
<div className={showInsights ? 'lg:col-span-2' : ''}>
|
||
<div className="mb-8">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary">{copy.advantage.eyebrow}</span>
|
||
</div>
|
||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary mb-6">{copy.advantage.heading}</h2>
|
||
<p className="font-body text-lg text-on-surface-variant mb-6">{profile.definitionBlock}</p>
|
||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant">
|
||
{copy.advantage.updated}: {reviewedAt}
|
||
</div>
|
||
</div>
|
||
{showInsights && <div className="bg-surface-container rounded-xl p-8 seo-card">
|
||
<div className="flex items-center gap-3 mb-6">
|
||
<SeoIcon name="psychology" className="w-8 h-8 text-primary" />
|
||
<h3 className="font-display font-semibold text-2xl text-primary">{copy.didYouKnow.title}</h3>
|
||
</div>
|
||
<ul className="space-y-6">
|
||
{copy.didYouKnow.items.map((item, index) => (
|
||
<li key={item.title} className={index < copy.didYouKnow.items.length - 1 ? 'border-b border-outline-variant/30 pb-4' : ''}>
|
||
<strong className="block font-body text-primary mb-1">{item.title}</strong>
|
||
<span className="text-sm text-on-surface-variant">{item.body}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>}
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoMethodology({
|
||
copy,
|
||
intent = 'identification',
|
||
}: {
|
||
copy: SeoTemplateCopy
|
||
intent?: 'identification' | 'diagnosis'
|
||
}) {
|
||
const methodology = intent === 'diagnosis' ? copy.diagnosisMethodology : copy.methodology
|
||
|
||
return (
|
||
<section className="bg-surface-container-low py-16 md:py-[120px]" id="methodology">
|
||
<div className="max-w-[1200px] mx-auto px-6">
|
||
<div className="text-center mb-16">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{methodology.eyebrow}</span>
|
||
<h2 className="font-display font-semibold text-[32px] md:text-[40px] leading-[1.2] text-primary">{methodology.title}</h2>
|
||
<p className="font-body text-on-surface-variant max-w-2xl mx-auto mt-4">{methodology.lead}</p>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||
{methodology.steps.map((step, index) => (
|
||
<div key={step.title} className="bg-surface p-8 rounded-xl shadow-sm border border-surface-container-high seo-card">
|
||
<div className="w-12 h-12 bg-primary-container text-on-primary-container rounded-full flex items-center justify-center font-display font-semibold text-xl mb-6">
|
||
{index + 1}
|
||
</div>
|
||
<h3 className="font-display font-semibold text-lg text-primary mb-3">{step.title}</h3>
|
||
<p className="font-body text-on-surface-variant text-sm">{step.body}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
const comparisonIcons: SeoIconName[] = ['check_circle', 'calendar', 'healing', 'sun']
|
||
|
||
export function SeoComparison({
|
||
profile,
|
||
copy,
|
||
intent = 'identification',
|
||
}: SectionProps & { intent?: 'identification' | 'diagnosis' | 'care' | 'watering' }) {
|
||
const tipLink = profile.relatedLinks[0]
|
||
|
||
return (
|
||
<section className="py-16 md:py-24" id="comparison">
|
||
<div className="max-w-[1200px] mx-auto px-6 grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||
<div className="lg:col-span-4">
|
||
<div className="lg:sticky lg:top-32">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{copy.comparison.eyebrow}</span>
|
||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary mb-6">{profile.featureTable.title}</h2>
|
||
<p className="font-body text-on-surface-variant mb-8">{copy.comparison.intentLead[intent]}</p>
|
||
{tipLink && (
|
||
<div className="bg-primary-container/20 p-6 rounded-lg border border-primary-container/30">
|
||
<h4 className="font-display font-semibold text-base text-primary mb-2">{copy.comparison.tipTitle}</h4>
|
||
<p className="text-sm text-on-surface-variant mb-3">{copy.comparison.tipBody}</p>
|
||
<Link className="text-primary text-xs font-semibold tracking-[0.1em] uppercase hover:underline" href={tipLink.href}>
|
||
{tipLink.label} →
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="lg:col-span-8 flex flex-col border-t border-surface-container-highest">
|
||
{profile.featureTable.rows.map((row, index) => (
|
||
<div key={row.feature} className="grid grid-cols-1 md:grid-cols-2 gap-8 py-8 border-b border-surface-container-highest items-start">
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<SeoIcon name={comparisonIcons[index % comparisonIcons.length]} className="w-5 h-5 text-primary" />
|
||
<h3 className="font-display font-semibold text-lg text-primary">{row.feature}</h3>
|
||
</div>
|
||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-primary mb-2">{copy.comparison.greenlens}</div>
|
||
<p className="font-body text-on-surface-variant text-sm">{row.greenlens}</p>
|
||
</div>
|
||
<div className="md:pt-9">
|
||
<div className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant/60 mb-2">
|
||
{profile.featureTable.alternativeLabel}
|
||
</div>
|
||
<p className="font-body text-on-surface-variant/80 text-sm">{row.alternative}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoContentSections({ profile }: { profile: SeoPageProfile }) {
|
||
if (!profile.contentSections || profile.contentSections.length === 0) return null
|
||
|
||
return (
|
||
<section className="bg-surface-container-low py-16">
|
||
<div className="max-w-[1200px] mx-auto px-6 grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||
{profile.contentSections.map((section) => (
|
||
<article key={section.title} className="bg-surface p-8 rounded-xl shadow-sm border border-surface-container-high seo-card">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{section.eyebrow}</span>
|
||
<h2 className="font-display font-semibold text-2xl text-primary mb-4">{section.title}</h2>
|
||
<p className="font-body text-on-surface-variant text-sm mb-4">{section.body}</p>
|
||
{section.bullets && (
|
||
<ul className="space-y-3">
|
||
{section.bullets.map((item) => (
|
||
<li key={item} className="flex items-start gap-2 text-sm text-on-surface-variant">
|
||
<SeoIcon name="check_circle" className="w-4 h-4 mt-0.5 shrink-0 text-primary" />
|
||
<span>{item}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</article>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoOfferBlock({ profile }: { profile: SeoPageProfile }) {
|
||
if (!profile.offer) return null
|
||
|
||
const { eyebrow, title, body, terms, ctaLabel } = profile.offer
|
||
|
||
return (
|
||
<section className="max-w-[1200px] mx-auto px-6 py-16">
|
||
<div className="bg-surface rounded-2xl p-8 md:p-12 border border-surface-container-high shadow-sm">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-4">{eyebrow}</span>
|
||
<h2 className="font-display font-semibold text-2xl md:text-3xl text-primary mb-4 max-w-[26ch]">{title}</h2>
|
||
<p className="font-body text-on-surface-variant leading-relaxed mb-6 max-w-[60ch]">{body}</p>
|
||
<a
|
||
className="inline-flex justify-center items-center px-8 py-3 bg-ink text-white rounded-full font-bold shadow-lg hover:bg-ink-soft transition-colors duration-300"
|
||
href="#download"
|
||
>
|
||
{ctaLabel}
|
||
</a>
|
||
<p className="font-body text-sm text-on-surface-variant mt-5 max-w-[60ch]">{terms}</p>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoBestFit({ profile, copy }: SectionProps) {
|
||
return (
|
||
<section className="max-w-[1200px] mx-auto px-6 py-16 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||
<article className="bg-secondary-container/40 rounded-xl p-8 border border-secondary-container">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-3">{copy.bestFit.chooseTag}</span>
|
||
<h2 className="font-display font-semibold text-2xl text-primary mb-6">{copy.bestFit.chooseTitle}</h2>
|
||
<ul className="space-y-4">
|
||
{profile.greenLensIf.map((item) => (
|
||
<li key={item} className="flex items-start gap-3 text-sm text-on-surface-variant">
|
||
<SeoIcon name="check_circle" className="w-5 h-5 mt-0.5 shrink-0 text-primary" />
|
||
<span>{item}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</article>
|
||
<article className="bg-surface-container rounded-xl p-8 border border-surface-container-high">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-on-surface-variant block mb-3">{copy.bestFit.notTag}</span>
|
||
<h2 className="font-display font-semibold text-2xl text-primary mb-6">{copy.bestFit.notTitle}</h2>
|
||
<ul className="space-y-4">
|
||
{profile.notBestIf.map((item) => (
|
||
<li key={item} className="flex items-start gap-3 text-sm text-on-surface-variant">
|
||
<span className="w-5 h-5 mt-0.5 shrink-0 rounded-full border border-outline-variant flex items-center justify-center text-[10px] text-on-surface-variant">
|
||
–
|
||
</span>
|
||
<span>{item}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</article>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoFaq({ profile, copy }: SectionProps) {
|
||
return (
|
||
<section className="bg-surface-container py-16 md:py-24" id="faq">
|
||
<div className="max-w-[900px] mx-auto px-6">
|
||
<div className="text-center mb-16">
|
||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary">{copy.faq.title}</h2>
|
||
</div>
|
||
<div className="space-y-6">
|
||
{profile.faqs.map((item) => (
|
||
<div key={item.question} className="bg-surface rounded-lg p-6 shadow-sm">
|
||
<h3 className="font-display font-semibold text-base text-primary mb-2">{item.question}</h3>
|
||
<p className="font-body text-on-surface-variant text-sm">{item.answer}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function SeoRelated({ profile, copy }: SectionProps) {
|
||
if (profile.relatedLinks.length === 0) return null
|
||
|
||
return (
|
||
<section className="max-w-[1200px] mx-auto px-6 py-16">
|
||
<div className="mb-10">
|
||
<span className="text-xs font-semibold tracking-[0.1em] uppercase text-primary block mb-3">{copy.related.tag}</span>
|
||
<h2 className="font-display font-semibold text-[32px] leading-[1.3] text-primary">{copy.related.title}</h2>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
{profile.relatedLinks.map((link) => (
|
||
<Link key={link.href} href={link.href} className="bg-surface p-6 rounded-xl border border-surface-container-high seo-card block">
|
||
<h3 className="font-display font-semibold text-lg text-primary mb-2">{link.label}</h3>
|
||
<p className="text-sm text-on-surface-variant">{link.description}</p>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|