since3-portal/modules/templates/catalog/pflege/template.tsx
2026-10-10 02:38:50 +02:00

315 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { AnimatePresence, motion } from "motion/react"
import { useState } from "react"
import { ArrowRightIcon, BrainIcon, CheckIcon, ClipboardCheckIcon, HandHeartIcon, HeartHandshakeIcon, HouseIcon, MapPinIcon, PhoneCallIcon, PillIcon, UsersIcon, XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import type { TemplateProps } from "../../lib/types"
import { DemoForm } from "../shared/demo-form"
import { TField, TSelect } from "../shared/fields"
import { splitList } from "../shared/hooks"
import { TImage } from "../shared/media"
import { Reveal, RevealWords, ease } from "../shared/motion"
import { TemplateShell } from "../shared/shell"
import { pflege, type PflegeData } from "./definition"
import { fonts } from "./fonts"
const em = "text-t-accent"
const container = "mx-auto w-full max-w-[1240px] px-5 md:px-8"
const heading = "font-t-display font-bold tracking-[-0.015em]"
const icons = [HandHeartIcon, PillIcon, HouseIcon, BrainIcon, UsersIcon, ClipboardCheckIcon]
// Schriftgröße per zoom, damit alles mitwächst (auch Abstände und feste Leisten)
const SCALES = [
{ label: "A", className: "" },
{ label: "A+", className: "[zoom:1.15]" },
{ label: "A++", className: "[zoom:1.3]" },
]
export function PflegeTemplate({ content }: TemplateProps<PflegeData>) {
const { brand, hero, services, budget, steps, team, testimonials, jobs, contact } = content.data
const show = (key: string) => !content.hidden.includes(key)
const [scale, setScale] = useState(0)
const tel = `tel:${brand.phone.replace(/\s/g, "")}`
return (
<TemplateShell def={pflege} content={content} fonts={fonts[content.fonts] ?? fonts.lesbar} className={cn("text-[17px]", SCALES[scale].className)}>
<div className="bg-t-accent py-2.5 text-center font-bold text-t-accent-fg">
<a href={tel} className="inline-flex items-center gap-2">
<PhoneCallIcon className="size-5" /> {brand.emergency}: {brand.phone}
</a>
</div>
<header className="sticky top-0 z-50 border-b border-t-fg/10 bg-t-bg/95 backdrop-blur">
<nav className={cn(container, "flex h-20 items-center justify-between gap-4")}>
<a href="#top" className="flex items-center gap-3">
<span className="grid size-11 place-items-center rounded-2xl bg-t-accent text-t-accent-fg"><HeartHandshakeIcon className="size-6" /></span>
<span className="leading-tight">
<span className={cn(heading, "block text-xl")}>{brand.name}</span>
<span className="text-sm text-t-muted">{brand.suffix}</span>
</span>
</a>
<div className="flex items-center gap-3">
<div role="group" aria-label="Schriftgröße" className="flex rounded-full border-2 border-t-fg/15 p-1">
{SCALES.map((option, i) => (
<button
key={option.label}
type="button"
aria-pressed={scale === i}
aria-label={`Schriftgröße ${option.label}`}
onClick={() => setScale(i)}
className={cn("cursor-pointer rounded-full px-2.5 py-1 font-bold transition-colors", scale === i ? "bg-t-fg text-t-bg" : "hover:bg-t-fg/5")}
style={{ fontSize: `${0.8 + i * 0.15}rem` }}
>
{option.label}
</button>
))}
</div>
<a href="#kontakt" className="hidden rounded-full bg-t-fg px-5 py-3 font-bold text-t-bg md:block">{hero.cta}</a>
</div>
</nav>
</header>
<section id="top" className={cn(container, "grid items-center gap-12 py-14 md:py-20 lg:grid-cols-2")}>
<div>
<RevealWords as="h1" onMount delay={0.1} text={hero.title} emClassName={em} className={cn(heading, "text-[clamp(2.5rem,5.5vw,4.5rem)] leading-[1.08]")} />
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.7, ease, delay: 0.5 }}>
<p className="mt-6 text-xl leading-relaxed text-t-muted">{hero.text}</p>
<div className="mt-8 flex flex-wrap gap-3">
<a href="#kontakt" className="inline-flex items-center gap-2 rounded-full bg-t-accent px-7 py-4 text-lg font-bold text-t-accent-fg">
{hero.cta} <ArrowRightIcon className="size-5" />
</a>
<a href={tel} className="inline-flex items-center gap-2 rounded-full border-2 border-t-fg px-7 py-4 text-lg font-bold">
<PhoneCallIcon className="size-5" /> Anrufen
</a>
</div>
<AreaCheck areas={hero.areas} />
</motion.div>
</div>
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.9, ease }} className="relative">
<div className="relative aspect-[4/3.6] overflow-hidden rounded-[2rem]">
<TImage src={hero.image} eager sizes="(min-width: 1024px) 50vw, 100vw" />
</div>
{["Feste Bezugspersonen", "Alle Pflegekassen"].map((badge, i) => (
<span
key={badge}
className={cn("absolute flex items-center gap-2 rounded-2xl bg-t-bg px-4 py-3 font-bold shadow-xl", i === 0 ? "-bottom-5 left-4 md:-left-6" : "top-6 -right-2 md:-right-6")}
>
<CheckIcon className="size-5 text-t-accent" /> {badge}
</span>
))}
</motion.div>
</section>
<section id="leistungen" className="bg-t-surface py-20 md:py-28">
<div className={container}>
<RevealWords text={services.title} emClassName={em} className={cn(heading, "text-[clamp(2.25rem,4.5vw,3.5rem)]")} />
<div className="mt-12 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{services.items.map((item, i) => {
const Icon = icons[i % icons.length]
return (
<Reveal key={i} delay={(i % 3) * 0.08}>
<article className="h-full rounded-3xl bg-t-bg p-7 ring-2 ring-transparent transition-shadow hover:ring-t-accent">
<span className="grid size-14 place-items-center rounded-2xl bg-t-surface text-t-accent"><Icon className="size-7" /></span>
<h3 className={cn(heading, "mt-5 text-2xl")}>{item.title}</h3>
<p className="mt-2 text-lg leading-relaxed text-t-muted">{item.text}</p>
</article>
</Reveal>
)
})}
</div>
</div>
</section>
{show("budget") && <Budget data-order="budget" budget={budget} />}
{show("steps") && (
<section data-order="steps" className={cn(container, "py-20 md:py-28")}>
<RevealWords text={steps.title} emClassName={em} className={cn(heading, "text-center text-[clamp(2.25rem,4.5vw,3.5rem)]")} />
<ol className="mt-12 grid gap-6 md:grid-cols-4">
{steps.items.map((step, i) => (
<Reveal key={i} delay={i * 0.1}>
<li className="text-center">
<span className={cn(heading, "mx-auto grid size-16 place-items-center rounded-full bg-t-accent text-2xl text-t-accent-fg")}>{i + 1}</span>
<p className={cn(heading, "mt-4 text-xl")}>{step.title}</p>
<p className="mt-1 text-lg text-t-muted">{step.text}</p>
</li>
</Reveal>
))}
</ol>
</section>
)}
{show("team") && (
<section data-order="team" className="bg-t-surface py-20 md:py-28">
<div className={cn(container, "grid gap-12 lg:grid-cols-2 lg:items-center")}>
<div>
<RevealWords text={team.title} emClassName={em} className={cn(heading, "text-[clamp(2.25rem,4.5vw,3.5rem)] leading-[1.1]")} />
<Reveal delay={0.1}><p className="mt-6 text-xl leading-relaxed text-t-muted">{team.text}</p></Reveal>
<div className="mt-8 grid grid-cols-2 gap-4 sm:grid-cols-4">
{team.members.map((member, i) => (
<Reveal key={i} delay={i * 0.06} className="text-center">
<div className="relative mx-auto aspect-square w-full overflow-hidden rounded-full">
<TImage src={member.image} alt={member.name} sizes="140px" />
</div>
<p className="mt-2 font-bold leading-tight">{member.name}</p>
<p className="text-sm text-t-muted">{member.role}</p>
</Reveal>
))}
</div>
</div>
<Reveal delay={0.1}>
<div className="relative aspect-[4/3] overflow-hidden rounded-[2rem]">
<TImage src={team.image} sizes="(min-width: 1024px) 50vw, 100vw" />
</div>
</Reveal>
</div>
</section>
)}
{show("testimonials") && (
<section data-order="testimonials" className={cn(container, "grid gap-6 py-20 md:grid-cols-2 md:py-28")}>
{testimonials.items.map((item, i) => (
<Reveal key={i} delay={i * 0.1}>
<figure className="h-full rounded-3xl border-2 border-t-fg/10 p-8">
<blockquote className="text-2xl leading-snug">„{item.quote}“</blockquote>
<figcaption className="mt-6 font-bold text-t-accent">{item.author}</figcaption>
</figure>
</Reveal>
))}
</section>
)}
{show("jobs") && (
<section data-order="jobs" className={cn(container, "pb-20")}>
<div className="flex flex-col items-start gap-6 rounded-[2rem] bg-t-fg p-8 text-t-bg md:flex-row md:items-center md:justify-between md:p-12">
<div>
<RevealWords text={jobs.title} emClassName={em} className={cn(heading, "text-3xl md:text-4xl")} />
<p className="mt-3 max-w-xl text-lg opacity-80">{jobs.text}</p>
</div>
<a href="#kontakt" className="shrink-0 rounded-full bg-t-accent px-7 py-4 text-lg font-bold text-t-accent-fg">{jobs.cta}</a>
</div>
</section>
)}
<section id="kontakt" className="bg-t-surface py-20 md:py-28">
<div className={cn(container, "grid gap-12 lg:grid-cols-2")}>
<div>
<RevealWords text={contact.title} emClassName={em} className={cn(heading, "text-[clamp(2.5rem,5vw,4rem)]")} />
<p className="mt-5 text-xl text-t-muted">{contact.text}</p>
<a href={tel} className={cn(heading, "mt-8 flex items-center gap-3 text-3xl text-t-accent md:text-4xl")}>
<PhoneCallIcon className="size-8" /> {brand.phone}
</a>
<p className="mt-4 flex items-center gap-2 text-lg"><MapPinIcon className="size-5 text-t-accent" /> {brand.address}</p>
</div>
<DemoForm
className="grid gap-5 rounded-3xl bg-t-bg p-6 md:p-9"
success={<p className={cn(heading, "py-8 text-3xl")}>Vielen Dank! Wir rufen Sie <span className={em}>heute noch</span> zurück.</p>}
>
<p className={cn(heading, "text-2xl")}>Rückruf erbitten</p>
<TSelect label="Worum geht es?" name="topic" options={["Pflege für mich", "Pflege für Angehörige", "Beratung", "Bewerbung"]} />
<TField label="Ihr Name" name="name" required autoComplete="name" />
<TField label="Telefonnummer" name="phone" type="tel" required autoComplete="tel" />
<TSelect label="Wann sind Sie erreichbar?" name="time" options={["Vormittags", "Nachmittags", "Abends", "Egal"]} />
<button type="submit" className="cursor-pointer rounded-full bg-t-accent py-4 text-lg font-bold text-t-accent-fg">Rückruf anfordern</button>
</DemoForm>
</div>
</section>
<footer className={cn(container, "flex flex-col gap-3 py-10 text-t-muted md:flex-row md:justify-between")}>
<p>{brand.name} · {brand.suffix} · {brand.email}</p>
<div className="flex gap-6">
<a href="#" className="hover:text-t-fg">Impressum</a>
<a href="#" className="hover:text-t-fg">Datenschutz</a>
</div>
</footer>
</TemplateShell>
)
}
function AreaCheck({ areas }: { areas: string }) {
const [zip, setZip] = useState("")
const [result, setResult] = useState<boolean | null>(null)
const prefixes = splitList(areas)
return (
<form
className="mt-10 rounded-3xl bg-t-surface p-5"
onSubmit={(event) => {
event.preventDefault()
setResult(prefixes.some((prefix) => zip.startsWith(prefix)))
}}
>
<label htmlFor="pflege-zip" className="font-bold">Sind wir in Ihrer Nähe? Postleitzahl prüfen:</label>
<div className="mt-3 flex gap-2">
<input
id="pflege-zip"
value={zip}
onChange={(e) => {
setZip(e.target.value.replace(/\D/g, "").slice(0, 5))
setResult(null)
}}
inputMode="numeric"
placeholder="z. B. 44135"
className="min-w-0 flex-1 rounded-full border-2 border-t-fg/15 bg-t-bg px-5 py-3 text-lg outline-none focus:border-t-accent"
/>
<button type="submit" disabled={zip.length < 5} className="cursor-pointer rounded-full bg-t-fg px-6 font-bold text-t-bg disabled:opacity-40">Prüfen</button>
</div>
<AnimatePresence mode="wait">
{result !== null && (
<motion.p
key={String(result)}
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className={cn("mt-3 flex items-center gap-2 font-bold", result ? "text-emerald-700" : "text-t-muted")}
>
{result ? <CheckIcon className="size-5" /> : <XIcon className="size-5" />}
{result ? "Gute Nachricht: Wir sind in Ihrer Gegend unterwegs!" : "Leider nicht in unserem Gebiet – rufen Sie uns trotzdem an, wir helfen gern weiter."}
</motion.p>
)}
</AnimatePresence>
</form>
)
}
function Budget({ budget }: { budget: PflegeData["budget"] }) {
const [index, setIndex] = useState(1)
const current = budget.items[Math.min(index, budget.items.length - 1)]
return (
<section className={cn(container, "py-20 md:py-28")}>
<div className="grid gap-10 rounded-[2rem] border-2 border-t-fg/10 p-6 md:p-12 lg:grid-cols-2 lg:items-center">
<div>
<RevealWords text={budget.title} emClassName={em} className={cn(heading, "text-[clamp(2rem,4vw,3.25rem)] leading-[1.1]")} />
<p className="mt-5 text-lg leading-relaxed text-t-muted">{budget.text}</p>
</div>
<div>
<div role="tablist" aria-label="Pflegegrad" className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{budget.items.map((item, i) => (
<button
key={item.grade}
type="button"
role="tab"
aria-selected={index === i}
onClick={() => setIndex(i)}
className={cn("cursor-pointer rounded-2xl px-3 py-3 font-bold transition-colors", index === i ? "bg-t-accent text-t-accent-fg" : "bg-t-surface hover:bg-t-fg/10")}
>
{item.grade}
</button>
))}
</div>
{current && (
<div className="mt-6 rounded-3xl bg-t-surface p-7 text-center">
<p className="text-lg text-t-muted">bis zu</p>
<motion.p key={current.amount} initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className={cn(heading, "text-6xl text-t-accent")}>
{current.amount} €
</motion.p>
<p className="mt-1 text-lg">pro Monat für Pflege durch uns</p>
</div>
)}
<p className="mt-3 text-sm text-t-muted">{budget.note}</p>
</div>
</div>
</section>
)
}