"use client"
import { AnimatePresence, motion, useInView, useMotionValue, useSpring, useTransform } from "motion/react"
import { useEffect, useRef, useState, type PointerEvent } from "react"
import {
ArrowRightIcon,
BotIcon,
ChartNoAxesColumnIcon,
CheckIcon,
InboxIcon,
LockIcon,
MailIcon,
MessageCircleIcon,
PhoneIcon,
PlugIcon,
SparklesIcon,
WorkflowIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import type { TemplateProps } from "../../lib/types"
import { splitList } from "../shared/hooks"
import { TImage } from "../shared/media"
import { Accordion, Marquee, Reveal, RevealWords, ease, useScrollNav } from "../shared/motion"
import { TemplateShell } from "../shared/shell"
import { saas, type SaasData } from "./definition"
import { fonts } from "./fonts"
const em = "bg-gradient-to-r from-t-accent to-t-fg bg-clip-text text-transparent"
const container = "mx-auto w-full max-w-[1200px] px-5 md:px-8"
const heading = "font-t-display font-semibold tracking-[-0.035em]"
const mono = { fontFamily: "var(--font-saas-mono), ui-monospace, monospace" }
const card = "rounded-3xl border border-t-fg/10 bg-t-surface/60"
const featureIcons = [InboxIcon, BotIcon, WorkflowIcon, ChartNoAxesColumnIcon, PlugIcon, LockIcon]
function useTypewriter(text: string, active: boolean, speed = 28) {
const [count, setCount] = useState(0)
useEffect(() => {
if (!active) return
const timer = setInterval(() => setCount((c) => (c >= text.length ? c : c + 2)), speed)
return () => clearInterval(timer)
}, [active, text.length, speed])
return text.slice(0, count)
}
function Eyebrow({ children }: { children: string }) {
return
{children}
}
export function SaasTemplate({ content }: TemplateProps) {
const { brand, hero, logos, features, steps, pricing, testimonials, faq, cta } = content.data
const show = (key: string) => !content.hidden.includes(key)
const links = [
{ href: "#funktionen", label: "Funktionen", visible: show("features") },
{ href: "#preise", label: "Preise", visible: show("pricing") },
{ href: "#faq", label: "FAQ", visible: show("faq") },
].filter((link) => link.visible)
return (
{hero.badge}
{hero.text}
{hero.note}
{show("logos") && (
{logos.title}
{logos.items.map((logo, i) => (
{logo.name}
))}
)}
{show("features") && (
{features.eyebrow}
{features.items.map((item, i) => {
const Icon = featureIcons[i % featureIcons.length]
const wide = i % 4 === 0 || i % 4 === 3
return (
{item.title}
{item.text}
)
})}
)}
{show("steps") && }
{show("pricing") && }
{show("testimonials") && (
{testimonials.items.map((item, i) => (
„{item.quote}“
{item.author}
{item.role}
))}
)}
{show("faq") && (
)}
)
}
function Logo({ name }: { name: string }) {
return (
{name}
)
}
function Nav({ name, cta, links }: { name: string; cta: string; links: { href: string; label: string }[] }) {
const { scrolled, hidden } = useScrollNav()
return (
)
}
const tickets = [
{ name: "Anna Weber", text: "Wo bleibt meine Bestellung #4821?", time: "2 Min", icon: MailIcon },
{ name: "Ben Schulz", text: "Kann ich die Größe noch ändern?", time: "8 Min", icon: MessageCircleIcon },
{ name: "Clara Yilmaz", text: "Rückruf wegen Rechnung", time: "15 Min", icon: PhoneIcon },
{ name: "David Ott", text: "Danke, alles angekommen!", time: "1 Std", icon: MailIcon },
]
function ProductMockup({ name }: { name: string }) {
const ref = useRef(null)
const inView = useInView(ref, { once: true, margin: "0px 0px -20% 0px" })
const x = useMotionValue(0)
const y = useMotionValue(0)
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [7, -7]), { stiffness: 120, damping: 20 })
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-9, 9]), { stiffness: 120, damping: 20 })
const reply = useTypewriter(
"Hallo Anna, deine Bestellung #4821 ist heute Morgen rausgegangen und kommt morgen bis 12 Uhr an. Hier ist dein Tracking-Link – melde dich gern, falls noch etwas offen ist!",
inView
)
function onMove(event: PointerEvent) {
const rect = event.currentTarget.getBoundingClientRect()
x.set((event.clientX - rect.left) / rect.width - 0.5)
y.set((event.clientY - rect.top) / rect.height - 0.5)
}
return (
{
x.set(0)
y.set(0)
}}
style={{ perspective: 1600 }}
className="relative mx-auto mt-16 max-w-5xl text-left md:mt-20"
>
app.{name.toLowerCase().replace(/[^a-z0-9]/g, "")}.de/inbox
{tickets.map((ticket, i) => (
{ticket.name.charAt(0)}
{ticket.name}
{ticket.time}
{ticket.text}
))}
Hallo, wo bleibt meine Bestellung #4821? Ich brauche sie bis Freitag.
KI-Vorschlag
{reply}
Senden
Bearbeiten
2 Min Ø Antwortzeit
63 % automatisch gelöst
)
}
function FeatureVisual({ index }: { index: number }) {
const ref = useRef(null)
const inView = useInView(ref, { once: true })
const typed = useTypewriter("Gern! Ich habe dir einen neuen Termin für Donnerstag reserviert …", inView && index % 6 === 1, 45)
const variant = index % 6
return (
{variant === 0 && (
{[MailIcon, MessageCircleIcon, PhoneIcon].map((Icon, i) => (
))}
)}
{variant === 1 &&
{typed}
}
{variant === 2 && (
{["Rechnungen → Buchhaltung", "VIP-Kunden priorisieren", "Nach 24 h nachfassen"].map((rule, i) => (
{rule}
))}
)}
{variant === 3 && (
{[38, 52, 45, 70, 62, 84, 78, 96].map((height, i) => (
))}
)}
{variant === 4 && (
{[MailIcon, MessageCircleIcon, PhoneIcon, PlugIcon, InboxIcon, WorkflowIcon, BotIcon, ChartNoAxesColumnIcon].map((Icon, i) => (
))}
)}
{variant === 5 && (
)}
)
}
function Steps({ steps, name }: { steps: SaasData["steps"]; name: string }) {
const ref = useRef(null)
const inView = useInView(ref, { once: true, margin: "0px 0px -20% 0px" })
const cli = name.toLowerCase().replace(/[^a-z0-9]/g, "") || "app"
const lines = [
`$ ${cli} connect --mail support@shop.de`,
"✓ Postfach verbunden · 2.341 Nachrichten",
`$ ${cli} learn --help-center`,
"✓ 128 Artikel gelernt",
`$ ${cli} autopilot on`,
"✓ 63 % der Anfragen werden automatisch beantwortet",
]
return (
{steps.eyebrow}
{steps.items.map((step, i) => (
{i + 1}
))}
{lines.map((line, i) => (
{line}
))}
)
}
function Pricing({ pricing }: { pricing: SaasData["pricing"] }) {
const [yearly, setYearly] = useState(true)
const discount = Math.min(90, Math.max(0, Number(pricing.discount) || 0))
return (
{pricing.eyebrow}
{[false, true].map((option) => (
setYearly(option)}
className={cn("relative cursor-pointer rounded-full px-5 py-2", yearly === option ? "text-t-bg" : "text-t-muted")}
>
{yearly === option && }
{option ? `Jährlich · −${discount} %` : "Monatlich"}
))}
{pricing.plans.map((plan, i) => {
const featured = Boolean(plan.highlight.trim())
const base = Number(plan.price.replace(",", ".")) || 0
const price = Math.round(yearly ? base * (1 - discount / 100) : base)
return (
{featured && Am beliebtesten }
{plan.name}
{plan.text}
{price} €
/ Monat
{splitList(plan.features, "\n").map((feature) => (
{feature}
))}
{plan.name} wählen
)
})}
)
}