Files
mintel.me/apps/web/src/components/ContactForm.tsx
Marc Mintel 9cfe7ee9e5
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
feat: redesign page heroes, implement organic markers, and streamline contact flow
- 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.
2026-02-16 19:34:08 +01:00

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>
);
}