diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 126db47..64a5cf5 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -6,21 +6,21 @@ 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." backLink={{ href: '/', label: 'Zurück' }} backgroundSymbol="?" /> -
+
-
+
diff --git a/app/globals.css b/app/globals.css index f5b1c74..27a8295 100644 --- a/app/globals.css +++ b/app/globals.css @@ -11,6 +11,7 @@ body { @apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white; line-height: 1.8; + -webkit-tap-highlight-color: transparent; } /* Typography */ @@ -64,10 +65,30 @@ } /* Focus states */ + a, button, input, textarea { + -webkit-tap-highlight-color: transparent; + } + a:focus, button:focus, - input:focus { - @apply outline-none ring-2 ring-slate-900 ring-offset-2 rounded-sm; + input:focus, + textarea:focus, + a:active, + button:active, + input:active, + textarea:active, + a:focus-visible, + button:focus-visible, + input:focus-visible, + textarea:focus-visible { + outline: none !important; + outline: 0 !important; + box-shadow: none !important; + border-radius: inherit !important; + } + + button::-moz-focus-inner { + border: 0 !important; } } diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index 10f8e98..86a2a8a 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -1,9 +1,10 @@ 'use client'; import * as React from 'react'; -import { useState, useMemo, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Check, ChevronRight, ChevronLeft, Send, Info, Plus, Minus } from 'lucide-react'; +import { useState, useMemo, useEffect, useRef } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion'; +import { Check, ChevronRight, ChevronLeft, Send, Info, Plus, Minus, Upload, FileText, X, Trash2, Palette, RefreshCw, Calendar, Zap, ShieldCheck, AlertCircle, ShoppingCart, Globe } from 'lucide-react'; import { ConceptWebsite, ConceptTarget, @@ -12,7 +13,8 @@ import { ConceptCode, ConceptCommunication, ConceptAutomation, - ConceptPrice + ConceptPrice, + HeroArchitecture } from './Landing/ConceptIllustrations'; // Pricing constants from PRICING.md @@ -21,10 +23,8 @@ const PRICING = { PAGE: 800, FEATURE: 2000, FUNCTION: 1000, - VISUAL_STAGING: 2000, COMPLEX_INTERACTION: 1500, NEW_DATASET: 400, - ADJUST_DATASET: 200, HOSTING_MONTHLY: 120, STORAGE_EXPANSION_MONTHLY: 10, CMS_SETUP: 1500, @@ -37,42 +37,81 @@ type ProjectType = 'website' | 'app'; interface FormState { projectType: ProjectType; - pages: number; + selectedPages: string[]; + otherPages: string[]; features: string[]; + otherFeatures: string[]; functions: string[]; - visualStaging: number; + otherFunctions: string[]; + apiSystems: string[]; + otherTech: string[]; + assets: string[]; + otherAssets: string[]; complexInteractions: number; newDatasets: number; - adjustDatasets: number; cmsSetup: boolean; - apiIntegrations: number; storageExpansion: number; name: string; email: string; + role: string; message: string; + sitemapFile: File | null; + contactFiles: File[]; + // Design + designVibe: string; + colorScheme: string[]; + references: string[]; + designWishes: string; + // Maintenance + expectedAdjustments: string; + languagesCount: number; + // Timeline + deadline: string; } const initialState: FormState = { projectType: 'website', - pages: 1, + selectedPages: ['Home'], + otherPages: [], features: [], + otherFeatures: [], functions: [], - visualStaging: 0, + otherFunctions: [], + apiSystems: [], + otherTech: [], + assets: [], + otherAssets: [], complexInteractions: 0, newDatasets: 0, - adjustDatasets: 0, cmsSetup: false, - apiIntegrations: 0, storageExpansion: 0, name: '', email: '', + role: '', message: '', + sitemapFile: null, + contactFiles: [], + designVibe: 'minimal', + colorScheme: ['#ffffff', '#f8fafc', '#0f172a'], + references: [], + designWishes: '', + expectedAdjustments: 'low', + languagesCount: 1, + deadline: 'flexible', }; +const PAGE_SAMPLES = [ + { id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' }, + { id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' }, + { id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' }, + { id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' }, + { id: 'Landing', label: 'Landingpage', desc: 'Optimiert für Marketing-Kampagnen.' }, + { id: 'Legal', label: 'Rechtliches', desc: 'Impressum & Datenschutz.' }, +]; + const FEATURE_OPTIONS = [ - { id: 'blog', label: 'Blog / Magazin', desc: 'Regelmäßige Artikel und Beiträge.' }, + { id: 'blog_news', label: 'Blog / News', desc: 'Ein Bereich für aktuelle Beiträge und Neuigkeiten.' }, { id: 'products', label: 'Produktbereich', desc: 'Katalog Ihrer Leistungen oder Produkte.' }, - { id: 'news', label: 'News / Aktuelles', desc: 'Neuigkeiten und Pressemitteilungen.' }, { id: 'jobs', label: 'Karriere / Jobs', desc: 'Stellenanzeigen und Bewerbungsoptionen.' }, { id: 'refs', label: 'Referenzen / Cases', desc: 'Präsentation Ihrer Projekte.' }, { id: 'events', label: 'Events / Termine', desc: 'Veranstaltungskalender.' }, @@ -83,36 +122,180 @@ const FUNCTION_OPTIONS = [ { id: 'filter', label: 'Filter-Systeme', desc: 'Kategorisierung und Sortierung.' }, { id: 'i18n', label: 'Mehrsprachigkeit', desc: 'Inhalte in mehreren Sprachen.' }, { id: 'pdf', label: 'PDF-Export', desc: 'Automatisierte PDF-Erstellung.' }, - { id: 'api', label: 'API-Anbindung', desc: 'Daten-Sync mit Drittsystemen.' }, { id: 'forms', label: 'Erweiterte Formulare', desc: 'Komplexe Abfragen & Logik.' }, ]; +const API_OPTIONS = [ + { id: 'crm', label: 'CRM System', desc: 'HubSpot, Salesforce, Pipedrive etc.' }, + { id: 'erp', label: 'ERP / Warenwirtschaft', desc: 'SAP, Microsoft Dynamics, Xentral etc.' }, + { id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' }, + { id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' }, + { id: 'ecommerce', label: 'E-Commerce / Shop', desc: 'Shopify, WooCommerce, Shopware Sync.' }, + { id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' }, +]; + +const ASSET_OPTIONS = [ + { id: 'logo', label: 'Logo', desc: 'Vektordatei Ihres Logos.' }, + { id: 'styleguide', label: 'Styleguide', desc: 'Farben, Schriften, Design-Vorgaben.' }, + { id: 'content_concept', label: 'Inhalts-Konzept', desc: 'Struktur und Texte sind bereits geplant.' }, + { id: 'media', label: 'Bild/Video-Material', desc: 'Professionelles Bildmaterial vorhanden.' }, + { id: 'icons', label: 'Icons', desc: 'Eigene Icon-Sets vorhanden.' }, + { id: 'illustrations', label: 'Illustrationen', desc: 'Eigene Illustrationen vorhanden.' }, + { id: 'fonts', label: 'Fonts', desc: 'Lizenzen für Hausschriften vorhanden.' }, +]; + +const DESIGN_VIBES = [ + { + id: 'minimal', + label: 'Minimalistisch', + desc: 'Viel Weißraum, klare Typografie.', + illustration: ( + + + + + + ) + }, + { + id: 'bold', + label: 'Mutig & Laut', + desc: 'Starke Kontraste, große Schriften.', + illustration: ( + + + + + ) + }, + { + id: 'nature', + label: 'Natürlich', + desc: 'Sanfte Erdtöne, organische Formen.', + illustration: ( + + + + + ) + }, + { + id: 'tech', + label: 'Technisch', + desc: 'Präzise Linien, dunkle Akzente.', + illustration: ( + + + + + + ) + }, +]; + +const HARMONIOUS_PALETTES = [ + ['#ffffff', '#f8fafc', '#0f172a'], + ['#000000', '#facc15', '#ffffff'], + ['#fdfcfb', '#e2e8f0', '#1e293b'], + ['#0f172a', '#38bdf8', '#ffffff'], + ['#fafaf9', '#78716c', '#1c1917'], + ['#f0fdf4', '#16a34a', '#064e3b'], + ['#fff7ed', '#ea580c', '#7c2d12'], + ['#f5f3ff', '#7c3aed', '#2e1065'], +]; + +function AnimatedNumber({ value }: { value: number }) { + const spring = useSpring(value, { stiffness: 50, damping: 20 }); + const display = useTransform(spring, (v) => Math.round(v).toLocaleString()); + const ref = useRef(null); + + useEffect(() => { + spring.set(value); + }, [value, spring]); + + useEffect(() => { + return display.on('change', (v) => { + if (ref.current) ref.current.textContent = v; + }); + }, [display]); + + return {value.toLocaleString()}; +} + export function ContactForm() { + const router = useRouter(); + const searchParams = useSearchParams(); const [stepIndex, setStepIndex] = useState(0); const [state, setState] = useState(initialState); const [isSubmitted, setIsSubmitted] = useState(false); + const formContainerRef = useRef(null); + + // URL Binding + useEffect(() => { + const step = searchParams.get('step'); + if (step) setStepIndex(parseInt(step)); + + const config = searchParams.get('config'); + if (config) { + try { + const decoded = JSON.parse(decodeURIComponent(escape(atob(config)))); + setState(s => ({ ...s, ...decoded })); + } catch (e) { + console.error("Failed to decode config", e); + } + } + }, []); + + useEffect(() => { + const params = new URLSearchParams(searchParams); + params.set('step', stepIndex.toString()); + + const configData = { + projectType: state.projectType, + selectedPages: state.selectedPages, + features: state.features, + functions: state.functions, + apiSystems: state.apiSystems, + cmsSetup: state.cmsSetup, + languagesCount: state.languagesCount, + deadline: state.deadline, + designVibe: state.designVibe, + colorScheme: state.colorScheme + }; + + const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData)))); + params.set('config', stateString); + + router.replace(`?${params.toString()}`, { scroll: false }); + }, [state.projectType, state.selectedPages, state.features, state.functions, state.apiSystems, state.cmsSetup, state.languagesCount, state.deadline, state.designVibe, state.colorScheme, stepIndex]); + + const totalPagesCount = useMemo(() => { + return state.selectedPages.length + state.otherPages.length; + }, [state.selectedPages, state.otherPages]); const totalPrice = useMemo(() => { if (state.projectType !== 'website') return 0; let total = PRICING.BASE_WEBSITE; - total += state.pages * PRICING.PAGE; - total += state.features.length * PRICING.FEATURE; - total += state.functions.length * PRICING.FUNCTION; - total += state.visualStaging * PRICING.VISUAL_STAGING; + total += totalPagesCount * PRICING.PAGE; + total += (state.features.length + state.otherFeatures.length) * PRICING.FEATURE; + total += (state.functions.length + state.otherFunctions.length) * PRICING.FUNCTION; total += state.complexInteractions * PRICING.COMPLEX_INTERACTION; total += state.newDatasets * PRICING.NEW_DATASET; - total += state.adjustDatasets * PRICING.ADJUST_DATASET; - total += state.apiIntegrations * PRICING.API_INTEGRATION; + total += (state.apiSystems.length + state.otherTech.length) * PRICING.API_INTEGRATION; if (state.cmsSetup) { total += PRICING.CMS_SETUP; - // CMS connection per feature that is selected - total += state.features.length * PRICING.CMS_CONNECTION_PER_FEATURE; + total += (state.features.length + state.otherFeatures.length) * PRICING.CMS_CONNECTION_PER_FEATURE; } - return total; - }, [state]); + // Multi-language factor (e.g. +20% per additional language) + if (state.languagesCount > 1) { + total *= (1 + (state.languagesCount - 1) * 0.2); + } + + return Math.round(total); + }, [state, totalPagesCount]); const monthlyPrice = useMemo(() => { if (state.projectType !== 'website') return 0; @@ -123,517 +306,702 @@ export function ContactForm() { setState((s) => ({ ...s, ...updates })); }; - const toggleFeature = (id: string) => { - setState(s => ({ - ...s, - features: s.features.includes(id) - ? s.features.filter(f => f !== id) - : [...s.features, id] - })); + const toggleItem = (list: string[], id: string) => { + return list.includes(id) ? list.filter(i => i !== id) : [...list, id]; }; - const toggleFunction = (id: string) => { - setState(s => ({ - ...s, - functions: s.functions.includes(id) - ? s.functions.filter(f => f !== id) - : [...s.functions, id] - })); - }; + const scrollToTop = () => { + if (formContainerRef.current) { + const offset = 120; + const bodyRect = document.body.getBoundingClientRect().top; + const elementRect = formContainerRef.current.getBoundingClientRect().top; + const elementPosition = elementRect - bodyRect; + const offsetPosition = elementPosition - offset; - const steps = [ - { - id: 'type', - title: 'Das Ziel', - description: 'Was möchten Sie realisieren?', - illustration: - }, - { - id: 'structure', - title: 'Die Basis', - description: 'Wie viele individuelle Seiten-Layouts benötigen wir?', - illustration: - }, - { - id: 'features', - title: 'Die Systeme', - description: 'Welche inhaltlichen Bereiche soll die Website haben?', - illustration: - }, - { - id: 'functions', - title: 'Die Logik', - description: 'Welche Funktionen werden benötigt?', - illustration: - }, - { - id: 'experience', - title: 'Das Erlebnis', - description: 'Soll die Seite durch besondere Effekte glänzen?', - illustration: - }, - { - id: 'content', - title: 'Die Pflege', - description: 'Wer kümmert sich um die Daten?', - illustration: - }, - { - id: 'contact', - title: 'Der Start', - description: 'Erzählen Sie mir mehr über Ihr Vorhaben.', - illustration: - }, - ]; - - const activeSteps = useMemo(() => { - if (state.projectType === 'website') return steps; - return [steps[0], steps[6]]; - }, [state.projectType]); - - useEffect(() => { - if (stepIndex >= activeSteps.length) { - setStepIndex(activeSteps.length - 1); + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); } - }, [activeSteps, stepIndex]); + }; const nextStep = () => { if (stepIndex < activeSteps.length - 1) { setStepIndex(stepIndex + 1); + setTimeout(scrollToTop, 50); } }; const prevStep = () => { if (stepIndex > 0) { setStepIndex(stepIndex - 1); + setTimeout(scrollToTop, 50); } }; - const Counter = ({ - label, - value, - onChange, - description, - }: { - label: string; - value: number; - onChange: (val: number) => void; - description?: string; - }) => ( -
-
-

