Files
mintel.me/apps/web/src/components/ContactForm.tsx
Marc Mintel cfda0daef9
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 4m5s
Build & Deploy / 🏗️ Build (push) Successful in 9m26s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
feat: implement industrial optimizations, hybrid dev workflow, and simplified reveal animations
2026-02-13 15:23:36 +01:00

844 lines
30 KiB
TypeScript

"use client";
import * as React from "react";
import { useState, useMemo, useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
ChevronRight,
ChevronLeft,
Send,
Check,
Sparkles,
Info,
} from "lucide-react";
import * as QRCode from "qrcode";
import * as confetti from "canvas-confetti";
import { FormState, Step } from "./ContactForm/types";
import { PRICING, initialState } from "./ContactForm/constants";
import { calculateTotals } from "@mintel/pdf";
import { PriceCalculation } from "./ContactForm/components/PriceCalculation";
import { ShareModal } from "./ShareModal";
// Steps
import { TypeStep } from "./ContactForm/steps/TypeStep";
import { CompanyStep } from "./ContactForm/steps/CompanyStep";
import { PresenceStep } from "./ContactForm/steps/PresenceStep";
import { BaseStep } from "./ContactForm/steps/BaseStep";
/* eslint-disable no-unused-vars */
import { FeaturesStep } from "./ContactForm/steps/FeaturesStep";
import { DesignStep } from "./ContactForm/steps/DesignStep";
import { AssetsStep } from "./ContactForm/steps/AssetsStep";
import { FunctionsStep } from "./ContactForm/steps/FunctionsStep";
import { ApiStep } from "./ContactForm/steps/ApiStep";
import { ContentStep } from "./ContactForm/steps/ContentStep";
import { LanguageStep } from "./ContactForm/steps/LanguageStep";
import { TimelineStep } from "./ContactForm/steps/TimelineStep";
import { ContactStep } from "./ContactForm/steps/ContactStep";
import { WebAppStep } from "./ContactForm/steps/WebAppStep";
import {
ConceptTarget,
ConceptWebsite,
ConceptPrototyping,
ConceptCommunication,
ConceptSystem,
ConceptCode,
ConceptAutomation,
ConceptPrice,
HeroArchitecture,
} from "./Landing/ConceptIllustrations";
export interface ContactFormProps {
initialStepIndex?: number;
initialState?: FormState;
onStepChange?: (_index: number) => void;
onStateChange?: (_state: FormState) => void;
}
export function ContactForm({
initialStepIndex,
initialState: propState,
onStepChange,
onStateChange,
}: ContactFormProps = {}) {
// Use a safe version of useRouter/useSearchParams that doesn't crash if not in a router context
let router: any = null;
let searchParams: any = null;
try {
router = useRouter();
} catch (_e) {
/* ignore */
}
try {
searchParams = useSearchParams();
} catch (_e) {
/* ignore */
}
const [internalStepIndex, setInternalStepIndex] = useState(0);
const [internalState, setInternalState] = useState<FormState>(initialState);
// Sync with props if provided
const stepIndex =
initialStepIndex !== undefined ? initialStepIndex : internalStepIndex;
const state = propState !== undefined ? propState : internalState;
const setStepIndex = (val: number) => {
setInternalStepIndex(val);
onStepChange?.(val);
};
const setState = (val: any) => {
if (typeof val === "function") {
setInternalState((prev) => {
const next = val(prev);
onStateChange?.(next);
return next;
});
} else {
setInternalState(val);
onStateChange?.(val);
}
};
const [isSubmitted, setIsSubmitted] = useState(false);
const [qrCodeData, setQrCodeData] = useState<string>("");
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [hoveredStep, setHoveredStep] = useState<number | null>(null);
const [isSticky, setIsSticky] = useState(false);
const formContainerRef = useRef<HTMLDivElement>(null);
const isRemotion =
typeof window !== "undefined" && (window as any).isRemotion;
const [isClient, setIsClient] = useState(isRemotion);
useEffect(() => {
if (isRemotion) return;
const handleScroll = () => {
if (formContainerRef.current) {
const rect = formContainerRef.current.getBoundingClientRect();
setIsSticky(rect.top <= 80);
}
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, [isRemotion]);
useEffect(() => {
if (!isRemotion) setIsClient(true);
}, [isRemotion]);
// URL Binding
useEffect(() => {
if (!searchParams) return;
const step = searchParams.get("step");
if (step) setStepIndex(parseInt(step));
const config = searchParams.get("config");
if (config) {
try {
const decoded = JSON.parse(decodeURIComponent(escape(atob(config))));
setInternalState((s: FormState) => ({ ...s, ...decoded }));
} catch (e) {
console.error("Failed to decode config", e);
}
}
}, [searchParams]);
const currentUrl = useMemo(() => {
if (!isClient) return "";
const params = new URLSearchParams();
params.set("step", stepIndex.toString());
const configData = {
projectType: state.projectType,
companyName: state.companyName,
employeeCount: state.employeeCount,
existingWebsite: state.existingWebsite,
socialMedia: state.socialMedia,
socialMediaUrls: state.socialMediaUrls,
existingDomain: state.existingDomain,
wishedDomain: state.wishedDomain,
websiteTopic: state.websiteTopic,
selectedPages: state.selectedPages,
otherPages: state.otherPages,
otherPagesCount: state.otherPagesCount,
features: state.features,
otherFeatures: state.otherFeatures,
otherFeaturesCount: state.otherFeaturesCount,
functions: state.functions,
otherFunctions: state.otherFunctions,
otherFunctionsCount: state.otherFunctionsCount,
apiSystems: state.apiSystems,
otherTech: state.otherTech,
otherTechCount: state.otherTechCount,
assets: state.assets,
otherAssets: state.otherAssets,
otherAssetsCount: state.otherAssetsCount,
cmsSetup: state.cmsSetup,
languagesList: state.languagesList,
deadline: state.deadline,
designVibe: state.designVibe,
colorScheme: state.colorScheme,
targetAudience: state.targetAudience,
userRoles: state.userRoles,
dataSensitivity: state.dataSensitivity,
platformType: state.platformType,
dontKnows: state.dontKnows,
visualStaging: state.visualStaging,
complexInteractions: state.complexInteractions,
};
const stateString = btoa(
unescape(encodeURIComponent(JSON.stringify(configData))),
);
params.set("config", stateString);
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
}, [state, stepIndex, isClient]);
useEffect(() => {
if (isRemotion) return;
if (currentUrl && router) {
router.replace(currentUrl, { scroll: false });
QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then(
setQrCodeData,
);
}
}, [currentUrl, router, isRemotion]);
const totals = useMemo(() => {
return calculateTotals(state, PRICING);
}, [state]);
// Destructuring moved to PriceCalculation if only used there
// const { totalPrice, monthlyPrice, totalPagesCount } = totals;
const updateState = (updates: Partial<FormState>) => {
setState((s: FormState) => ({ ...s, ...updates }));
};
const toggleItem = (list: string[], id: string) => {
return list.includes(id) ? list.filter((i) => i !== id) : [...list, id];
};
const scrollToTop = () => {
if (isRemotion) return;
if (formContainerRef.current) {
const offset = 120;
const bodyRect = document.body.getBoundingClientRect().top;
const elementRect = formContainerRef.current.getBoundingClientRect().top;
const elementPosition = elementRect - bodyRect;
const offsetPosition = elementPosition - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
});
}
};
const nextStep = () => {
if (stepIndex < activeSteps.length - 1) {
setStepIndex(stepIndex + 1);
if (!isRemotion) setTimeout(scrollToTop, 50);
}
};
const prevStep = () => {
if (stepIndex > 0) {
setStepIndex(stepIndex - 1);
if (!isRemotion) setTimeout(scrollToTop, 50);
}
};
const steps: Step[] = [
{
id: "type",
title: "Das Ziel",
description: "Was möchten Sie realisieren?",
illustration: <ConceptTarget className="w-full h-full" />,
chapter: "strategy",
},
{
id: "company",
title: "Unternehmen",
description: "Wer sind Sie?",
illustration: <ConceptCommunication className="w-full h-full" />,
chapter: "strategy",
},
{
id: "presence",
title: "Präsenz",
description: "Bestehende Kanäle von {company}.",
illustration: <ConceptSystem className="w-full h-full" />,
chapter: "strategy",
},
{
id: "features",
title: "Die Systeme",
description: "Welche inhaltlichen Bereiche planen wir für {company}?",
illustration: <ConceptPrototyping className="w-full h-full" />,
chapter: "scope",
},
{
id: "base",
title: "Die Seiten",
description: "Welche Seiten benötigen wir?",
illustration: <ConceptWebsite className="w-full h-full" />,
chapter: "scope",
},
{
id: "design",
title: "Design-Wünsche",
description: "Wie soll die neue Präsenz von {company} wirken?",
illustration: <ConceptCommunication className="w-full h-full" />,
chapter: "creative",
},
{
id: "assets",
title: "Ihre Assets",
description: "Was bringen Sie bereits mit?",
illustration: <ConceptSystem className="w-full h-full" />,
chapter: "creative",
},
{
id: "functions",
title: "Die Logik",
description: "Welche Funktionen werden benötigt?",
illustration: <ConceptCode className="w-full h-full" />,
chapter: "tech",
},
{
id: "api",
title: "Schnittstellen",
description: "Datenaustausch mit Drittsystemen.",
illustration: <ConceptAutomation className="w-full h-full" />,
chapter: "tech",
},
{
id: "content",
title: "Die Pflege",
description: "Wer kümmert sich um die Daten?",
illustration: <ConceptPrice className="w-full h-full" />,
chapter: "tech",
},
{
id: "language",
title: "Sprachen",
description: "Globale Reichweite planen.",
illustration: <ConceptCommunication className="w-full h-full" />,
chapter: "tech",
},
{
id: "timeline",
title: "Zeitplan",
description: "Wann soll das Projekt live gehen?",
illustration: <HeroArchitecture className="w-full h-full" />,
chapter: "final",
},
{
id: "contact",
title: "Abschluss",
description: "Erzählen Sie mir mehr über Ihr Vorhaben.",
illustration: <ConceptCommunication className="w-full h-full" />,
chapter: "final",
},
{
id: "webapp",
title: "Web App Details",
description: "Spezifische Anforderungen für {company}.",
illustration: <ConceptSystem className="w-full h-full" />,
chapter: "scope",
},
];
const chapters = [
{ id: "strategy", title: "Strategie" },
{ id: "scope", title: "Umfang" },
{ id: "creative", title: "Design" },
{ id: "tech", title: "Technik" },
{ id: "final", title: "Start" },
];
const activeSteps = useMemo(() => {
if (state.projectType === "website") {
return steps.filter((s) => s.id !== "webapp");
}
// Web App flow
return [
steps.find((s) => s.id === "type")!,
steps.find((s) => s.id === "company")!,
steps.find((s) => s.id === "presence")!,
steps.find((s) => s.id === "webapp")!,
{
...steps.find((s) => s.id === "functions")!,
title: "Funktionen",
description: "Kern-Features Ihrer Anwendung.",
},
{
...steps.find((s) => s.id === "api")!,
title: "Integrationen",
description: "Anbindung an bestehende Systeme.",
},
steps.find((s) => s.id === "timeline")!,
steps.find((s) => s.id === "contact")!,
];
}, [state.projectType, state.companyName]);
useEffect(() => {
if (stepIndex >= activeSteps.length) setStepIndex(activeSteps.length - 1);
}, [activeSteps, stepIndex]);
const renderStepContent = () => {
const currentStep = activeSteps[stepIndex];
switch (currentStep.id) {
case "type":
return <TypeStep state={state} updateState={updateState} />;
case "company":
return <CompanyStep state={state} updateState={updateState} />;
case "presence":
return (
<PresenceStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "base":
return (
<BaseStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "features":
return (
<FeaturesStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "design":
return <DesignStep state={state} updateState={updateState} />;
case "assets":
return (
<AssetsStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "functions":
return (
<FunctionsStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "api":
return (
<ApiStep
state={state}
updateState={updateState}
toggleItem={toggleItem}
/>
);
case "content":
return <ContentStep state={state} updateState={updateState} />;
case "language":
return <LanguageStep state={state} updateState={updateState} />;
case "timeline":
return <TimelineStep state={state} updateState={updateState} />;
case "contact":
return <ContactStep state={state} updateState={updateState} />;
case "webapp":
return <WebAppStep state={state} updateState={updateState} />;
default:
return null;
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (stepIndex === activeSteps.length - 1) {
// Handle submission
const mailBody = `
Name: ${state.name}
Email: ${state.email}
Rolle: ${state.role}
Projekt: ${state.projectType}
Konfiguration: ${currentUrl}
Nachricht:
${state.message}
`;
window.location.href = `mailto:marc@mintel.me?subject=Projektanfrage: ${state.name}&body=${encodeURIComponent(mailBody)}`;
// Celebration!
const duration = 5 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
const randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min;
const interval: any = !isRemotion
? setInterval(function () {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
(confetti as any)({
...defaults,
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
});
(confetti as any)({
...defaults,
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
});
}, 250)
: null;
setIsSubmitted(true);
} else {
nextStep();
}
};
const handleShare = () => {
setIsShareModalOpen(true);
};
if (isSubmitted) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="p-12 md:p-24 bg-slate-900 text-white rounded-[4rem] text-center space-y-12"
>
<div className="w-24 h-24 bg-white text-slate-900 rounded-full flex items-center justify-center mx-auto">
<Check size={48} strokeWidth={3} />
</div>
<div className="space-y-6">
<h2 className="text-5xl font-bold tracking-tight">
Anfrage gesendet!
</h2>
<p className="text-slate-400 text-2xl max-w-2xl mx-auto leading-relaxed">
Vielen Dank, {state.name.split(" ")[0]}. Ich melde mich zeitnah bei
Ihnen.
</p>
</div>
<button
type="button"
onClick={() => {
setIsSubmitted(false);
setStepIndex(0);
setState(initialState);
}}
className="text-slate-400 hover:text-white transition-colors underline underline-offset-8 text-lg focus:outline-none overflow-hidden relative rounded-full"
>
Neue Anfrage starten
</button>
</motion.div>
);
}
return (
<div
ref={formContainerRef}
className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start"
>
<div className="lg:col-span-8 space-y-12">
<div
className={`sticky top-[80px] z-40 transition-all duration-500 ${isSticky ? "bg-white/80 backdrop-blur-md py-4 -mx-12 px-12 -mt-px border-b border-slate-50" : "bg-transparent py-6 border-none"}`}
>
<div className={`flex flex-col ${isSticky ? "gap-4" : "gap-8"}`}>
<div className="flex flex-row items-center justify-between gap-8">
<div className="flex items-center gap-6">
<motion.div
animate={{
scale: isSticky ? 0.7 : 1,
width: isSticky ? 80 : 128,
height: isSticky ? 80 : 128,
borderRadius: isSticky ? "1.75rem" : "2.5rem",
}}
className="shrink-0 bg-slate-50 flex items-center justify-center relative shadow-inner z-10"
>
<div className="p-3 w-full h-full flex items-center justify-center overflow-hidden rounded-[inherit]">
{activeSteps[stepIndex].illustration}
</div>
<AnimatePresence>
{!isSticky && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
}}
className="absolute -bottom-2 -right-2 w-10 h-10 bg-slate-900 text-white rounded-full flex items-center justify-center font-bold text-sm border-4 border-white shadow-xl z-20"
>
{stepIndex + 1}
</motion.div>
)}
</AnimatePresence>
</motion.div>
<div className="space-y-1 min-w-0">
<motion.div
animate={{
opacity: isSticky ? 0 : 1,
height: isSticky ? 0 : "auto",
marginBottom: isSticky ? 0 : 4,
}}
className="flex items-center gap-3 overflow-hidden"
>
<span className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 flex items-center gap-2">
<Sparkles size={12} className="text-slate-300" />
Schritt {stepIndex + 1} / {activeSteps.length}
</span>
</motion.div>
<motion.h3
animate={{
fontSize: isSticky ? "1.5rem" : "2.25rem",
lineHeight: isSticky ? "2rem" : "2.5rem",
color: isSticky ? "#0f172a" : "#0f172a",
}}
className="font-black tracking-tight truncate"
>
{activeSteps[stepIndex].title.replace(
"{company}",
state.companyName || "Ihr Unternehmen",
)}
</motion.h3>
<motion.p
animate={{
fontSize: isSticky ? "0.875rem" : "1.125rem",
lineHeight: isSticky ? "1.25rem" : "1.75rem",
}}
className="text-slate-500 leading-relaxed max-w-2xl truncate overflow-hidden"
>
{activeSteps[stepIndex].description.replace(
"{company}",
state.companyName || "Ihr Unternehmen",
)}
</motion.p>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
{stepIndex > 0 ? (
<motion.button
whileHover={{ x: -3, backgroundColor: "#f8fafc" }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={prevStep}
className={`flex items-center justify-center gap-2 rounded-full border-2 border-slate-100 transition-all font-bold focus:outline-none whitespace-nowrap ${isSticky ? "px-5 py-2 text-sm" : "px-8 py-4 text-lg"}`}
>
<ChevronLeft size={isSticky ? 16 : 20} /> Zurück
</motion.button>
) : (
<div className={isSticky ? "w-0" : "w-32"} />
)}
{stepIndex < activeSteps.length - 1 ? (
<motion.button
id="focus-target-next"
whileHover={{ x: 3, scale: 1.02 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={nextStep}
className={`flex items-center justify-center gap-2 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold shadow-xl shadow-slate-200 focus:outline-none group whitespace-nowrap ${isSticky ? "px-6 py-2 text-sm" : "px-10 py-4 text-lg"}`}
>
Weiter{" "}
<ChevronRight
size={isSticky ? 16 : 20}
className="transition-transform group-hover:translate-x-1"
/>
</motion.button>
) : (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="submit"
form="contact-form"
disabled={!state.email || !state.name}
className={`flex items-center justify-center gap-2 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-slate-200 focus:outline-none group whitespace-nowrap ${isSticky ? "px-6 py-2 text-sm" : "px-10 py-4 text-lg"}`}
>
Senden{" "}
<Send
size={isSticky ? 16 : 20}
className="transition-transform group-hover:translate-x-1 group-hover:-translate-y-1"
/>
</motion.button>
)}
</div>
</div>
<div className="relative">
<div
className={`flex gap-1.5 transition-all duration-500 ${isSticky ? "h-1" : "h-2.5"}`}
>
{activeSteps.map((step, i) => (
<div
key={i}
className="flex-1 h-full flex items-center relative"
onMouseEnter={() => setHoveredStep(i)}
onMouseLeave={() => setHoveredStep(null)}
>
<button
type="button"
onClick={() => {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`w-full h-full rounded-full transition-all duration-700 ${
i === stepIndex
? "bg-slate-900 scale-y-150 shadow-lg shadow-slate-200"
: i < stepIndex
? "bg-slate-400"
: "bg-slate-100"
} cursor-pointer focus:outline-none p-0 border-none relative group`}
>
<AnimatePresence>
{hoveredStep === i && (
<motion.div
initial={{
opacity: 0,
y: 5,
x: "-50%",
scale: 0.9,
}}
animate={{
opacity: 1,
y: isSticky ? -35 : -40,
x: "-50%",
scale: 1,
}}
exit={{ opacity: 0, y: 5, x: "-50%", scale: 0.9 }}
className="absolute left-1/2 px-3 py-1.5 bg-white text-slate-900 text-[10px] font-black uppercase tracking-[0.15em] rounded-md whitespace-nowrap pointer-events-none z-50 shadow-[0_10px_30px_rgba(0,0,0,0.1)] border border-slate-100"
>
{step.title.replace(
"{company}",
state.companyName || "Unternehmen",
)}
</motion.div>
)}
</AnimatePresence>
</button>
</div>
))}
</div>
{!isSticky && (
<div className="flex justify-between mt-4 px-1">
{chapters.map((chapter, _idx) => {
const chapterSteps = activeSteps.filter(
(s) => s.chapter === chapter.id,
);
if (chapterSteps.length === 0) return null;
const firstStepIdx = activeSteps.indexOf(chapterSteps[0]);
const lastStepIdx = activeSteps.indexOf(
chapterSteps[chapterSteps.length - 1],
);
const isActive =
stepIndex >= firstStepIdx && stepIndex <= lastStepIdx;
return (
<div
key={chapter.id}
className={`text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500 ${
isActive ? "text-slate-900" : "text-slate-300"
}`}
>
{chapter.title}
</div>
);
})}
</div>
)}
</div>
</div>
</div>
<form
id="contact-form"
onSubmit={handleSubmit}
className="min-h-[450px] relative pt-12"
>
<AnimatePresence mode="wait">
<motion.div
key={activeSteps[stepIndex].id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
>
{renderStepContent()}
</motion.div>
</AnimatePresence>
{/* Contextual Help / Why this matters */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="mt-24 p-10 bg-slate-50 rounded-[3rem] border border-slate-100 flex gap-8 items-start relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-12 opacity-[0.03] pointer-events-none">
<Sparkles size={160} />
</div>
<div className="w-14 h-14 shrink-0 bg-white rounded-2xl flex items-center justify-center text-slate-900 shadow-sm relative z-10">
<Info size={28} />
</div>
<div className="space-y-2 relative z-10">
<h4 className="text-xl font-bold text-slate-900">
Warum das wichtig ist
</h4>
<p className="text-lg text-slate-500 leading-relaxed max-w-2xl">
{stepIndex === 0 &&
"Die Wahl zwischen Website und Web App bestimmt die technologische Basis. Web Apps nutzen oft Frameworks wie React oder Next.js für hochgradig interaktive Prozesse, während Websites auf Content-Präsentation optimiert sind."}
{stepIndex === 1 &&
"Ihr Unternehmen und dessen Größe helfen mir, die Skalierbarkeit und die notwendige Infrastruktur von Beginn an richtig zu dimensionieren."}
{stepIndex === 2 &&
"Bestehende Kanäle geben Aufschluss über Ihre aktuelle Markenidentität. Wir können diese entweder konsequent weiterführen oder einen bewussten Neuanfang wagen."}
{stepIndex > 2 &&
"Jede Auswahl beeinflusst die Komplexität und damit auch die Entwicklungszeit. Mein Ziel ist es, für Sie das beste Preis-Leistungs-Verhältnis zu finden."}
</p>
</div>
</motion.div>
</form>
</div>
<PriceCalculation
state={state}
totals={totals}
isClient={isClient}
_qrCodeData={qrCodeData}
onShare={handleShare}
/>
<ShareModal
isOpen={isShareModalOpen}
onClose={() => setIsShareModalOpen(false)}
url={currentUrl}
qrCodeData={qrCodeData}
/>
</div>
);
}