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
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:
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user