{label}

- {description &&

{description}

} -
-
- - {value} - -
-
- ); + const randomizeColors = () => { + const palette = HARMONIOUS_PALETTES[Math.floor(Math.random() * HARMONIOUS_PALETTES.length)]; + updateState({ colorScheme: palette }); + }; - const Checkbox = ({ - label, - desc, - checked, - onChange + const RepeatableList = ({ + items, + onAdd, + onRemove, + placeholder }: { - label: string; - desc: string; - checked: boolean; - onChange: () => void; - }) => ( + items: string[]; + onAdd: (val: string) => void; + onRemove: (index: number) => void; + placeholder: string; + }) => { + const [input, setInput] = useState(''); + return ( +
+
+ setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (input.trim()) { + onAdd(input.trim()); + setInput(''); + } + } + }} + placeholder={placeholder} + className="flex-1 p-6 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors text-sm" + /> + +
+
+ + {items.map((item, i) => ( + + {item} + + + ))} + +
+
+ ); + }; + + const steps = [ + { id: 'type', title: 'Das Ziel', description: 'Was möchten Sie realisieren?', illustration: }, + { id: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: }, + { id: 'features', title: 'Die Systeme', description: 'Welche inhaltlichen Bereiche planen wir?', illustration: }, + { id: 'design', title: 'Design-Wünsche', description: 'Wie soll die Seite wirken?', illustration: }, + { id: 'assets', title: 'Ihre Assets', description: 'Was bringen Sie bereits mit?', illustration: }, + { id: 'functions', title: 'Die Logik', description: 'Welche Funktionen werden benötigt?', illustration: }, + { id: 'api', title: 'Schnittstellen', description: 'Datenaustausch mit Drittsystemen.', illustration: }, + { id: 'content', title: 'Die Pflege', description: 'Wer kümmert sich um die Daten?', illustration: }, + { id: 'timeline', title: 'Zeitplan', description: 'Wann soll das Projekt live gehen?', illustration: }, + { id: 'contact', title: 'Der Start', description: 'Erzählen Sie mir mehr über Ihr Vorhaben.', illustration: }, + ]; + + const activeSteps = useMemo(() => { + if (state.projectType === 'website') return steps; + return [steps[0], steps[8], steps[9]]; + }, [state.projectType]); + + useEffect(() => { + if (stepIndex >= activeSteps.length) setStepIndex(activeSteps.length - 1); + }, [activeSteps, stepIndex]); + + const Checkbox = ({ label, desc, checked, onChange }: { label: string; desc: string; checked: boolean; onChange: () => void }) => ( ); const renderStepContent = () => { const currentStep = activeSteps[stepIndex]; - switch (currentStep.id) { case 'type': return (
{[ - { id: 'website', label: 'Website', desc: 'Eine klassische Webpräsenz, ein Portfolio oder ein Blog.' }, - { id: 'app', label: 'App / Software', desc: 'Ein internes Tool, ein Dashboard oder eine komplexe Prozess-Logik.' }, + { id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: }, + { id: 'app', label: 'App / Software', desc: 'Internes Tool, Dashboard oder Prozess-Logik.', illustration: }, ].map((type) => ( ))}
); - - case 'structure': + case 'base': return (
- updateState({ pages: v })} - description="Wie viele grundlegend verschiedene Seiten benötigen wir? (z.B. Startseite, Leistungs-Übersicht, Kontakt, Über uns). Jede Seite wird individuell gestaltet und umgesetzt." - /> +
+ {PAGE_SAMPLES.map(p => ( + updateState({ selectedPages: toggleItem(state.selectedPages, p.id) })} + /> + ))} +
+
+
+

Weitere Seiten?

+ updateState({ otherPages: [...state.otherPages, v] })} + onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })} + placeholder="z.B. Team-Detail, FAQ..." + /> +
+
+

