From 8124e9cc959bc007ac0423c26dc0c772162e41e4 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 30 Jan 2026 18:11:52 +0100 Subject: [PATCH] form --- app/contact/page.tsx | 6 +- .../ContactForm/components/Input.tsx | 36 ++++++ .../components/PriceCalculation.tsx | 63 +++++++---- src/components/ContactForm/steps/ApiStep.tsx | 55 +-------- .../ContactForm/steps/AssetsStep.tsx | 8 +- src/components/ContactForm/steps/BaseStep.tsx | 15 ++- .../ContactForm/steps/CompanyStep.tsx | 25 ++--- .../ContactForm/steps/ContactStep.tsx | 100 ++++++----------- .../ContactForm/steps/ContentStep.tsx | 30 ++--- .../ContactForm/steps/DesignStep.tsx | 95 +++++++--------- .../ContactForm/steps/FeaturesStep.tsx | 8 +- .../ContactForm/steps/FunctionsStep.tsx | 10 +- .../ContactForm/steps/LanguageStep.tsx | 105 ++++++++++++++---- .../ContactForm/steps/PresenceStep.tsx | 69 +++++------- src/components/ContactForm/steps/TypeStep.tsx | 2 +- .../ContactForm/steps/WebAppStep.tsx | 12 +- 16 files changed, 326 insertions(+), 313 deletions(-) create mode 100644 src/components/ContactForm/components/Input.tsx diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 64a5cf5..9a5c554 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -6,7 +6,7 @@ import { ContactForm } from '../../src/components/ContactForm'; export default function ContactPage() { return ( -
+
Projekt
konfigurieren.} description="Nutzen Sie den Konfigurator für eine erste Einschätzung oder schreiben Sie mir direkt eine Email." @@ -15,9 +15,7 @@ export default function ContactPage() { />
- - - +
diff --git a/src/components/ContactForm/components/Input.tsx b/src/components/ContactForm/components/Input.tsx new file mode 100644 index 0000000..0c3af34 --- /dev/null +++ b/src/components/ContactForm/components/Input.tsx @@ -0,0 +1,36 @@ +'use client'; + +import * as React from 'react'; +import { LucideIcon } from 'lucide-react'; + +interface InputProps extends React.InputHTMLAttributes { + label?: string; + icon?: LucideIcon; + isTextArea?: boolean; + rows?: number; +} + +export function Input({ label, icon: Icon, isTextArea, className = '', ...props }: InputProps) { + const InputComponent = isTextArea ? 'textarea' : 'input'; + + return ( +
+ {label && ( + + )} +
+ {Icon && ( +
+ +
+ )} + +
+
+ ); +} diff --git a/src/components/ContactForm/components/PriceCalculation.tsx b/src/components/ContactForm/components/PriceCalculation.tsx index 6737137..630f555 100644 --- a/src/components/ContactForm/components/PriceCalculation.tsx +++ b/src/components/ContactForm/components/PriceCalculation.tsx @@ -39,39 +39,56 @@ export function PriceCalculation({ const totalFunctions = state.functions.length + state.otherFunctions.length + (state.otherFunctionsCount || 0); const totalApis = state.apiSystems.length + state.otherTech.length + (state.otherTechCount || 0); + const [pdfLoading, setPdfLoading] = React.useState(false); + const languagesCount = state.languagesList.length || 1; + return ( -
-
-

Kalkulation

Unverbindliche Schätzung.

+
+
{state.projectType === 'website' ? ( <> -
Basis Website{PRICING.BASE_WEBSITE.toLocaleString()} €
-
+
{totalPages > 0 && (
{totalPages}x Seite{(totalPages * PRICING.PAGE).toLocaleString()} €
)} {totalFeatures > 0 && (
{totalFeatures}x System-Modul{(totalFeatures * PRICING.FEATURE).toLocaleString()} €
)} {totalFunctions > 0 && (
{totalFunctions}x Logik-Funktion{(totalFunctions * PRICING.FUNCTION).toLocaleString()} €
)} - {state.visualStaging > 0 && (
{state.visualStaging}x Visuelle Inszenierung{(state.visualStaging * PRICING.VISUAL_STAGING).toLocaleString()} €
)} - {state.complexInteractions > 0 && (
{state.complexInteractions}x Komplexe Interaktion{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} €
)} {totalApis > 0 && (
{totalApis}x API Sync{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} €
)} {state.cmsSetup && (
CMS Setup & Anbindung{(PRICING.CMS_SETUP + totalFeatures * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} €
)} {state.newDatasets > 0 && (
{state.newDatasets}x Inhalte einpflegen{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} €
)} - {state.languagesCount > 1 && (
Mehrsprachigkeit ({state.languagesCount}x)+{(totalPrice - (totalPrice / (1 + (state.languagesCount - 1) * 0.2))).toLocaleString()} €
)} + {languagesCount > 1 && (
Mehrsprachigkeit ({languagesCount}x)+{(totalPrice - (totalPrice / (1 + (languagesCount - 1) * 0.2))).toLocaleString()} €
)} +
+
+
+ Gesamt +
+
+ € +
+
+
+
+
+
+ Betrieb & Hosting + {monthlyPrice.toLocaleString()} € / Monat +
-
Gesamt

Einmalig / Netto

-
Betrieb & Hosting{monthlyPrice.toLocaleString()} € / Monat

Inklusive Hosting, Sicherheitsupdates, Backups und Analytics-Reports.

-
+
{isClient && ( } fileName={`kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`} - className="w-full flex items-center justify-center gap-3 px-8 py-4 rounded-full border border-slate-200 text-slate-900 font-bold text-sm uppercase tracking-widest hover:bg-white hover:border-slate-900 transition-all focus:outline-none overflow-hidden relative rounded-full" + className="w-full flex items-center justify-center gap-3 px-8 py-4 rounded-full border border-slate-200 text-slate-900 font-bold text-sm uppercase tracking-widest hover:bg-white hover:border-slate-900 transition-all focus:outline-none overflow-hidden relative" + onClick={() => { + setPdfLoading(true); + setTimeout(() => setPdfLoading(false), 2000); + }} > {({ loading }) => (
- {loading ? 'PDF wird erstellt...' : 'Als PDF speichern'} + {loading || pdfLoading ? 'PDF wird erstellt...' : 'Als PDF speichern'}
)}
@@ -81,33 +98,33 @@ export function PriceCalculation({ )}
) : (
-
-
- +
+
+
-
-

Web Apps und Individual-Software werden nach tatsächlichem Aufwand abgerechnet.

-

{PRICING.APP_HOURLY} € / Std.

+
+

Web Apps werden nach Aufwand abgerechnet.

+

{PRICING.APP_HOURLY} € / Std.

{onShare && ( )}
diff --git a/src/components/ContactForm/steps/ApiStep.tsx b/src/components/ContactForm/steps/ApiStep.tsx index 3f86f3d..396699d 100644 --- a/src/components/ContactForm/steps/ApiStep.tsx +++ b/src/components/ContactForm/steps/ApiStep.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import { FormState } from '../types'; import { Checkbox } from '../components/Checkbox'; import { RepeatableList } from '../components/RepeatableList'; -import { Minus, Plus, Share2, ListPlus } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { Share2, ListPlus } from 'lucide-react'; +import { motion } from 'framer-motion'; import { Reveal } from '../../Reveal'; interface ApiStepProps { @@ -32,7 +32,7 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
-
+

@@ -44,7 +44,7 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) { whileTap={{ scale: 0.95 }} type="button" onClick={() => toggleDontKnow('api')} - className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${ + className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${ state.dontKnows?.includes('api') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200' }`} > @@ -86,7 +86,7 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
-
+

Weitere Systeme oder eigene APIs?

@@ -98,51 +98,6 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) { placeholder="z.B. Microsoft Graph, Google Search Console, Custom REST API..." />
- - -
-
-

Anzahl weiterer Schnittstellen

-

Falls Sie weitere Integrationen planen, diese aber noch nicht benennen können.

-
-
- updateState({ otherTechCount: Math.max(0, state.otherTechCount - 1) })} - className="w-14 h-14 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none" - > - - - - - {state.otherTechCount} - - - updateState({ otherTechCount: state.otherTechCount + 1 })} - className="w-14 h-14 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none" - > - - -
-
-
diff --git a/src/components/ContactForm/steps/AssetsStep.tsx b/src/components/ContactForm/steps/AssetsStep.tsx index 4b5feb0..1395d9b 100644 --- a/src/components/ContactForm/steps/AssetsStep.tsx +++ b/src/components/ContactForm/steps/AssetsStep.tsx @@ -31,7 +31,7 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
-
+

Vorhandene Assets

@@ -41,7 +41,7 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) whileTap={{ scale: 0.95 }} type="button" onClick={() => toggleDontKnow('assets')} - className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${ + className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${ state.dontKnows?.includes('assets') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200' }`} > @@ -71,7 +71,7 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
-
+

Weitere vorhandene Unterlagen?

@@ -88,7 +88,7 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.4 }} - className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 shadow-sm hover:shadow-xl transition-all duration-500" + className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500" >
diff --git a/src/components/ContactForm/steps/BaseStep.tsx b/src/components/ContactForm/steps/BaseStep.tsx index 9ee6c52..f5ef14f 100644 --- a/src/components/ContactForm/steps/BaseStep.tsx +++ b/src/components/ContactForm/steps/BaseStep.tsx @@ -6,6 +6,7 @@ import { Checkbox } from '../components/Checkbox'; import { RepeatableList } from '../components/RepeatableList'; import { motion, AnimatePresence } from 'framer-motion'; import { Minus, Plus, FileText, ListPlus } from 'lucide-react'; +import { Input } from '../components/Input'; interface BaseStepProps { state: FormState; @@ -30,20 +31,18 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) { animate={{ opacity: 1, y: 0 }} className="space-y-8" > -

Thema der Website

- updateState({ websiteTopic: e.target.value })} - className="w-full p-10 bg-white border border-slate-100 rounded-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl" />
-
+

Die Seiten

@@ -53,7 +52,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) { whileTap={{ scale: 0.95 }} type="button" onClick={() => toggleDontKnow('pages')} - className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${ + className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${ state.dontKnows?.includes('pages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200' }`} > @@ -88,7 +87,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
-
+

Weitere individuelle Seiten?

@@ -105,7 +104,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.4 }} - className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 shadow-sm hover:shadow-xl transition-all duration-500" + className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500" >
diff --git a/src/components/ContactForm/steps/CompanyStep.tsx b/src/components/ContactForm/steps/CompanyStep.tsx index 5594130..406f962 100644 --- a/src/components/ContactForm/steps/CompanyStep.tsx +++ b/src/components/ContactForm/steps/CompanyStep.tsx @@ -6,6 +6,7 @@ import { EMPLOYEE_OPTIONS } from '../constants'; import { motion } from 'framer-motion'; import { Building2, Users } from 'lucide-react'; import { Reveal } from '../../Reveal'; +import { Input } from '../components/Input'; interface CompanyStepProps { state: FormState; @@ -18,28 +19,24 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
-
+

Unternehmen

-
- - updateState({ companyName: e.target.value })} - className="w-full p-8 bg-white border border-slate-100 rounded-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl" - /> -
+ updateState({ companyName: e.target.value })} + />
-
+

Mitarbeiteranzahl

@@ -48,12 +45,12 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) { {EMPLOYEE_OPTIONS.map((option, index) => ( updateState({ employeeCount: option.id })} className={`p-6 rounded-[2rem] border-2 transition-all duration-300 font-bold text-lg ${ - state.employeeCount === option.id ? 'border-slate-900 bg-slate-900 text-white shadow-lg' : 'border-slate-100 bg-white hover:border-slate-300 text-slate-600' + state.employeeCount === option.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-300 text-slate-600' }`} > {option.label} diff --git a/src/components/ContactForm/steps/ContactStep.tsx b/src/components/ContactForm/steps/ContactStep.tsx index 1f8ed19..7d232ad 100644 --- a/src/components/ContactForm/steps/ContactStep.tsx +++ b/src/components/ContactForm/steps/ContactStep.tsx @@ -5,6 +5,7 @@ import { FormState } from '../types'; import { FileText, Upload, X, User, Mail, Briefcase, MessageSquare } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { Reveal } from '../../Reveal'; +import { Input } from '../components/Input'; interface ContactStepProps { state: FormState; @@ -16,78 +17,49 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
-
- -
-
- -
- updateState({ name: e.target.value })} - className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl" - /> -
-
+ updateState({ name: e.target.value })} + />
-
- -
-
- -
- updateState({ email: e.target.value })} - className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl" - /> -
-
+ updateState({ email: e.target.value })} + />
-
- -
-
- -
- updateState({ role: e.target.value })} - className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl" - /> -
-
+ updateState({ role: e.target.value })} + />
-
- -
-
- -
-