This commit is contained in:
2026-01-30 09:36:23 +01:00
parent 520be462f0
commit ba08724a52
5 changed files with 1523 additions and 76 deletions

View File

@@ -11,7 +11,6 @@
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 */
@@ -65,30 +64,17 @@
}
/* Focus states */
a, button, input, textarea {
-webkit-tap-highlight-color: transparent;
}
a:focus,
button:focus,
input:focus,
textarea:focus,
a:active,
button:active,
input:active,
textarea:active,
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
textarea:focus {
outline: none !important;
outline: 0 !important;
box-shadow: none !important;
border-radius: inherit !important;
}
button::-moz-focus-inner {
border: 0 !important;
/* Remove default tap highlight on mobile */
* {
-webkit-tap-highlight-color: transparent !important;
}
}
@@ -382,7 +368,8 @@
}
.highlighter-tag:focus {
@apply ring-2 ring-slate-900 ring-offset-2 -translate-y-0.5 scale-105;
@apply -translate-y-0.5 scale-105;
outline: none !important;
}
/* Marker Title Styles */

833
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts"
},
"dependencies": {
"@react-pdf/renderer": "^4.3.2",
"@types/ioredis": "^4.28.10",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
@@ -24,6 +25,7 @@
"mermaid": "^11.12.2",
"next": "^16.1.6",
"prismjs": "^1.30.0",
"qrcode": "^1.5.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"shiki": "^1.24.2",
@@ -33,6 +35,7 @@
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^25.0.6",
"@types/prismjs": "^1.26.5",
"@types/qrcode": "^1.5.6",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tsx": "^4.21.0",

View File

@@ -4,7 +4,10 @@ import * as React from '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 { Check, ChevronRight, ChevronLeft, Send, Info, Plus, Minus, Upload, FileText, X, Trash2, Palette, RefreshCw, Calendar, Zap, ShieldCheck, AlertCircle, ShoppingCart, Globe, Download, Share2 } from 'lucide-react';
import dynamic from 'next/dynamic';
import * as QRCode from 'qrcode';
import { EstimationPDF } from './EstimationPDF';
import {
ConceptWebsite,
ConceptTarget,
@@ -17,6 +20,12 @@ import {
HeroArchitecture
} from './Landing/ConceptIllustrations';
// Dynamically import PDF components to avoid SSR issues
const PDFDownloadLink = dynamic(
() => import('@react-pdf/renderer').then((mod) => mod.PDFDownloadLink),
{ ssr: false }
);
// Pricing constants from PRICING.md
const PRICING = {
BASE_WEBSITE: 6000,
@@ -131,6 +140,9 @@ const API_OPTIONS = [
{ 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: 'hr', label: 'HR / Recruiting', desc: 'Personio, Workday, Recruitee etc.' },
{ id: 'realestate', label: 'Immobilien', desc: 'OpenImmo, FlowFact, Immowelt Sync.' },
{ id: 'calendar', label: 'Termine / Booking', desc: 'Calendly, Shore, Doctolib etc.' },
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
];
@@ -228,8 +240,14 @@ export function ContactForm() {
const [stepIndex, setStepIndex] = useState(0);
const [state, setState] = useState<FormState>(initialState);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isClient, setIsClient] = useState(false);
const [qrCodeData, setQrCodeData] = useState<string>('');
const formContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setIsClient(true);
}, []);
// URL Binding
useEffect(() => {
const step = searchParams.get('step');
@@ -246,8 +264,9 @@ export function ContactForm() {
}
}, []);
useEffect(() => {
const params = new URLSearchParams(searchParams);
const currentUrl = useMemo(() => {
if (!isClient) return '';
const params = new URLSearchParams();
params.set('step', stepIndex.toString());
const configData = {
@@ -266,8 +285,15 @@ export function ContactForm() {
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]);
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
}, [state, stepIndex, isClient]);
useEffect(() => {
if (currentUrl) {
router.replace(currentUrl, { scroll: false });
QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then(setQrCodeData);
}
}, [currentUrl]);
const totalPagesCount = useMemo(() => {
return state.selectedPages.length + state.otherPages.length;
@@ -341,7 +367,15 @@ export function ContactForm() {
const randomizeColors = () => {
const palette = HARMONIOUS_PALETTES[Math.floor(Math.random() * HARMONIOUS_PALETTES.length)];
updateState({ colorScheme: palette });
// Maintain length if user added more colors
let finalPalette = [...palette];
if (state.colorScheme.length > palette.length) {
const diff = state.colorScheme.length - palette.length;
for(let i=0; i<diff; i++) {
finalPalette.push('#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0'));
}
}
updateState({ colorScheme: finalPalette });
};
const RepeatableList = ({
@@ -383,7 +417,7 @@ export function ContactForm() {
setInput('');
}
}}
className="w-16 h-16 rounded-full bg-slate-900 text-white flex items-center justify-center hover:bg-slate-800 transition-colors shrink-0 focus:outline-none overflow-hidden relative"
className="w-16 h-16 rounded-full bg-slate-900 text-white flex items-center justify-center hover:bg-slate-800 transition-colors shrink-0 focus:outline-none overflow-hidden relative rounded-[2rem]"
>
<Plus size={24} />
</button>
@@ -399,7 +433,7 @@ export function ContactForm() {
className="flex items-center gap-2 px-4 py-2 bg-slate-100 rounded-full text-sm font-medium text-slate-700"
>
<span className="truncate max-w-[200px]">{item}</span>
<button type="button" onClick={() => onRemove(i)} className="text-slate-400 hover:text-slate-900 transition-colors focus:outline-none overflow-hidden relative">
<button type="button" onClick={() => onRemove(i)} className="text-slate-400 hover:text-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full">
<X size={14} />
</button>
</motion.div>
@@ -436,7 +470,7 @@ export function ContactForm() {
<button
type="button"
onClick={onChange}
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex items-start gap-4 focus:outline-none overflow-hidden relative ${
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex items-start gap-4 focus:outline-none overflow-hidden relative rounded-[2rem] ${
checked ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
@@ -464,7 +498,7 @@ export function ContactForm() {
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 focus:outline-none overflow-hidden relative ${
className={`p-10 rounded-[2.5rem] border-2 text-left transition-all duration-300 focus:outline-none overflow-hidden relative rounded-[2.5rem] ${
state.projectType === type.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
@@ -500,7 +534,7 @@ export function ContactForm() {
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Sitemap hochladen (optional)</p>
<div
className={`relative group border-2 border-dashed rounded-[2rem] p-6 transition-all duration-300 flex flex-col items-center justify-center gap-2 cursor-pointer min-h-[120px] ${
className={`relative group border-2 border-dashed rounded-[2rem] p-6 transition-all duration-300 flex flex-col items-center justify-center gap-2 cursor-pointer min-h-[120px] rounded-[2rem] ${
state.sitemapFile ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400 bg-white'
}`}
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
@@ -520,7 +554,7 @@ export function ContactForm() {
<div className="flex items-center gap-3 text-slate-900">
<FileText size={24} />
<span className="font-bold text-sm truncate max-w-[150px]">{state.sitemapFile.name}</span>
<button type="button" onClick={(e) => { e.stopPropagation(); updateState({ sitemapFile: null }); }} className="p-1 hover:bg-slate-200 rounded-full transition-colors focus:outline-none overflow-hidden relative"><X size={16} /></button>
<button type="button" onClick={(e) => { e.stopPropagation(); updateState({ sitemapFile: null }); }} className="p-1 hover:bg-slate-200 rounded-full transition-colors focus:outline-none overflow-hidden relative rounded-full"><X size={16} /></button>
</div>
) : (
<>
@@ -574,7 +608,7 @@ export function ContactForm() {
key={vibe.id}
type="button"
onClick={() => updateState({ designVibe: vibe.id })}
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none overflow-hidden relative group ${
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none overflow-hidden relative rounded-[2rem] group ${
state.designVibe === vibe.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
@@ -584,7 +618,7 @@ export function ContactForm() {
</div>
<h4 className={`font-bold ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
</div>
<p className={`text-sm leading-relaxed ${state.designVibe === vibe.id ? 'text-slate-100' : 'text-slate-500'}`}>{vibe.desc}</p>
<p className={`text-sm leading-relaxed ${state.designVibe === vibe.id ? 'text-white opacity-90' : 'text-slate-500'}`}>{vibe.desc}</p>
</button>
))}
</div>
@@ -596,15 +630,16 @@ export function ContactForm() {
<button
type="button"
onClick={randomizeColors}
className="w-full md:w-auto flex items-center justify-center gap-3 px-8 py-4 rounded-full bg-slate-900 text-white text-sm font-bold uppercase tracking-widest hover:bg-slate-800 transition-all focus:outline-none shadow-xl shadow-slate-200 active:scale-95 overflow-hidden relative"
className="w-full md:w-auto flex items-center justify-center gap-3 px-10 py-5 rounded-full bg-slate-900 text-white text-sm font-bold uppercase tracking-[0.2em] hover:bg-slate-800 transition-all focus:outline-none shadow-2xl shadow-slate-300 active:scale-95 overflow-hidden relative rounded-full group"
>
<RefreshCw size={18} /> Farbschema würfeln
<RefreshCw size={20} className="group-hover:rotate-180 transition-transform duration-500" />
<span>Farbschema würfeln</span>
</button>
</div>
<div className="flex flex-wrap gap-6 p-8 bg-white border border-slate-100 rounded-[2rem]">
{state.colorScheme.map((color, i) => (
<div key={i} className="flex flex-col items-center gap-3">
<div className="relative w-16 h-16 rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="relative w-16 h-16 rounded-2xl border border-slate-200 shadow-sm overflow-hidden rounded-2xl">
<input
type="color"
value={color}
@@ -622,7 +657,7 @@ export function ContactForm() {
<button
type="button"
onClick={() => updateState({ colorScheme: [...state.colorScheme, '#000000'] })}
className="w-16 h-16 rounded-2xl border-2 border-dashed border-slate-200 flex items-center justify-center text-slate-300 hover:border-slate-900 hover:text-slate-900 transition-colors focus:outline-none overflow-hidden relative"
className="w-16 h-16 rounded-2xl border-2 border-dashed border-slate-200 flex items-center justify-center text-slate-300 hover:border-slate-900 hover:text-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-2xl"
>
<Plus size={24} />
</button>
@@ -645,7 +680,7 @@ export function ContactForm() {
value={state.designWishes}
onChange={(e) => updateState({ designWishes: e.target.value })}
placeholder="Erzählen Sie mir mehr über Ihre Vorstellungen..."
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors text-sm resize-none"
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors text-sm resize-none rounded-[2rem]"
rows={3}
/>
</div>
@@ -688,13 +723,27 @@ export function ContactForm() {
</div>
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Weitere Funktionen?</p>
<RepeatableList
items={state.otherFunctions}
<RepeatableList
items={state.otherFunctions}
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
placeholder="z.B. Login-Bereich, Buchungssystem..."
/>
</div>
<div className="p-8 bg-white border border-slate-100 rounded-[2rem] space-y-4">
<div className="flex justify-between items-start">
<div>
<h4 className="text-xl font-bold text-slate-900">Besondere Interaktionen</h4>
<p className="text-sm text-slate-500 mt-1">Aufwendige Animationen oder komplexe UI-Logik pro Abschnitt.</p>
</div>
<div className="flex items-center gap-6">
<button type="button" onClick={() => updateState({ complexInteractions: Math.max(0, state.complexInteractions - 1) })} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full"><Minus size={18} /></button>
<span className="text-2xl font-bold w-8 text-center">{state.complexInteractions}</span>
<button type="button" onClick={() => updateState({ complexInteractions: state.complexInteractions + 1 })} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full"><Plus size={18} /></button>
</div>
</div>
</div>
</div>
);
case 'api':
@@ -738,7 +787,7 @@ export function ContactForm() {
<button
type="button"
onClick={() => updateState({ cmsSetup: !state.cmsSetup })}
className={`w-16 h-9 rounded-full transition-colors relative focus:outline-none overflow-hidden relative ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`}
className={`w-16 h-9 rounded-full transition-colors relative focus:outline-none overflow-hidden relative rounded-full ${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>
@@ -756,12 +805,12 @@ export function ContactForm() {
key={opt.id}
type="button"
onClick={() => updateState({ expectedAdjustments: opt.id })}
className={`p-4 rounded-2xl border-2 text-left transition-all focus:outline-none overflow-hidden relative ${
className={`p-4 rounded-2xl border-2 text-left transition-all focus:outline-none overflow-hidden relative rounded-2xl ${
state.expectedAdjustments === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 bg-white hover:border-slate-400'
}`}
>
<p className="font-bold text-sm">{opt.label}</p>
<p className={`text-[10px] ${state.expectedAdjustments === opt.id ? 'text-slate-100' : 'text-slate-500'}`}>{opt.desc}</p>
<p className={`text-[10px] ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
@@ -790,9 +839,9 @@ export function ContactForm() {
<h4 className="text-xl font-bold text-slate-900 flex items-center gap-2"><Globe size={20} /> Mehrsprachigkeit</h4>
<p className="text-sm text-slate-500">Wie viele Sprachen soll die Website unterstützen?</p>
<div className="flex items-center gap-8">
<button type="button" onClick={() => updateState({ languagesCount: Math.max(1, state.languagesCount - 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 focus:outline-none overflow-hidden relative"><Minus size={20} /></button>
<button type="button" onClick={() => updateState({ languagesCount: Math.max(1, state.languagesCount - 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 focus:outline-none overflow-hidden relative rounded-full"><Minus size={20} /></button>
<span className="text-3xl font-bold w-12 text-center">{state.languagesCount}</span>
<button type="button" onClick={() => updateState({ languagesCount: state.languagesCount + 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 focus:outline-none overflow-hidden relative"><Plus size={20} /></button>
<button type="button" onClick={() => updateState({ languagesCount: state.languagesCount + 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 focus:outline-none overflow-hidden relative rounded-full"><Plus size={20} /></button>
</div>
</div>
)}
@@ -803,9 +852,9 @@ export function ContactForm() {
<p className="text-sm text-slate-500 mt-2 leading-relaxed">Für wie viele Datensätze soll ich die initiale Befüllung übernehmen?</p>
</div>
<div className="flex items-center gap-8 mt-2">
<button type="button" onClick={() => updateState({ newDatasets: Math.max(0, state.newDatasets - 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 focus:outline-none overflow-hidden relative"><Minus size={20} /></button>
<button type="button" onClick={() => updateState({ newDatasets: Math.max(0, state.newDatasets - 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 focus:outline-none overflow-hidden relative rounded-full"><Minus size={20} /></button>
<span className="text-3xl font-bold w-12 text-center">{state.newDatasets}</span>
<button type="button" onClick={() => updateState({ newDatasets: state.newDatasets + 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 focus:outline-none overflow-hidden relative"><Plus size={20} /></button>
<button type="button" onClick={() => updateState({ newDatasets: state.newDatasets + 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 focus:outline-none overflow-hidden relative rounded-full"><Plus size={20} /></button>
</div>
</div>
</div>
@@ -824,12 +873,12 @@ export function ContactForm() {
key={opt.id}
type="button"
onClick={() => updateState({ deadline: opt.id })}
className={`p-8 rounded-[2rem] border-2 text-left transition-all focus:outline-none overflow-hidden relative ${
className={`p-8 rounded-[2rem] border-2 text-left transition-all focus:outline-none overflow-hidden relative rounded-[2rem] ${
state.deadline === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
<h4 className={`font-bold mb-1 ${state.deadline === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</h4>
<p className={`text-sm ${state.deadline === opt.id ? 'text-slate-100' : 'text-slate-500'}`}>{opt.desc}</p>
<p className={`text-sm ${state.deadline === opt.id ? 'text-white opacity-80' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
@@ -847,16 +896,16 @@ export function ContactForm() {
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" />
<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 rounded-[2rem]" />
<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 rounded-[2rem]" />
</div>
<input type="text" placeholder="Ihre Rolle (z.B. CEO, Marketing Manager...)" value={state.role} onChange={(e) => updateState({ role: 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" />
<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" />
<input type="text" placeholder="Ihre Rolle (z.B. CEO, Marketing Manager...)" value={state.role} onChange={(e) => updateState({ role: 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 rounded-[2rem]" />
<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 rounded-[2rem]" />
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Dateien hochladen (optional)</p>
<div
className={`relative group border-2 border-dashed rounded-[2rem] p-8 transition-all duration-300 flex flex-col items-center justify-center gap-4 cursor-pointer min-h-[160px] ${
className={`relative group border-2 border-dashed rounded-[2rem] p-8 transition-all duration-300 flex flex-col items-center justify-center gap-4 cursor-pointer min-h-[160px] rounded-[2rem] ${
state.contactFiles.length > 0 ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400 bg-white'
}`}
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
@@ -887,7 +936,7 @@ export function ContactForm() {
e.stopPropagation();
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
}}
className="p-1 hover:bg-slate-100 rounded-full transition-colors focus:outline-none overflow-hidden relative"
className="p-1 hover:bg-slate-100 rounded-full transition-colors focus:outline-none overflow-hidden relative rounded-full"
>
<X size={16} />
</button>
@@ -914,8 +963,39 @@ export function ContactForm() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (stepIndex === activeSteps.length - 1) setIsSubmitted(true);
else nextStep();
if (stepIndex === activeSteps.length - 1) {
// Handle submission
const mailBody = `
Name: ${state.name}
Email: ${state.email}
Rolle: ${state.role}
Projekt: ${state.projectType}
Konfiguration: ${currentUrl}
Nachricht:
${state.message}
`;
window.location.href = `mailto:marc@mintel.me?subject=Projektanfrage: ${state.name}&body=${encodeURIComponent(mailBody)}`;
setIsSubmitted(true);
} else {
nextStep();
}
};
const handleShare = async () => {
if (navigator.share) {
try {
await navigator.share({
title: 'Meine Projekt-Konfiguration',
url: currentUrl
});
} catch (e) {
console.error("Share failed", e);
}
} else {
navigator.clipboard.writeText(currentUrl);
alert('Link in die Zwischenablage kopiert!');
}
};
if (isSubmitted) {
@@ -923,7 +1003,7 @@ export function ContactForm() {
<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 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 focus:outline-none overflow-hidden relative">Neue Anfrage starten</button>
<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 focus:outline-none overflow-hidden relative rounded-full">Neue Anfrage starten</button>
</motion.div>
);
}
@@ -942,18 +1022,19 @@ export function ContactForm() {
</div>
<div className="flex gap-3">
{activeSteps.map((step, i) => (
<button
key={i}
onClick={() => i < stepIndex && setStepIndex(i)}
title={step.title}
disabled={i >= stepIndex}
className={`h-1.5 flex-1 rounded-full transition-all duration-700 relative group ${i <= stepIndex ? 'bg-slate-900' : 'bg-slate-100'} ${i < stepIndex ? 'cursor-pointer' : 'cursor-default'}`}
<button
key={i}
type="button"
onClick={() => {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`h-1.5 flex-1 rounded-full transition-all duration-700 relative group ${i <= stepIndex ? 'bg-slate-900' : 'bg-slate-100'} cursor-pointer focus:outline-none`}
>
{i < stepIndex && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
{step.title}
</div>
)}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded-lg opacity-0 group-hover:opacity-100 transition-all duration-300 whitespace-nowrap pointer-events-none z-50 translate-y-1 group-hover:translate-y-0 shadow-xl">
{step.title}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-slate-900" />
</div>
</button>
))}
</div>
@@ -961,22 +1042,27 @@ export function ContactForm() {
<div className="flex justify-between items-center py-4 border-y border-slate-50">
{stepIndex > 0 ? (
<button type="button" onClick={prevStep} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden">
<button type="button" onClick={prevStep} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
<ChevronLeft size={14} /> Zurück
</button>
) : <div />}
{stepIndex < activeSteps.length - 1 && (
<button type="button" onClick={nextStep} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden">
Weiter <ChevronRight size={14} />
<div className="flex items-center gap-4">
<button type="button" onClick={handleShare} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
<Share2 size={14} /> Teilen
</button>
)}
{stepIndex < activeSteps.length - 1 && (
<button type="button" onClick={nextStep} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
Weiter <ChevronRight size={14} />
</button>
)}
</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 focus:outline-none overflow-hidden relative"><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 focus:outline-none overflow-hidden relative">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 focus:outline-none overflow-hidden relative">Anfrage senden <Send size={24} /></button>)}
{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 focus:outline-none overflow-hidden relative rounded-full"><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 focus:outline-none overflow-hidden relative rounded-full">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 focus:outline-none overflow-hidden relative rounded-full">Anfrage senden <Send size={24} /></button>)}
</div>
</form>
</div>
@@ -991,6 +1077,7 @@ export function ContactForm() {
{totalPagesCount > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalPagesCount}x Seite</span><span className="font-medium text-slate-900">{(totalPagesCount * PRICING.PAGE).toLocaleString()} </span></div>)}
{state.features.length + state.otherFeatures.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.features.length + state.otherFeatures.length}x System-Modul</span><span className="font-medium text-slate-900">{((state.features.length + state.otherFeatures.length) * PRICING.FEATURE).toLocaleString()} </span></div>)}
{state.functions.length + state.otherFunctions.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.functions.length + state.otherFunctions.length}x Logik-Funktion</span><span className="font-medium text-slate-900">{((state.functions.length + state.otherFunctions.length) * PRICING.FUNCTION).toLocaleString()} </span></div>)}
{state.complexInteractions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.complexInteractions}x Komplexes UI/Animation</span><span className="font-medium text-slate-900">{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} </span></div>)}
{state.apiSystems.length + state.otherTech.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.apiSystems.length + state.otherTech.length}x API Sync</span><span className="font-medium text-slate-900">{((state.apiSystems.length + state.otherTech.length) * PRICING.API_INTEGRATION).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 text-slate-900">{(PRICING.CMS_SETUP + (state.features.length + state.otherFeatures.length) * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} </span></div>)}
{state.newDatasets > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.newDatasets}x Inhalte einpflegen</span><span className="font-medium text-slate-900">{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} </span></div>)}
@@ -998,6 +1085,23 @@ export function ContactForm() {
</div>
<div className="pt-8 space-y-2"><div className="flex justify-between items-end"><span className="text-2xl font-bold text-slate-900">Gesamt</span><div className="text-right"><div className="text-4xl font-bold tracking-tighter text-slate-900"><AnimatedNumber value={totalPrice} /> </div><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 text-slate-900">{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="pt-6">
{isClient && (
<PDFDownloadLink
document={<EstimationPDF state={state} totalPrice={totalPrice} monthlyPrice={monthlyPrice} totalPagesCount={totalPagesCount} pricing={PRICING} qrCodeData={qrCodeData} />}
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"
>
{({ loading }) => (
<div className="flex items-center gap-3">
<Download size={18} />
<span>{loading ? 'PDF wird erstellt...' : 'Als PDF speichern'}</span>
</div>
)}
</PDFDownloadLink>
)}
</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 text-slate-900">{PRICING.APP_HOURLY} <span className="text-lg text-slate-400 font-normal">/ Std.</span></p></div></div>)}
</div>

View File

@@ -0,0 +1,520 @@
'use client';
import * as React from 'react';
import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer';
const styles = StyleSheet.create({
page: {
padding: 40,
backgroundColor: '#ffffff',
fontFamily: 'Helvetica',
fontSize: 10,
color: '#1a1a1a',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 40,
borderBottom: 2,
borderBottomColor: '#000000',
paddingBottom: 20,
},
brand: {
fontSize: 18,
fontWeight: 'bold',
letterSpacing: -0.5,
},
quoteInfo: {
textAlign: 'right',
},
quoteTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 4,
},
quoteDate: {
fontSize: 9,
color: '#666666',
},
recipientSection: {
marginBottom: 30,
},
recipientLabel: {
fontSize: 8,
color: '#999999',
textTransform: 'uppercase',
marginBottom: 4,
},
recipientName: {
fontSize: 12,
fontWeight: 'bold',
},
recipientRole: {
fontSize: 10,
color: '#666666',
},
table: {
display: 'flex',
width: 'auto',
marginBottom: 30,
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#f8fafc',
borderBottom: 1,
borderBottomColor: '#e2e8f0',
paddingVertical: 8,
paddingHorizontal: 12,
},
tableRow: {
flexDirection: 'row',
borderBottom: 1,
borderBottomColor: '#f1f5f9',
paddingVertical: 10,
paddingHorizontal: 12,
alignItems: 'flex-start',
},
colPos: { width: '8%' },
colDesc: { width: '62%' },
colQty: { width: '10%', textAlign: 'center' },
colPrice: { width: '20%', textAlign: 'right' },
headerText: {
fontSize: 8,
fontWeight: 'bold',
color: '#64748b',
textTransform: 'uppercase',
},
posText: { fontSize: 9, color: '#94a3b8' },
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 2 },
itemDesc: { fontSize: 8, color: '#64748b', lineHeight: 1.4 },
priceText: { fontSize: 10, fontWeight: 'bold' },
summarySection: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 10,
},
summaryTable: {
width: '40%',
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 4,
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 12,
borderTop: 1,
borderTopColor: '#000000',
marginTop: 8,
},
totalLabel: { fontSize: 12, fontWeight: 'bold' },
totalValue: { fontSize: 16, fontWeight: 'bold' },
configSection: {
marginTop: 20,
padding: 20,
backgroundColor: '#f8fafc',
borderRadius: 8,
},
configTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 10,
textTransform: 'uppercase',
color: '#475569',
},
configGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 20,
},
configItem: {
width: '45%',
marginBottom: 10,
},
configLabel: { fontSize: 8, color: '#94a3b8', marginBottom: 2 },
configValue: { fontSize: 9, color: '#1e293b' },
qrSection: {
marginTop: 30,
alignItems: 'center',
justifyContent: 'center',
},
qrImage: {
width: 80,
height: 80,
},
qrText: {
fontSize: 7,
color: '#94a3b8',
marginTop: 5,
},
footer: {
position: 'absolute',
bottom: 30,
left: 40,
right: 40,
borderTop: 1,
borderTopColor: '#f1f5f9',
paddingTop: 20,
flexDirection: 'row',
justifyContent: 'space-between',
fontSize: 8,
color: '#94a3b8',
},
pageNumber: {
position: 'absolute',
bottom: 30,
right: 40,
fontSize: 8,
color: '#94a3b8',
}
});
interface PDFProps {
state: any;
totalPrice: number;
monthlyPrice: number;
totalPagesCount: number;
pricing: any;
qrCodeData?: string;
}
export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount, pricing, qrCodeData }: PDFProps) => {
const date = new Date().toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const vibeLabels: Record<string, string> = {
minimal: 'Minimalistisch',
bold: 'Mutig & Laut',
nature: 'Natürlich',
tech: 'Technisch'
};
const deadlineLabels: Record<string, string> = {
asap: 'So schnell wie möglich',
'2-3-months': 'In 2-3 Monaten',
'3-6-months': 'In 3-6 Monaten',
flexible: 'Flexibel'
};
const assetLabels: Record<string, string> = {
logo: 'Logo',
styleguide: 'Styleguide',
content_concept: 'Inhalts-Konzept',
media: 'Bild/Video-Material',
icons: 'Icons',
illustrations: 'Illustrationen',
fonts: 'Fonts'
};
const featureLabels: Record<string, string> = {
blog_news: 'Blog / News',
products: 'Produktbereich',
jobs: 'Karriere / Jobs',
refs: 'Referenzen / Cases',
events: 'Events / Termine'
};
const functionLabels: Record<string, string> = {
search: 'Suche',
filter: 'Filter-Systeme',
i18n: 'Mehrsprachigkeit',
pdf: 'PDF-Export',
forms: 'Erweiterte Formulare'
};
const apiLabels: Record<string, string> = {
crm: 'CRM System',
erp: 'ERP / Warenwirtschaft',
stripe: 'Stripe / Payment',
newsletter: 'Newsletter / Marketing',
ecommerce: 'E-Commerce / Shop',
hr: 'HR / Recruiting',
realestate: 'Immobilien',
calendar: 'Termine / Booking',
social: 'Social Media Sync'
};
const positions = [];
let pos = 1;
if (state.projectType === 'website') {
positions.push({
pos: pos++,
title: 'Basis Website Setup',
desc: 'Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.',
qty: 1,
price: pricing.BASE_WEBSITE
});
const allPages = [...state.selectedPages.map(p => p === 'Home' ? 'Startseite' : p), ...state.otherPages];
positions.push({
pos: pos++,
title: 'Individuelle Seiten',
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${allPages.join(', ')}).`,
qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE
});
if (state.features.length > 0 || state.otherFeatures.length > 0) {
const allFeatures = [...state.features.map(f => featureLabels[f] || f), ...state.otherFeatures];
positions.push({
pos: pos++,
title: 'System-Module',
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(', ')}. Inklusive Datenstruktur und Darstellung.`,
qty: allFeatures.length,
price: allFeatures.length * pricing.FEATURE
});
}
if (state.functions.length > 0 || state.otherFunctions.length > 0) {
const allFunctions = [...state.functions.map(f => functionLabels[f] || f), ...state.otherFunctions];
positions.push({
pos: pos++,
title: 'Logik-Funktionen',
desc: `Erweiterte Funktionen: ${allFunctions.join(', ')}.`,
qty: allFunctions.length,
price: allFunctions.length * pricing.FUNCTION
});
}
if (state.apiSystems.length > 0 || state.otherTech.length > 0) {
const allApis = [...state.apiSystems.map(a => apiLabels[a] || a), ...state.otherTech];
positions.push({
pos: pos++,
title: 'Schnittstellen (API)',
desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(', ')}.`,
qty: allApis.length,
price: allApis.length * pricing.API_INTEGRATION
});
}
if (state.cmsSetup) {
positions.push({
pos: pos++,
title: 'Inhaltsverwaltung (CMS)',
desc: 'Einrichtung eines Systems zur eigenständigen Pflege von Inhalten und Datensätzen.',
qty: 1,
price: pricing.CMS_SETUP + (state.features.length + state.otherFeatures.length) * pricing.CMS_CONNECTION_PER_FEATURE
});
}
if (state.newDatasets > 0) {
positions.push({
pos: pos++,
title: 'Inhaltspflege (Initial)',
desc: `Manuelle Einpflege von ${state.newDatasets} Datensätzen (z.B. Produkte, Blogartikel).`,
qty: state.newDatasets,
price: state.newDatasets * pricing.NEW_DATASET
});
}
if (state.complexInteractions > 0) {
positions.push({
pos: pos++,
title: 'Besondere Interaktionen',
desc: `Umsetzung von ${state.complexInteractions} komplexen UI-Animationen oder interaktiven Logik-Abschnitten.`,
qty: state.complexInteractions,
price: state.complexInteractions * pricing.COMPLEX_INTERACTION
});
}
if (state.languagesCount > 1) {
const factorPrice = totalPrice - (totalPrice / (1 + (state.languagesCount - 1) * 0.2));
positions.push({
pos: pos++,
title: 'Mehrsprachigkeit',
desc: `Erweiterung des Systems auf ${state.languagesCount} Sprachen (Struktur & Logik).`,
qty: state.languagesCount,
price: Math.round(factorPrice)
});
}
} else {
positions.push({
pos: pos++,
title: 'App / Software Entwicklung',
desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
qty: 1,
price: 0
});
}
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<View>
<Text style={styles.brand}>marc mintel</Text>
<Text style={{ fontSize: 8, color: '#64748b', marginTop: 4 }}>Digital Systems & Design</Text>
</View>
<View style={styles.quoteInfo}>
<Text style={styles.quoteTitle}>Kostenschätzung</Text>
<Text style={styles.quoteDate}>{date}</Text>
<Text style={[styles.quoteDate, { marginTop: 2 }]}>Projekt: {state.projectType === 'website' ? 'Website' : 'Software'}</Text>
</View>
</View>
<View style={styles.recipientSection}>
<Text style={styles.recipientLabel}>Ansprechpartner</Text>
<Text style={styles.recipientName}>{state.name || 'Interessent'}</Text>
{state.role && <Text style={styles.recipientRole}>{state.role}</Text>}
<Text style={styles.recipientRole}>{state.email}</Text>
</View>
<View style={styles.table}>
<View style={styles.tableHeader}>
<Text style={[styles.headerText, styles.colPos]}>Pos</Text>
<Text style={[styles.headerText, styles.colDesc]}>Beschreibung</Text>
<Text style={[styles.headerText, styles.colQty]}>Menge</Text>
<Text style={[styles.headerText, styles.colPrice]}>Betrag</Text>
</View>
{positions.map((item, i) => (
<View key={i} style={styles.tableRow}>
<Text style={[styles.posText, styles.colPos]}>{item.pos}</Text>
<View style={styles.colDesc}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemDesc}>{item.desc}</Text>
</View>
<Text style={[styles.posText, styles.colQty]}>{item.qty}</Text>
<Text style={[styles.priceText, styles.colPrice]}>
{item.price > 0 ? `${item.price.toLocaleString()}` : 'n. A.'}
</Text>
</View>
))}
</View>
<View style={styles.summarySection}>
<View style={styles.summaryTable}>
<View style={styles.summaryRow}>
<Text style={{ color: '#64748b' }}>Zwischensumme (Netto)</Text>
<Text>{totalPrice.toLocaleString()} </Text>
</View>
<View style={styles.summaryRow}>
<Text style={{ color: '#64748b' }}>Umsatzsteuer (0%)*</Text>
<Text>0,00 </Text>
</View>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Gesamtsumme</Text>
<Text style={styles.totalValue}>{totalPrice.toLocaleString()} </Text>
</View>
<Text style={{ fontSize: 7, color: '#94a3b8', textAlign: 'right', marginTop: -4 }}>
*Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.
</Text>
{state.projectType === 'website' && (
<View style={[styles.summaryRow, { marginTop: 15, borderTop: 1, borderTopColor: '#f1f5f9', paddingTop: 10 }]}>
<Text style={{ color: '#64748b', fontSize: 9 }}>Betrieb & Hosting</Text>
<Text style={{ fontSize: 9, fontWeight: 'bold' }}>{monthlyPrice.toLocaleString()} / Monat</Text>
</View>
)}
</View>
</View>
<View style={styles.footer}>
<Text>marc@mintel.me</Text>
<Text>mintel.me</Text>
<Text>Digital Systems & Design</Text>
</View>
<Text style={styles.pageNumber} render={({ pageNumber, totalPages }) => `Seite ${pageNumber} von ${totalPages}`} fixed />
</Page>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<View>
<Text style={styles.brand}>marc mintel</Text>
</View>
<View style={styles.quoteInfo}>
<Text style={styles.quoteTitle}>Projektdetails</Text>
</View>
</View>
<View style={styles.configSection}>
<Text style={styles.configTitle}>Konfiguration & Wünsche</Text>
<View style={styles.configGrid}>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Design-Vibe</Text>
<Text style={styles.configValue}>{vibeLabels[state.designVibe] || state.designVibe}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Farbschema</Text>
<Text style={styles.configValue}>{state.colorScheme.join(', ')}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Zeitplan</Text>
<Text style={styles.configValue}>{deadlineLabels[state.deadline] || state.deadline}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Assets vorhanden</Text>
<Text style={styles.configValue}>{state.assets.map(a => assetLabels[a] || a).join(', ') || 'Keine angegeben'}</Text>
</View>
{state.otherAssets.length > 0 && (
<View style={styles.configItem}>
<Text style={styles.configLabel}>Weitere Assets</Text>
<Text style={styles.configValue}>{state.otherAssets.join(', ')}</Text>
</View>
)}
<View style={styles.configItem}>
<Text style={styles.configLabel}>Sprachen</Text>
<Text style={styles.configValue}>{state.languagesCount}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>CMS (Inhaltsverwaltung)</Text>
<Text style={styles.configValue}>{state.cmsSetup ? 'Ja' : 'Nein'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Änderungsfrequenz</Text>
<Text style={styles.configValue}>
{state.expectedAdjustments === 'low' ? 'Selten' :
state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'}
</Text>
</View>
</View>
{state.designWishes && (
<View style={{ marginTop: 15 }}>
<Text style={styles.configLabel}>Design-Vorstellungen</Text>
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.designWishes}</Text>
</View>
)}
{state.references.length > 0 && (
<View style={{ marginTop: 15 }}>
<Text style={styles.configLabel}>Referenzen</Text>
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.references.join('\n')}</Text>
</View>
)}
{state.message && (
<View style={{ marginTop: 15 }}>
<Text style={styles.configLabel}>Nachricht / Anmerkungen</Text>
<Text style={[styles.configValue, { lineHeight: 1.4 }]}>{state.message}</Text>
</View>
)}
</View>
{qrCodeData && (
<View style={styles.qrSection}>
<Image src={qrCodeData} style={styles.qrImage} />
<Text style={styles.qrText}>QR-Code scannen, um Konfiguration online zu öffnen</Text>
</View>
)}
<View style={styles.footer}>
<Text>marc@mintel.me</Text>
<Text>mintel.me</Text>
<Text>Digital Systems & Design</Text>
</View>
<Text style={styles.pageNumber} render={({ pageNumber, totalPages }) => `Seite ${pageNumber} von ${totalPages}`} fixed />
</Page>
</Document>
);
};