Sitemap hochladen (optional)

+
{ e.preventDefault(); e.stopPropagation(); }} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + const file = e.dataTransfer.files?.[0]; + if (file) updateState({ sitemapFile: file }); + }} + onClick={() => document.getElementById('sitemap-upload')?.click()} + > + { + const file = e.target.files?.[0]; + if (file) updateState({ sitemapFile: file }); + }} /> + {state.sitemapFile ? ( +
+ + {state.sitemapFile.name} + +
+ ) : ( + <> + +

Sitemap hierher ziehen oder klicken

+ + )} +
+
+

Was zählt als Seite?

-

Eine Seite ist ein eigenständiges Layout. Wenn Sie 10 Leistungen haben, die alle das gleiche Layout nutzen, zählt das als 1 Layout (Seite) plus ein "Feature" für die Verwaltung der Leistungen.

+

Eine Seite ist ein eigenständiges Layout. Wenn Sie 10 Leistungen haben, die alle das gleiche Layout nutzen, zählt das als 1 Seite plus ein "System-Modul" für die Verwaltung der Leistungen.

); - case 'features': return ( -
- {FEATURE_OPTIONS.map(opt => ( - toggleFeature(opt.id)} +
+
+ {FEATURE_OPTIONS.map(opt => ( + updateState({ features: toggleItem(state.features, opt.id) })} + /> + ))} +
+
+

Weitere inhaltliche Bereiche?

+ updateState({ otherFeatures: [...state.otherFeatures, v] })} + onRemove={(i) => updateState({ otherFeatures: state.otherFeatures.filter((_, idx) => idx !== i) })} + placeholder="z.B. Partner-Portal, Download-Center..." /> - ))} +
); + case 'design': + return ( +
+
+

Design-Vibe wählen

+
+ {DESIGN_VIBES.map(vibe => ( + + ))} +
+
+
+
+

Farbschema

+ +
+
+ {state.colorScheme.map((color, i) => ( +
+
+ { + const newColors = [...state.colorScheme]; + newColors[i] = e.target.value; + updateState({ colorScheme: newColors }); + }} + className="absolute inset-0 w-[200%] h-[200%] -translate-x-1/4 -translate-y-1/4 cursor-pointer" + /> +
+ {color} +
+ ))} + +
+
+ +
+

Referenz-Webseiten

+ updateState({ references: [...state.references, v] })} + onRemove={(i) => updateState({ references: state.references.filter((_, idx) => idx !== i) })} + placeholder="https://..." + /> +
+ +
+

Besondere Wünsche

+