form
This commit is contained in:
@@ -11,7 +11,6 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
|
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
@@ -65,30 +64,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Focus states */
|
/* Focus states */
|
||||||
a, button, input, textarea {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus,
|
a:focus,
|
||||||
button:focus,
|
button:focus,
|
||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus {
|
||||||
a:active,
|
|
||||||
button:active,
|
|
||||||
input:active,
|
|
||||||
textarea:active,
|
|
||||||
a:focus-visible,
|
|
||||||
button:focus-visible,
|
|
||||||
input:focus-visible,
|
|
||||||
textarea:focus-visible {
|
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
outline: 0 !important;
|
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: inherit !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button::-moz-focus-inner {
|
/* Remove default tap highlight on mobile */
|
||||||
border: 0 !important;
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +368,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-tag:focus {
|
.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 */
|
/* Marker Title Styles */
|
||||||
|
|||||||
833
package-lock.json
generated
833
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts"
|
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@types/ioredis": "^4.28.10",
|
"@types/ioredis": "^4.28.10",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"shiki": "^1.24.2",
|
"shiki": "^1.24.2",
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import * as React from 'react';
|
|||||||
import { useState, useMemo, useEffect, useRef } from 'react';
|
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion';
|
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 {
|
import {
|
||||||
ConceptWebsite,
|
ConceptWebsite,
|
||||||
ConceptTarget,
|
ConceptTarget,
|
||||||
@@ -17,6 +20,12 @@ import {
|
|||||||
HeroArchitecture
|
HeroArchitecture
|
||||||
} from './Landing/ConceptIllustrations';
|
} 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
|
// Pricing constants from PRICING.md
|
||||||
const PRICING = {
|
const PRICING = {
|
||||||
BASE_WEBSITE: 6000,
|
BASE_WEBSITE: 6000,
|
||||||
@@ -131,6 +140,9 @@ const API_OPTIONS = [
|
|||||||
{ id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' },
|
{ id: 'stripe', label: 'Stripe / Payment', desc: 'Zahlungsabwicklung und Abonnements.' },
|
||||||
{ id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' },
|
{ id: 'newsletter', label: 'Newsletter / Marketing', desc: 'Mailchimp, Brevo, ActiveCampaign etc.' },
|
||||||
{ id: 'ecommerce', label: 'E-Commerce / Shop', desc: 'Shopify, WooCommerce, Shopware Sync.' },
|
{ 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.' },
|
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -228,8 +240,14 @@ export function ContactForm() {
|
|||||||
const [stepIndex, setStepIndex] = useState(0);
|
const [stepIndex, setStepIndex] = useState(0);
|
||||||
const [state, setState] = useState<FormState>(initialState);
|
const [state, setState] = useState<FormState>(initialState);
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const [qrCodeData, setQrCodeData] = useState<string>('');
|
||||||
const formContainerRef = useRef<HTMLDivElement>(null);
|
const formContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// URL Binding
|
// URL Binding
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const step = searchParams.get('step');
|
const step = searchParams.get('step');
|
||||||
@@ -246,8 +264,9 @@ export function ContactForm() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const currentUrl = useMemo(() => {
|
||||||
const params = new URLSearchParams(searchParams);
|
if (!isClient) return '';
|
||||||
|
const params = new URLSearchParams();
|
||||||
params.set('step', stepIndex.toString());
|
params.set('step', stepIndex.toString());
|
||||||
|
|
||||||
const configData = {
|
const configData = {
|
||||||
@@ -266,8 +285,15 @@ export function ContactForm() {
|
|||||||
const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData))));
|
const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData))));
|
||||||
params.set('config', stateString);
|
params.set('config', stateString);
|
||||||
|
|
||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
||||||
}, [state.projectType, state.selectedPages, state.features, state.functions, state.apiSystems, state.cmsSetup, state.languagesCount, state.deadline, state.designVibe, state.colorScheme, stepIndex]);
|
}, [state, stepIndex, isClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUrl) {
|
||||||
|
router.replace(currentUrl, { scroll: false });
|
||||||
|
QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then(setQrCodeData);
|
||||||
|
}
|
||||||
|
}, [currentUrl]);
|
||||||
|
|
||||||
const totalPagesCount = useMemo(() => {
|
const totalPagesCount = useMemo(() => {
|
||||||
return state.selectedPages.length + state.otherPages.length;
|
return state.selectedPages.length + state.otherPages.length;
|
||||||
@@ -341,7 +367,15 @@ export function ContactForm() {
|
|||||||
|
|
||||||
const randomizeColors = () => {
|
const randomizeColors = () => {
|
||||||
const palette = HARMONIOUS_PALETTES[Math.floor(Math.random() * HARMONIOUS_PALETTES.length)];
|
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 = ({
|
const RepeatableList = ({
|
||||||
@@ -383,7 +417,7 @@ export function ContactForm() {
|
|||||||
setInput('');
|
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} />
|
<Plus size={24} />
|
||||||
</button>
|
</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"
|
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>
|
<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} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -436,7 +470,7 @@ export function ContactForm() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onChange}
|
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'
|
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}
|
key={type.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ projectType: type.id as ProjectType })}
|
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'
|
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">
|
<div className="space-y-4">
|
||||||
<p className="text-sm font-bold text-slate-900">Sitemap hochladen (optional)</p>
|
<p className="text-sm font-bold text-slate-900">Sitemap hochladen (optional)</p>
|
||||||
<div
|
<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'
|
state.sitemapFile ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400 bg-white'
|
||||||
}`}
|
}`}
|
||||||
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||||
@@ -520,7 +554,7 @@ export function ContactForm() {
|
|||||||
<div className="flex items-center gap-3 text-slate-900">
|
<div className="flex items-center gap-3 text-slate-900">
|
||||||
<FileText size={24} />
|
<FileText size={24} />
|
||||||
<span className="font-bold text-sm truncate max-w-[150px]">{state.sitemapFile.name}</span>
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -574,7 +608,7 @@ export function ContactForm() {
|
|||||||
key={vibe.id}
|
key={vibe.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ designVibe: vibe.id })}
|
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'
|
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>
|
</div>
|
||||||
<h4 className={`font-bold ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
|
<h4 className={`font-bold ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
|
||||||
</div>
|
</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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -596,15 +630,16 @@ export function ContactForm() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={randomizeColors}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-6 p-8 bg-white border border-slate-100 rounded-[2rem]">
|
<div className="flex flex-wrap gap-6 p-8 bg-white border border-slate-100 rounded-[2rem]">
|
||||||
{state.colorScheme.map((color, i) => (
|
{state.colorScheme.map((color, i) => (
|
||||||
<div key={i} className="flex flex-col items-center gap-3">
|
<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
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={color}
|
value={color}
|
||||||
@@ -622,7 +657,7 @@ export function ContactForm() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ colorScheme: [...state.colorScheme, '#000000'] })}
|
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} />
|
<Plus size={24} />
|
||||||
</button>
|
</button>
|
||||||
@@ -645,7 +680,7 @@ export function ContactForm() {
|
|||||||
value={state.designWishes}
|
value={state.designWishes}
|
||||||
onChange={(e) => updateState({ designWishes: e.target.value })}
|
onChange={(e) => updateState({ designWishes: e.target.value })}
|
||||||
placeholder="Erzählen Sie mir mehr über Ihre Vorstellungen..."
|
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}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -688,13 +723,27 @@ export function ContactForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm font-bold text-slate-900">Weitere Funktionen?</p>
|
<p className="text-sm font-bold text-slate-900">Weitere Funktionen?</p>
|
||||||
<RepeatableList
|
<RepeatableList
|
||||||
items={state.otherFunctions}
|
items={state.otherFunctions}
|
||||||
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
|
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
|
||||||
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
|
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
|
||||||
placeholder="z.B. Login-Bereich, Buchungssystem..."
|
placeholder="z.B. Login-Bereich, Buchungssystem..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
case 'api':
|
case 'api':
|
||||||
@@ -738,7 +787,7 @@ export function ContactForm() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ cmsSetup: !state.cmsSetup })}
|
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' : ''}`} />
|
<div className={`absolute top-1 left-1 w-7 h-7 bg-white rounded-full transition-transform ${state.cmsSetup ? 'translate-x-7' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
@@ -756,12 +805,12 @@ export function ContactForm() {
|
|||||||
key={opt.id}
|
key={opt.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ expectedAdjustments: opt.id })}
|
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'
|
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="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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<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>
|
<p className="text-sm text-slate-500">Wie viele Sprachen soll die Website unterstützen?</p>
|
||||||
<div className="flex items-center gap-8">
|
<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>
|
<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>
|
||||||
</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>
|
<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>
|
||||||
<div className="flex items-center gap-8 mt-2">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -824,12 +873,12 @@ export function ContactForm() {
|
|||||||
key={opt.id}
|
key={opt.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ deadline: opt.id })}
|
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'
|
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>
|
<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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -847,16 +896,16 @@ export function ContactForm() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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="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" />
|
<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>
|
</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" />
|
<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" />
|
<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">
|
<div className="space-y-4">
|
||||||
<p className="text-sm font-bold text-slate-900">Dateien hochladen (optional)</p>
|
<p className="text-sm font-bold text-slate-900">Dateien hochladen (optional)</p>
|
||||||
<div
|
<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'
|
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(); }}
|
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||||
@@ -887,7 +936,7 @@ export function ContactForm() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
|
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} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -914,8 +963,39 @@ export function ContactForm() {
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (stepIndex === activeSteps.length - 1) setIsSubmitted(true);
|
if (stepIndex === activeSteps.length - 1) {
|
||||||
else nextStep();
|
// 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) {
|
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">
|
<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="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>
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -942,18 +1022,19 @@ export function ContactForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{activeSteps.map((step, i) => (
|
{activeSteps.map((step, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => i < stepIndex && setStepIndex(i)}
|
type="button"
|
||||||
title={step.title}
|
onClick={() => {
|
||||||
disabled={i >= stepIndex}
|
setStepIndex(i);
|
||||||
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'}`}
|
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-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">
|
||||||
<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}
|
||||||
{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>
|
</div>
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -961,22 +1042,27 @@ export function ContactForm() {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center py-4 border-y border-slate-50">
|
<div className="flex justify-between items-center py-4 border-y border-slate-50">
|
||||||
{stepIndex > 0 ? (
|
{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
|
<ChevronLeft size={14} /> Zurück
|
||||||
</button>
|
</button>
|
||||||
) : <div />}
|
) : <div />}
|
||||||
{stepIndex < activeSteps.length - 1 && (
|
<div className="flex items-center gap-4">
|
||||||
<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">
|
<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">
|
||||||
Weiter <ChevronRight size={14} />
|
<Share2 size={14} /> Teilen
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="min-h-[450px]">
|
<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>
|
<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">
|
<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 > 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">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 < 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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>)}
|
{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.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.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.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.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>)}
|
{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>
|
||||||
<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 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-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 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>
|
</div>
|
||||||
|
|||||||
520
src/components/EstimationPDF.tsx
Normal file
520
src/components/EstimationPDF.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user