chore: stabilize apps/web (lint, build, typecheck fixes)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-11 11:56:13 +01:00
parent 8ba81809b0
commit ecea90dc91
50 changed files with 5596 additions and 3456 deletions

View File

@@ -1,34 +1,43 @@
'use client';
"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, ArrowRight } from 'lucide-react';
import * as QRCode from 'qrcode';
import * as confetti from 'canvas-confetti';
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 '../logic/pricing/calculator';
import { PriceCalculation } from './ContactForm/components/PriceCalculation';
import { ShareModal } from './ShareModal';
import { FormState, Step } from "./ContactForm/types";
import { PRICING, initialState } from "./ContactForm/constants";
import { calculateTotals } from "../logic/pricing/calculator";
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';
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 { 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,
@@ -39,28 +48,42 @@ import {
ConceptCode,
ConceptAutomation,
ConceptPrice,
HeroArchitecture
} from './Landing/ConceptIllustrations';
HeroArchitecture,
} from "./Landing/ConceptIllustrations";
export interface ContactFormProps {
initialStepIndex?: number;
initialState?: FormState;
onStepChange?: (index: number) => void;
onStateChange?: (state: FormState) => void;
onStepChange?: (_index: number) => void;
onStateChange?: (_state: FormState) => void;
}
export function ContactForm({ initialStepIndex, initialState: propState, onStepChange, onStateChange }: ContactFormProps = {}) {
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 */ }
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 stepIndex =
initialStepIndex !== undefined ? initialStepIndex : internalStepIndex;
const state = propState !== undefined ? propState : internalState;
const setStepIndex = (val: number) => {
@@ -69,8 +92,8 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
};
const setState = (val: any) => {
if (typeof val === 'function') {
setInternalState(prev => {
if (typeof val === "function") {
setInternalState((prev) => {
const next = val(prev);
onStateChange?.(next);
return next;
@@ -82,13 +105,14 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
};
const [isSubmitted, setIsSubmitted] = useState(false);
const [qrCodeData, setQrCodeData] = useState<string>('');
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 isRemotion =
typeof window !== "undefined" && (window as any).isRemotion;
const [isClient, setIsClient] = useState(isRemotion);
useEffect(() => {
@@ -99,9 +123,9 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
setIsSticky(rect.top <= 80);
}
};
window.addEventListener('scroll', handleScroll);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [isRemotion]);
useEffect(() => {
@@ -111,10 +135,10 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
// URL Binding
useEffect(() => {
if (!searchParams) return;
const step = searchParams.get('step');
const step = searchParams.get("step");
if (step) setStepIndex(parseInt(step));
const config = searchParams.get('config');
const config = searchParams.get("config");
if (config) {
try {
const decoded = JSON.parse(decodeURIComponent(escape(atob(config))));
@@ -126,9 +150,9 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
}, [searchParams]);
const currentUrl = useMemo(() => {
if (!isClient) return '';
if (!isClient) return "";
const params = new URLSearchParams();
params.set('step', stepIndex.toString());
params.set("step", stepIndex.toString());
const configData = {
projectType: state.projectType,
@@ -166,11 +190,13 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
platformType: state.platformType,
dontKnows: state.dontKnows,
visualStaging: state.visualStaging,
complexInteractions: state.complexInteractions
complexInteractions: state.complexInteractions,
};
const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData))));
params.set('config', stateString);
const stateString = btoa(
unescape(encodeURIComponent(JSON.stringify(configData))),
);
params.set("config", stateString);
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
}, [state, stepIndex, isClient]);
@@ -179,7 +205,9 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
if (isRemotion) return;
if (currentUrl && router) {
router.replace(currentUrl, { scroll: false });
QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then(setQrCodeData);
QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then(
setQrCodeData,
);
}
}, [currentUrl, router, isRemotion]);
@@ -187,14 +215,15 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
return calculateTotals(state, PRICING);
}, [state]);
const { totalPrice, monthlyPrice, totalPagesCount } = totals;
// 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];
return list.includes(id) ? list.filter((i) => i !== id) : [...list, id];
};
const scrollToTop = () => {
@@ -208,7 +237,7 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
behavior: "smooth",
});
}
};
@@ -228,44 +257,136 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
};
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' },
{
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' },
{ 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');
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')!,
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]);
@@ -276,35 +397,72 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
const renderStepContent = () => {
const currentStep = activeSteps[stepIndex];
switch (currentStep.id) {
case 'type':
case "type":
return <TypeStep state={state} updateState={updateState} />;
case 'company':
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':
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':
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':
case "language":
return <LanguageStep state={state} updateState={updateState} />;
case 'timeline':
case "timeline":
return <TimelineStep state={state} updateState={updateState} />;
case 'contact':
case "contact":
return <ContactStep state={state} updateState={updateState} />;
case 'webapp':
case "webapp":
return <WebAppStep state={state} updateState={updateState} />;
default: return null;
default:
return null;
}
};
@@ -329,19 +487,30 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
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 randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min;
const interval: any = !isRemotion ? setInterval(function () {
const timeLeft = animationEnd - Date.now();
const interval: any = !isRemotion
? setInterval(function () {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
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;
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 {
@@ -355,21 +524,48 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
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 innerhalb von 24 Stunden 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
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 innerhalb
von 24 Stunden 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
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'}`}
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-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
@@ -377,7 +573,7 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
scale: isSticky ? 0.7 : 1,
width: isSticky ? 80 : 128,
height: isSticky ? 80 : 128,
borderRadius: isSticky ? '1.75rem' : '2.5rem'
borderRadius: isSticky ? "1.75rem" : "2.5rem",
}}
className="shrink-0 bg-slate-50 flex items-center justify-center relative shadow-inner z-10"
>
@@ -390,7 +586,11 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
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}
@@ -400,7 +600,11 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
</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 }}
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">
@@ -410,22 +614,28 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
</motion.div>
<motion.h3
animate={{
fontSize: isSticky ? '1.5rem' : '2.25rem',
lineHeight: isSticky ? '2rem' : '2.5rem',
color: isSticky ? '#0f172a' : '#0f172a'
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')}
{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'
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')}
{activeSteps[stepIndex].description.replace(
"{company}",
state.companyName || "Ihr Unternehmen",
)}
</motion.p>
</div>
</div>
@@ -433,15 +643,17 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
<div className="flex items-center gap-3 shrink-0">
{stepIndex > 0 ? (
<motion.button
whileHover={{ x: -3, backgroundColor: '#f8fafc' }}
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'}`}
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'} />}
) : (
<div className={isSticky ? "w-0" : "w-32"} />
)}
{stepIndex < activeSteps.length - 1 ? (
<motion.button
@@ -450,9 +662,13 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
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'}`}
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" />
Weiter{" "}
<ChevronRight
size={isSticky ? 16 : 20}
className="transition-transform group-hover:translate-x-1"
/>
</motion.button>
) : (
<motion.button
@@ -461,16 +677,22 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
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'}`}
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" />
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'}`}>
<div
className={`flex gap-1.5 transition-all duration-500 ${isSticky ? "h-1" : "h-2.5"}`}
>
{activeSteps.map((step, i) => (
<div
key={i}
@@ -484,19 +706,36 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
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`}
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 }}
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')}
{step.title.replace(
"{company}",
state.companyName || "Unternehmen",
)}
</motion.div>
)}
</AnimatePresence>
@@ -507,19 +746,25 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
{!isSticky && (
<div className="flex justify-between mt-4 px-1">
{chapters.map((chapter, idx) => {
const chapterSteps = activeSteps.filter(s => s.chapter === chapter.id);
{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;
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'
}`}
className={`text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500 ${
isActive ? "text-slate-900" : "text-slate-300"
}`}
>
{chapter.title}
</div>
@@ -531,8 +776,11 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
</div>
</div>
<form id="contact-form" onSubmit={handleSubmit} className="min-h-[450px] relative pt-12">
<form
id="contact-form"
onSubmit={handleSubmit}
className="min-h-[450px] relative pt-12"
>
<AnimatePresence mode="wait">
<motion.div
key={activeSteps[stepIndex].id}
@@ -559,12 +807,18 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
<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>
<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."}
{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>
@@ -574,7 +828,7 @@ export function ContactForm({ initialStepIndex, initialState: propState, onStepC
state={state}
totals={totals}
isClient={isClient}
qrCodeData={qrCodeData}
_qrCodeData={qrCodeData}
onShare={handleShare}
/>