"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(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(""); const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [hoveredStep, setHoveredStep] = useState(null); const [isSticky, setIsSticky] = useState(false); const formContainerRef = useRef(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) => { 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: , chapter: "strategy", }, { id: "company", title: "Unternehmen", description: "Wer sind Sie?", illustration: , chapter: "strategy", }, { id: "presence", title: "Präsenz", description: "Bestehende Kanäle von {company}.", illustration: , chapter: "strategy", }, { id: "features", title: "Die Systeme", description: "Welche inhaltlichen Bereiche planen wir für {company}?", illustration: , chapter: "scope", }, { id: "base", title: "Die Seiten", description: "Welche Seiten benötigen wir?", illustration: , chapter: "scope", }, { id: "design", title: "Design-Wünsche", description: "Wie soll die neue Präsenz von {company} wirken?", illustration: , chapter: "creative", }, { id: "assets", title: "Ihre Assets", description: "Was bringen Sie bereits mit?", illustration: , chapter: "creative", }, { id: "functions", title: "Die Logik", description: "Welche Funktionen werden benötigt?", illustration: , chapter: "tech", }, { id: "api", title: "Schnittstellen", description: "Datenaustausch mit Drittsystemen.", illustration: , chapter: "tech", }, { id: "content", title: "Die Pflege", description: "Wer kümmert sich um die Daten?", illustration: , chapter: "tech", }, { id: "language", title: "Sprachen", description: "Globale Reichweite planen.", illustration: , chapter: "tech", }, { id: "timeline", title: "Zeitplan", description: "Wann soll das Projekt live gehen?", illustration: , chapter: "final", }, { id: "contact", title: "Abschluss", description: "Erzählen Sie mir mehr über Ihr Vorhaben.", illustration: , chapter: "final", }, { id: "webapp", title: "Web App Details", description: "Spezifische Anforderungen für {company}.", illustration: , 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 ; case "company": return ; case "presence": return ( ); case "base": return ( ); case "features": return ( ); case "design": return ; case "assets": return ( ); case "functions": return ( ); case "api": return ( ); case "content": return ; case "language": return ; case "timeline": return ; case "contact": return ; case "webapp": return ; 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 (

Anfrage gesendet!

Vielen Dank, {state.name.split(" ")[0]}. Ich melde mich zeitnah bei Ihnen.

); } return (
{activeSteps[stepIndex].illustration}
{!isSticky && ( {stepIndex + 1} )}
Schritt {stepIndex + 1} / {activeSteps.length} {activeSteps[stepIndex].title.replace( "{company}", state.companyName || "Ihr Unternehmen", )} {activeSteps[stepIndex].description.replace( "{company}", state.companyName || "Ihr Unternehmen", )}
{stepIndex > 0 ? ( Zurück ) : (
)} {stepIndex < activeSteps.length - 1 ? ( Weiter{" "} ) : ( Senden{" "} )}
{activeSteps.map((step, i) => (
setHoveredStep(i)} onMouseLeave={() => setHoveredStep(null)} >
))}
{!isSticky && (
{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 (
{chapter.title}
); })}
)}
{renderStepContent()} {/* Contextual Help / Why this matters */}

Warum das wichtig ist

{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."}

setIsShareModalOpen(false)} url={currentUrl} qrCodeData={qrCodeData} />
); }