Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Failing after 4m3s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 5s
- Refined hero sections for About, Blog, Websites, and Case Studies for a bespoke industrial entry point. - Redesigned Marker component using layered SVG paths for an organic, hand-drawn highlighter effect. - Restored technical precision in ArchitectureVisualizer with refined line thickness. - Streamlined contact page by removing generic headers and prioritizing the configurator/gateway. - Updated technical references to reflect self-hosted Gitea infrastructure. - Cleaned up unused imports and addressed linting warnings across modified pages.
399 lines
13 KiB
TypeScript
399 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { useState, useMemo, useEffect, useRef } from "react";
|
|
import { motion } from "framer-motion";
|
|
import * as confetti from "canvas-confetti";
|
|
import {
|
|
Layers,
|
|
BrainCircuit,
|
|
Workflow,
|
|
Plug,
|
|
CheckCircle2,
|
|
AlertCircle,
|
|
} from "lucide-react";
|
|
|
|
import { FormState } from "./ContactForm/types";
|
|
import {
|
|
PRICING,
|
|
initialState,
|
|
FEATURE_OPTIONS,
|
|
FUNCTION_OPTIONS,
|
|
API_OPTIONS,
|
|
PAGE_SAMPLES,
|
|
ASSET_OPTIONS,
|
|
} from "./ContactForm/constants";
|
|
import { calculateTotals } from "@mintel/pdf";
|
|
import { sendContactInquiry } from "../actions/contact";
|
|
|
|
// Configurator Components
|
|
import { ConfiguratorLayout } from "./ContactForm/Configurator/ConfiguratorLayout";
|
|
import { NarrativeInput } from "./ContactForm/Configurator/NarrativeInput";
|
|
import { ModuleGrid } from "./ContactForm/Configurator/ModuleGrid";
|
|
import { ReferenceInput } from "./ContactForm/Configurator/ReferenceInput";
|
|
import { Launchpad } from "./ContactForm/Configurator/Launchpad";
|
|
import { Reveal } from "./Reveal";
|
|
|
|
interface ContactFormProps {
|
|
initialStepIndex?: number;
|
|
initialState?: Partial<FormState>;
|
|
}
|
|
|
|
const CONFIGURATOR_STEPS = [
|
|
{ id: "intro", title: "INITIALISIERUNG" },
|
|
{ id: "scope", title: "PROJEKT_UMFANG" },
|
|
{ id: "refs", title: "INSPIRATIONS_QUELLE" },
|
|
{ id: "assets", title: "BESTANDS_AUFNAHME" },
|
|
{ id: "features", title: "KERN_MODULE" },
|
|
{ id: "functions", title: "FUNKTIONALE_ERW" },
|
|
{ id: "api", title: "SYSTEM_INTEGRATION" },
|
|
{ id: "launch", title: "FINALE_SEQUENZ" },
|
|
];
|
|
|
|
import { ContactGateway } from "./ContactForm/ContactGateway";
|
|
import { DirectMessageFlow } from "./ContactForm/DirectMessageFlow";
|
|
|
|
type FlowState = "discovery" | "configurator" | "direct-message";
|
|
|
|
export function ContactForm({
|
|
initialStepIndex = 0,
|
|
initialState: injectedState,
|
|
}: ContactFormProps) {
|
|
const [flow, setFlow] = useState<FlowState>("discovery");
|
|
const [stepIndex, setStepIndex] = useState(initialStepIndex);
|
|
const [state, setState] = useState<FormState>({
|
|
...initialState,
|
|
...injectedState,
|
|
});
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Scroll to top on flow or step change
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
containerRef.current.scrollTo({ top: 0, behavior: "smooth" });
|
|
} else {
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
}, [flow, stepIndex]);
|
|
|
|
// Keyboard Navigation (only for configurator)
|
|
useEffect(() => {
|
|
if (flow !== "configurator") return;
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (
|
|
e.target instanceof HTMLInputElement ||
|
|
e.target instanceof HTMLTextAreaElement
|
|
)
|
|
return;
|
|
if (e.key === "Enter" && stepIndex < CONFIGURATOR_STEPS.length - 1) {
|
|
handleNext();
|
|
} else if (e.key === "Backspace" && stepIndex > 0) {
|
|
handlePrev();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [stepIndex, flow]);
|
|
|
|
const totals = useMemo(() => calculateTotals(state as any, PRICING), [state]);
|
|
|
|
const updateState = (updates: Partial<FormState>) => {
|
|
setState((prev) => ({ ...prev, ...updates }));
|
|
};
|
|
|
|
const toggleItem = (list: string[], id: string) => {
|
|
return list.includes(id) ? list.filter((i) => i !== id) : [...list, id];
|
|
};
|
|
|
|
const handleNext = () => setStepIndex((prev) => prev + 1);
|
|
const handlePrev = () => setStepIndex((prev) => Math.max(0, prev - 1));
|
|
|
|
const handleSubmit = async (e?: React.FormEvent) => {
|
|
e?.preventDefault();
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
const result = await sendContactInquiry({
|
|
name: state.name,
|
|
email: state.email,
|
|
companyName: state.companyName,
|
|
projectType: state.projectType,
|
|
message: state.message,
|
|
isFreeText: flow === "direct-message",
|
|
config: flow === "configurator" ? state : undefined,
|
|
});
|
|
|
|
setIsSubmitting(false);
|
|
|
|
if (result.success) {
|
|
setIsSubmitted(true);
|
|
// Celebration
|
|
const duration = 3 * 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 = 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);
|
|
} else {
|
|
setError(result.error || "Unerwarteter Fehler bei der Übertragung.");
|
|
}
|
|
};
|
|
|
|
// Success view (unified)
|
|
if (isSubmitted) {
|
|
return (
|
|
<div className="w-full min-h-[50vh] bg-white flex flex-col items-center justify-center p-8 text-center space-y-8 z-50 rounded-xl border border-slate-100 shadow-sm">
|
|
<Reveal width="100%" delay={0.1}>
|
|
<div className="flex flex-col items-center space-y-8">
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ type: "spring" }}
|
|
className="w-24 h-24 bg-green-500 rounded-full flex items-center justify-center text-slate-900 mb-4"
|
|
>
|
|
<CheckCircle2 size={48} />
|
|
</motion.div>
|
|
<h1 className="text-3xl md:text-5xl font-mono text-slate-900 tracking-widest">
|
|
SEQUENZ_INITIIERT
|
|
</h1>
|
|
<p className="text-slate-600 font-mono">
|
|
Das System wird Sie in Kürze unter {state.email} kontaktieren.
|
|
</p>
|
|
<button
|
|
onClick={() => {
|
|
setIsSubmitted(false);
|
|
setFlow("discovery");
|
|
setStepIndex(0);
|
|
setState(initialState);
|
|
}}
|
|
className="mt-8 text-green-600 underline font-mono hover:text-green-800"
|
|
>
|
|
[ NEUE_SEQUENZ_STARTEN ]
|
|
</button>
|
|
</div>
|
|
</Reveal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Gateway Flow
|
|
if (flow === "discovery") {
|
|
return (
|
|
<ContactGateway
|
|
name={state.name}
|
|
setName={(v) => updateState({ name: v })}
|
|
company={state.companyName}
|
|
setCompany={(v) => updateState({ companyName: v })}
|
|
projectType={state.projectType}
|
|
setProjectType={(v) => updateState({ projectType: v })}
|
|
onChooseConfigurator={() => setFlow("configurator")}
|
|
onChooseDirectMessage={() => setFlow("direct-message")}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Direct Message Flow
|
|
if (flow === "direct-message") {
|
|
return (
|
|
<DirectMessageFlow
|
|
name={state.name}
|
|
email={state.email}
|
|
setEmail={(v) => updateState({ email: v })}
|
|
company={state.companyName}
|
|
message={state.message}
|
|
setMessage={(v) => updateState({ message: v })}
|
|
onBack={() => setFlow("discovery")}
|
|
onSubmit={handleSubmit}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Configurator Flow
|
|
const renderConfiguratorContent = () => {
|
|
switch (CONFIGURATOR_STEPS[stepIndex].id) {
|
|
case "intro":
|
|
return (
|
|
<NarrativeInput
|
|
name={state.name}
|
|
setName={(v) => updateState({ name: v })}
|
|
company={state.companyName}
|
|
setCompany={(v) => updateState({ companyName: v })}
|
|
projectType={state.projectType}
|
|
setProjectType={(v) => updateState({ projectType: v })}
|
|
/>
|
|
);
|
|
case "scope":
|
|
return (
|
|
<ModuleGrid
|
|
title="SYSTEM MODULE"
|
|
options={PAGE_SAMPLES.map((p) => ({
|
|
id: p.id,
|
|
title: p.label,
|
|
description: p.desc,
|
|
icon: <Layers size={20} />,
|
|
}))}
|
|
selected={state.selectedPages}
|
|
onToggle={(id) =>
|
|
updateState({
|
|
selectedPages: toggleItem(state.selectedPages, id),
|
|
})
|
|
}
|
|
otherCount={state.otherPagesCount}
|
|
onOtherCountChange={(val) => updateState({ otherPagesCount: val })}
|
|
otherLabel="Weitere Seiten"
|
|
/>
|
|
);
|
|
case "refs":
|
|
return (
|
|
<ReferenceInput
|
|
references={state.references}
|
|
setReferences={(refs) => updateState({ references: refs })}
|
|
/>
|
|
);
|
|
case "assets":
|
|
return (
|
|
<ModuleGrid
|
|
title="VORHANDENE ASSETS"
|
|
options={ASSET_OPTIONS.map((a) => ({
|
|
id: a.id,
|
|
title: a.label,
|
|
description: a.desc,
|
|
icon: <Layers size={20} />,
|
|
}))}
|
|
selected={state.assets}
|
|
onToggle={(id) =>
|
|
updateState({ assets: toggleItem(state.assets, id) })
|
|
}
|
|
/>
|
|
);
|
|
case "features":
|
|
return (
|
|
<ModuleGrid
|
|
title="KERN-FEATURES"
|
|
options={FEATURE_OPTIONS.map((f) => ({
|
|
id: f.id,
|
|
title: f.label,
|
|
description: f.desc,
|
|
priceEstimate: "€€",
|
|
icon: <BrainCircuit size={20} />,
|
|
}))}
|
|
selected={state.features}
|
|
onToggle={(id) =>
|
|
updateState({ features: toggleItem(state.features, id) })
|
|
}
|
|
otherCount={state.otherFeaturesCount}
|
|
onOtherCountChange={(val) =>
|
|
updateState({ otherFeaturesCount: val })
|
|
}
|
|
otherLabel="Zusätzliche Features"
|
|
/>
|
|
);
|
|
case "functions":
|
|
return (
|
|
<ModuleGrid
|
|
title="ERWEITERTE FUNKTIONEN"
|
|
options={FUNCTION_OPTIONS.map((f) => ({
|
|
id: f.id,
|
|
title: f.label,
|
|
description: f.desc,
|
|
priceEstimate: "€€€",
|
|
icon: <Workflow size={20} />,
|
|
}))}
|
|
selected={state.functions}
|
|
onToggle={(id) =>
|
|
updateState({ functions: toggleItem(state.functions, id) })
|
|
}
|
|
/>
|
|
);
|
|
case "api":
|
|
return (
|
|
<ModuleGrid
|
|
title="SYSTEM-INTEGRATIONEN"
|
|
options={API_OPTIONS.map((a) => ({
|
|
id: a.id,
|
|
title: a.label,
|
|
description: a.desc,
|
|
priceEstimate: "€€€",
|
|
icon: <Plug size={20} />,
|
|
}))}
|
|
selected={state.apiSystems}
|
|
onToggle={(id) =>
|
|
updateState({ apiSystems: toggleItem(state.apiSystems, id) })
|
|
}
|
|
/>
|
|
);
|
|
case "launch":
|
|
return (
|
|
<div className="space-y-6">
|
|
{error && (
|
|
<Reveal width="100%" direction="none">
|
|
<div className="flex items-center gap-3 p-4 bg-red-50 text-red-700 rounded-xl border border-red-100 mb-6">
|
|
<AlertCircle size={20} />
|
|
<span className="text-sm font-bold uppercase tracking-wider">
|
|
{error}
|
|
</span>
|
|
</div>
|
|
</Reveal>
|
|
)}
|
|
<Launchpad
|
|
email={state.email}
|
|
setEmail={(v) => updateState({ email: v })}
|
|
timeline={state.deadline}
|
|
setTimeline={(v) => updateState({ deadline: v })}
|
|
message={state.message}
|
|
setMessage={(v) => updateState({ message: v })}
|
|
onSubmit={handleSubmit}
|
|
isValid={!!state.email && !!state.name}
|
|
/>
|
|
</div>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div ref={containerRef} className="w-full h-full overflow-y-auto">
|
|
<ConfiguratorLayout
|
|
stepIndex={stepIndex}
|
|
totalSteps={CONFIGURATOR_STEPS.length}
|
|
title={CONFIGURATOR_STEPS[stepIndex].title}
|
|
onNext={
|
|
stepIndex < CONFIGURATOR_STEPS.length - 1 ? handleNext : undefined
|
|
}
|
|
onPrev={stepIndex > 0 ? handlePrev : undefined}
|
|
isSubmitting={isSubmitting}
|
|
totalPrice={totals.totalPrice}
|
|
monthlyPrice={totals.monthlyPrice}
|
|
onRestart={() => {
|
|
setFlow("discovery");
|
|
setStepIndex(0);
|
|
setState(initialState);
|
|
}}
|
|
>
|
|
{renderConfiguratorContent()}
|
|
</ConfiguratorLayout>
|
|
</div>
|
|
);
|
|
}
|