contact
This commit is contained in:
@@ -2,18 +2,25 @@ import * as React from 'react';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Section } from '../../src/components/Section';
|
||||
import { ContactForm } from '../../src/components/ContactForm';
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
|
||||
<PageHeader
|
||||
title={<>Kontakt <br /><span className="text-slate-200">& Anfrage.</span></>}
|
||||
description="Haben Sie ein Projekt im Kopf? Schreiben Sie mir einfach. Ich antworte meistens innerhalb von 24 Stunden."
|
||||
title={<>Projekt <br /><span className="text-slate-200">konfigurieren.</span></>}
|
||||
description="Nutzen Sie den Konfigurator für eine erste Einschätzung oder schreiben Sie mir direkt eine Email."
|
||||
backLink={{ href: '/', label: 'Zurück' }}
|
||||
backgroundSymbol="@"
|
||||
backgroundSymbol="?"
|
||||
/>
|
||||
|
||||
<Section number="01" title="Direkt">
|
||||
<Section number="01" title="Konfigurator" containerVariant="wide">
|
||||
<Reveal delay={0.2}>
|
||||
<ContactForm />
|
||||
</Reveal>
|
||||
</Section>
|
||||
|
||||
<Section number="02" title="Direkt">
|
||||
<div className="grid grid-cols-1 gap-24">
|
||||
<Reveal delay={0.4}>
|
||||
<div className="space-y-8">
|
||||
@@ -28,20 +35,6 @@ export default function ContactPage() {
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.6}>
|
||||
<div className="p-12 md:p-20 border border-slate-200 rounded-[3rem] bg-white relative overflow-hidden hover:border-slate-400 transition-all duration-500">
|
||||
<div className="absolute top-0 right-0 text-[15rem] font-bold text-slate-50 select-none translate-x-1/4 -translate-y-1/4 opacity-50">!</div>
|
||||
<div className="relative z-10 space-y-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900">Was ich von Ihnen brauche:</h2>
|
||||
<ul className="space-y-6 text-xl md:text-2xl text-slate-600 font-serif italic">
|
||||
<li className="flex gap-6"><span className="text-slate-900 font-bold font-sans not-italic">01</span> Was ist das Ziel des Projekts?</li>
|
||||
<li className="flex gap-6"><span className="text-slate-900 font-bold font-sans not-italic">02</span> Gibt es eine Deadline?</li>
|
||||
<li className="flex gap-6"><span className="text-slate-900 font-bold font-sans not-italic">03</span> Welches Budget ist eingeplant?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
641
src/components/ContactForm.tsx
Normal file
641
src/components/ContactForm.tsx
Normal file
@@ -0,0 +1,641 @@
|
||||
'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 {
|
||||
ConceptWebsite,
|
||||
ConceptTarget,
|
||||
ConceptPrototyping,
|
||||
ConceptSystem,
|
||||
ConceptCode,
|
||||
ConceptCommunication,
|
||||
ConceptAutomation,
|
||||
ConceptPrice
|
||||
} from './Landing/ConceptIllustrations';
|
||||
|
||||
// Pricing constants from PRICING.md
|
||||
const PRICING = {
|
||||
BASE_WEBSITE: 6000,
|
||||
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,
|
||||
CMS_CONNECTION_PER_FEATURE: 800,
|
||||
API_INTEGRATION: 1000,
|
||||
APP_HOURLY: 120,
|
||||
};
|
||||
|
||||
type ProjectType = 'website' | 'app';
|
||||
|
||||
interface FormState {
|
||||
projectType: ProjectType;
|
||||
pages: number;
|
||||
features: string[];
|
||||
functions: string[];
|
||||
visualStaging: number;
|
||||
complexInteractions: number;
|
||||
newDatasets: number;
|
||||
adjustDatasets: number;
|
||||
cmsSetup: boolean;
|
||||
apiIntegrations: number;
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const initialState: FormState = {
|
||||
projectType: 'website',
|
||||
pages: 1,
|
||||
features: [],
|
||||
functions: [],
|
||||
visualStaging: 0,
|
||||
complexInteractions: 0,
|
||||
newDatasets: 0,
|
||||
adjustDatasets: 0,
|
||||
cmsSetup: false,
|
||||
apiIntegrations: 0,
|
||||
storageExpansion: 0,
|
||||
name: '',
|
||||
email: '',
|
||||
message: '',
|
||||
};
|
||||
|
||||
const FEATURE_OPTIONS = [
|
||||
{ id: 'blog', label: 'Blog / Magazin', desc: 'Regelmäßige Artikel und Beiträge.' },
|
||||
{ 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.' },
|
||||
];
|
||||
|
||||
const FUNCTION_OPTIONS = [
|
||||
{ id: 'search', label: 'Suche', desc: 'Volltextsuche über alle Inhalte.' },
|
||||
{ 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.' },
|
||||
];
|
||||
|
||||
export function ContactForm() {
|
||||
const [stepIndex, setStepIndex] = useState(0);
|
||||
const [state, setState] = useState<FormState>(initialState);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
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 += state.complexInteractions * PRICING.COMPLEX_INTERACTION;
|
||||
total += state.newDatasets * PRICING.NEW_DATASET;
|
||||
total += state.adjustDatasets * PRICING.ADJUST_DATASET;
|
||||
total += state.apiIntegrations * 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;
|
||||
}
|
||||
|
||||
return total;
|
||||
}, [state]);
|
||||
|
||||
const monthlyPrice = useMemo(() => {
|
||||
if (state.projectType !== 'website') return 0;
|
||||
return PRICING.HOSTING_MONTHLY + (state.storageExpansion * PRICING.STORAGE_EXPANSION_MONTHLY);
|
||||
}, [state]);
|
||||
|
||||
const updateState = (updates: Partial<FormState>) => {
|
||||
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 toggleFunction = (id: string) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
functions: s.functions.includes(id)
|
||||
? s.functions.filter(f => f !== id)
|
||||
: [...s.functions, id]
|
||||
}));
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 'type',
|
||||
title: 'Das Ziel',
|
||||
description: 'Was möchten Sie realisieren?',
|
||||
illustration: <ConceptTarget className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'structure',
|
||||
title: 'Die Basis',
|
||||
description: 'Wie viele individuelle Seiten-Layouts benötigen wir?',
|
||||
illustration: <ConceptWebsite className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
title: 'Die Systeme',
|
||||
description: 'Welche inhaltlichen Bereiche soll die Website haben?',
|
||||
illustration: <ConceptPrototyping className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
title: 'Die Logik',
|
||||
description: 'Welche Funktionen werden benötigt?',
|
||||
illustration: <ConceptCode className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'experience',
|
||||
title: 'Das Erlebnis',
|
||||
description: 'Soll die Seite durch besondere Effekte glänzen?',
|
||||
illustration: <ConceptAutomation className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'Die Pflege',
|
||||
description: 'Wer kümmert sich um die Daten?',
|
||||
illustration: <ConceptSystem className="w-full h-full" />
|
||||
},
|
||||
{
|
||||
id: 'contact',
|
||||
title: 'Der Start',
|
||||
description: 'Erzählen Sie mir mehr über Ihr Vorhaben.',
|
||||
illustration: <ConceptCommunication className="w-full h-full" />
|
||||
},
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}, [activeSteps, stepIndex]);
|
||||
|
||||
const nextStep = () => {
|
||||
if (stepIndex < activeSteps.length - 1) {
|
||||
setStepIndex(stepIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (stepIndex > 0) {
|
||||
setStepIndex(stepIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const Counter = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
description,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (val: number) => void;
|
||||
description?: string;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-4 p-8 bg-white border border-slate-100 rounded-[2rem] hover:border-slate-200 transition-colors">
|
||||
<div>
|
||||
<h4 className="text-xl font-bold text-slate-900">{label}</h4>
|
||||
{description && <p className="text-sm text-slate-500 mt-2 leading-relaxed">{description}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-8 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(Math.max(0, value - 1))}
|
||||
className="w-12 h-12 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors"
|
||||
>
|
||||
<Minus size={20} />
|
||||
</button>
|
||||
<span className="text-3xl font-bold w-12 text-center">{value}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(value + 1)}
|
||||
className="w-12 h-12 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors"
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Checkbox = ({
|
||||
label,
|
||||
desc,
|
||||
checked,
|
||||
onChange
|
||||
}: {
|
||||
label: string;
|
||||
desc: string;
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex items-start gap-4 ${
|
||||
checked
|
||||
? 'border-slate-900 bg-slate-900 text-white'
|
||||
: 'border-slate-100 bg-white hover:border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<div className={`mt-1 w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 ${checked ? 'border-white bg-white text-slate-900' : 'border-slate-200'}`}>
|
||||
{checked && <Check size={14} strokeWidth={4} />}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold mb-1">{label}</h4>
|
||||
<p className={`text-sm leading-relaxed ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
const renderStepContent = () => {
|
||||
const currentStep = activeSteps[stepIndex];
|
||||
|
||||
switch (currentStep.id) {
|
||||
case 'type':
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ 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.' },
|
||||
].map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
type="button"
|
||||
onClick={() => updateState({ projectType: type.id as ProjectType })}
|
||||
className={`p-10 rounded-[2.5rem] border-2 text-left transition-all duration-300 ${
|
||||
state.projectType === type.id
|
||||
? 'border-slate-900 bg-slate-900 text-white'
|
||||
: 'border-slate-100 bg-white hover:border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<h4 className="text-2xl font-bold mb-3">{type.label}</h4>
|
||||
<p className={`text-lg leading-relaxed ${state.projectType === type.id ? 'text-slate-300' : 'text-slate-500'}`}>{type.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'structure':
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Counter
|
||||
label="Anzahl der Seiten-Layouts"
|
||||
value={state.pages}
|
||||
onChange={(v) => 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."
|
||||
/>
|
||||
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">
|
||||
<Info className="text-slate-400 shrink-0 mt-1" size={24} />
|
||||
<div className="text-sm text-slate-600 space-y-2 leading-relaxed">
|
||||
<p className="font-bold text-slate-900">Was zählt als Seite?</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'features':
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{FEATURE_OPTIONS.map(opt => (
|
||||
<Checkbox
|
||||
key={opt.id}
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.features.includes(opt.id)}
|
||||
onChange={() => toggleFeature(opt.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'functions':
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{FUNCTION_OPTIONS.map(opt => (
|
||||
<Checkbox
|
||||
key={opt.id}
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.functions.includes(opt.id)}
|
||||
onChange={() => toggleFunction(opt.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'experience':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Counter
|
||||
label="Besondere Design-Abschnitte"
|
||||
value={state.visualStaging}
|
||||
onChange={(v) => updateState({ visualStaging: v })}
|
||||
description="Wie viele Sektionen sollen ein High-End Erlebnis bieten? (z.B. komplexe Scroll-Animationen, interaktive Storytelling-Elemente)."
|
||||
/>
|
||||
<Counter
|
||||
label="Komplexe Interaktionen"
|
||||
value={state.complexInteractions}
|
||||
onChange={(v) => updateState({ complexInteractions: v })}
|
||||
description="Benötigen Sie spezielle Tools wie einen Produkt-Konfigurator oder eine Live-Vorschau?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'content':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between p-8 bg-white border border-slate-100 rounded-[2rem]">
|
||||
<div className="max-w-[70%]">
|
||||
<h4 className="text-xl font-bold text-slate-900">Selbst verwalten (CMS)</h4>
|
||||
<p className="text-sm text-slate-500 mt-1">Möchten Sie Texte und Bilder jederzeit selbst über eine einfache Oberfläche ändern können?</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateState({ cmsSetup: !state.cmsSetup })}
|
||||
className={`w-16 h-9 rounded-full transition-colors relative ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`}
|
||||
>
|
||||
<div className={`absolute top-1 left-1 w-7 h-7 bg-white rounded-full transition-transform ${state.cmsSetup ? 'translate-x-7' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Counter
|
||||
label="Inhalte einpflegen"
|
||||
value={state.newDatasets}
|
||||
onChange={(v) => updateState({ newDatasets: v })}
|
||||
description="Für wie viele Datensätze (z.B. Blogartikel, Produkte) soll ich die initiale Befüllung übernehmen?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'contact':
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ihr Name"
|
||||
required
|
||||
value={state.name}
|
||||
onChange={(e) => updateState({ name: e.target.value })}
|
||||
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Ihre Email"
|
||||
required
|
||||
value={state.email}
|
||||
onChange={(e) => updateState({ email: e.target.value })}
|
||||
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
placeholder="Erzählen Sie mir kurz von Ihrem Projekt..."
|
||||
value={state.message}
|
||||
onChange={(e) => updateState({ message: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (stepIndex === activeSteps.length - 1) {
|
||||
setIsSubmitted(true);
|
||||
} else {
|
||||
nextStep();
|
||||
}
|
||||
};
|
||||
|
||||
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 habe Ihre Konfiguration erhalten und 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"
|
||||
>
|
||||
Neue Anfrage starten
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
<div className="lg:col-span-8 space-y-12">
|
||||
{/* Progress Header */}
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-8">
|
||||
<div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4">
|
||||
{activeSteps[stepIndex].illustration}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400">Schritt {stepIndex + 1} von {activeSteps.length}</span>
|
||||
<h3 className="text-4xl font-bold tracking-tight">{activeSteps[stepIndex].title}</h3>
|
||||
<p className="text-xl text-slate-500 leading-relaxed">{activeSteps[stepIndex].description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{activeSteps.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-1.5 flex-1 rounded-full transition-all duration-700 ${i <= stepIndex ? 'bg-slate-900' : 'bg-slate-100'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="min-h-[450px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeSteps[stepIndex].id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
|
||||
>
|
||||
{renderStepContent()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex justify-between mt-16">
|
||||
{stepIndex > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex items-center gap-3 px-10 py-5 rounded-full border border-slate-200 hover:border-slate-900 transition-all font-bold text-lg"
|
||||
>
|
||||
<ChevronLeft size={24} /> Zurück
|
||||
</button>
|
||||
) : <div />}
|
||||
|
||||
{stepIndex < activeSteps.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="flex items-center gap-3 px-10 py-5 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold text-lg shadow-xl shadow-slate-200"
|
||||
>
|
||||
Weiter <ChevronRight size={24} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!state.email || !state.name}
|
||||
className="flex items-center gap-3 px-10 py-5 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-slate-200"
|
||||
>
|
||||
Anfrage senden <Send size={24} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 lg:sticky lg:top-24">
|
||||
<div className="p-10 bg-slate-50 border border-slate-100 rounded-[3rem] space-y-10">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<ConceptPrice className="w-8 h-8" />
|
||||
<h3 className="text-2xl font-bold">Kalkulation</h3>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">Basierend auf Ihren aktuellen Angaben.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{state.projectType === 'website' ? (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-4 border-b border-slate-200">
|
||||
<span className="text-slate-600 font-medium">Basis Website</span>
|
||||
<span className="font-bold text-lg">{PRICING.BASE_WEBSITE.toLocaleString()} €</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2 hide-scrollbar">
|
||||
{state.pages > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{state.pages}x Seiten-Layout</span>
|
||||
<span className="font-medium">{(state.pages * PRICING.PAGE).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
{state.features.length > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{state.features.length}x System-Modul</span>
|
||||
<span className="font-medium">{(state.features.length * PRICING.FEATURE).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
{state.functions.length > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{state.functions.length}x Logik-Funktion</span>
|
||||
<span className="font-medium">{(state.functions.length * PRICING.FUNCTION).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
{state.visualStaging > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{state.visualStaging}x Design-Inszenierung</span>
|
||||
<span className="font-medium">{(state.visualStaging * PRICING.VISUAL_STAGING).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
{state.complexInteractions > 0 && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">{state.complexInteractions}x Interaktion</span>
|
||||
<span className="font-medium">{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
{state.cmsSetup && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-slate-500">CMS Setup & Anbindung</span>
|
||||
<span className="font-medium">{(PRICING.CMS_SETUP + state.features.length * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} €</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-8 space-y-2">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-2xl font-bold">Gesamt</span>
|
||||
<div className="text-right">
|
||||
<span className="text-4xl font-bold tracking-tighter">{totalPrice.toLocaleString()} €</span>
|
||||
<p className="text-[10px] text-slate-400 mt-1 uppercase tracking-widest font-bold">Einmalig / Netto</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-slate-200 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600 font-medium">Betrieb & Hosting</span>
|
||||
<span className="font-bold text-lg">{monthlyPrice.toLocaleString()} € / Monat</span>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-[2rem] text-xs text-slate-500 flex gap-4 leading-relaxed border border-slate-100">
|
||||
<Info size={18} className="shrink-0 text-slate-300" />
|
||||
<p>Inklusive Hosting, Sicherheitsupdates, Backups und Analytics-Reports.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="py-12 text-center space-y-6">
|
||||
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center mx-auto shadow-sm">
|
||||
<ConceptAutomation className="w-12 h-12" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-slate-600 text-sm leading-relaxed">
|
||||
Apps und Individual-Software werden nach tatsächlichem Aufwand abgerechnet.
|
||||
</p>
|
||||
<p className="text-3xl font-bold">{PRICING.APP_HOURLY} € <span className="text-lg text-slate-400 font-normal">/ Std.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] leading-relaxed text-slate-400 italic text-center">
|
||||
Ein verbindliches Angebot erstelle ich nach einem persönlichen Gespräch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ interface SectionProps {
|
||||
variant?: 'white' | 'gray';
|
||||
borderTop?: boolean;
|
||||
connector?: React.ReactNode;
|
||||
containerVariant?: 'narrow' | 'normal' | 'wide';
|
||||
}
|
||||
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
@@ -21,49 +22,84 @@ export const Section: React.FC<SectionProps> = ({
|
||||
variant = 'white',
|
||||
borderTop = false,
|
||||
connector,
|
||||
containerVariant = 'narrow',
|
||||
}) => {
|
||||
const bgClass = variant === 'gray' ? 'bg-slate-50' : 'bg-white';
|
||||
const borderClass = borderTop ? 'border-t border-slate-100' : '';
|
||||
const containerClass = containerVariant === 'wide' ? 'wide-container' : containerVariant === 'normal' ? 'container' : 'narrow-container';
|
||||
|
||||
// If no number and title, or if we want to force a simple layout, we could add a prop.
|
||||
// But let's make it smart: if it's wide, maybe we want the title on top anyway?
|
||||
// The user specifically asked to move it above for the configurator.
|
||||
|
||||
const isTopTitle = containerVariant === 'wide';
|
||||
|
||||
return (
|
||||
<section className={`relative py-24 md:py-32 group ${bgClass} ${borderClass} ${className}`}>
|
||||
<div className="narrow-container">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-16">
|
||||
{/* Sidebar: Number & Title */}
|
||||
<div className="md:col-span-3 relative">
|
||||
{/* Connector Line */}
|
||||
{connector && (
|
||||
<div className="absolute left-[2.5rem] top-0 bottom-0 w-24 hidden md:block -z-10 pointer-events-none">
|
||||
{connector}
|
||||
</div>
|
||||
<div className={containerClass}>
|
||||
{isTopTitle ? (
|
||||
<div className="space-y-16">
|
||||
{(number || title) && (
|
||||
<div className="flex flex-col md:flex-row md:items-end gap-6 md:gap-12">
|
||||
{number && (
|
||||
<Reveal delay={delay}>
|
||||
<span className="block text-6xl md:text-8xl font-bold text-slate-100 leading-none select-none">
|
||||
{number}
|
||||
</span>
|
||||
</Reveal>
|
||||
)}
|
||||
{title && (
|
||||
<Reveal delay={delay + 0.1}>
|
||||
<div className="flex items-center gap-3 mb-2 md:mb-4">
|
||||
<div className="h-px w-6 bg-slate-900"></div>
|
||||
<h2 className="text-xs font-bold uppercase tracking-[0.3em] text-slate-900">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="md:sticky md:top-32 space-y-6">
|
||||
{number && (
|
||||
<Reveal delay={delay}>
|
||||
<span className="block text-6xl md:text-8xl font-bold text-slate-100 leading-none select-none relative bg-white/0 backdrop-blur-[2px]">
|
||||
{number}
|
||||
</span>
|
||||
</Reveal>
|
||||
)}
|
||||
{title && (
|
||||
<Reveal delay={delay + 0.1}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-px w-6 bg-slate-900"></div>
|
||||
<h2 className="text-xs font-bold uppercase tracking-[0.3em] text-slate-900">
|
||||
{title}
|
||||
</h2>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-16">
|
||||
{/* Sidebar: Number & Title */}
|
||||
<div className="md:col-span-3 relative">
|
||||
{/* Connector Line */}
|
||||
{connector && (
|
||||
<div className="absolute left-[2.5rem] top-0 bottom-0 w-24 hidden md:block -z-10 pointer-events-none">
|
||||
{connector}
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
<div className="md:sticky md:top-32 space-y-6">
|
||||
{number && (
|
||||
<Reveal delay={delay}>
|
||||
<span className="block text-6xl md:text-8xl font-bold text-slate-100 leading-none select-none relative bg-white/0 backdrop-blur-[2px]">
|
||||
{number}
|
||||
</span>
|
||||
</Reveal>
|
||||
)}
|
||||
{title && (
|
||||
<Reveal delay={delay + 0.1}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-px w-6 bg-slate-900"></div>
|
||||
<h2 className="text-xs font-bold uppercase tracking-[0.3em] text-slate-900">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="md:col-span-9">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="md:col-span-9">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user