This commit is contained in:
2026-01-30 00:47:39 +01:00
parent f536765b6c
commit 36bb12f656
3 changed files with 720 additions and 50 deletions

View File

@@ -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>

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

View File

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