web app form

This commit is contained in:
2026-01-30 11:04:48 +01:00
parent cea56ac58d
commit 316e4b6fe9
19 changed files with 540 additions and 334 deletions

View File

@@ -24,6 +24,7 @@ import { ContentStep } from './ContactForm/steps/ContentStep';
import { LanguageStep } from './ContactForm/steps/LanguageStep'; import { LanguageStep } from './ContactForm/steps/LanguageStep';
import { TimelineStep } from './ContactForm/steps/TimelineStep'; import { TimelineStep } from './ContactForm/steps/TimelineStep';
import { ContactStep } from './ContactForm/steps/ContactStep'; import { ContactStep } from './ContactForm/steps/ContactStep';
import { WebAppStep } from './ContactForm/steps/WebAppStep';
import { import {
ConceptTarget, ConceptTarget,
@@ -84,7 +85,11 @@ export function ContactForm() {
languagesCount: state.languagesCount, languagesCount: state.languagesCount,
deadline: state.deadline, deadline: state.deadline,
designVibe: state.designVibe, designVibe: state.designVibe,
colorScheme: state.colorScheme colorScheme: state.colorScheme,
targetAudience: state.targetAudience,
userRoles: state.userRoles,
dataSensitivity: state.dataSensitivity,
platformType: state.platformType
}; };
const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData)))); const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData))));
@@ -182,11 +187,22 @@ export function ContactForm() {
{ id: 'language', title: 'Sprachen', description: 'Globale Reichweite planen.', illustration: <ConceptCommunication className="w-full h-full" /> }, { id: 'language', title: 'Sprachen', description: 'Globale Reichweite planen.', illustration: <ConceptCommunication className="w-full h-full" /> },
{ id: 'timeline', title: 'Zeitplan', description: 'Wann soll das Projekt live gehen?', illustration: <HeroArchitecture className="w-full h-full" /> }, { id: 'timeline', title: 'Zeitplan', description: 'Wann soll das Projekt live gehen?', illustration: <HeroArchitecture className="w-full h-full" /> },
{ id: 'contact', title: 'Abschluss', description: 'Erzählen Sie mir mehr über Ihr Vorhaben.', illustration: <ConceptCommunication className="w-full h-full" /> }, { id: 'contact', title: 'Abschluss', description: 'Erzählen Sie mir mehr über Ihr Vorhaben.', illustration: <ConceptCommunication className="w-full h-full" /> },
{ id: 'webapp', title: 'Web App Details', description: 'Spezifische Anforderungen für Ihre Anwendung.', illustration: <ConceptSystem className="w-full h-full" /> },
]; ];
const activeSteps = useMemo(() => { const activeSteps = useMemo(() => {
if (state.projectType === 'website') return steps; if (state.projectType === 'website') {
return [steps[0], steps[9], steps[10]]; return steps.filter(s => s.id !== 'webapp');
}
// Web App flow
return [
steps.find(s => s.id === 'type')!,
steps.find(s => s.id === 'webapp')!,
{ ...steps.find(s => s.id === 'functions')!, title: 'Funktionen', description: 'Kern-Features Ihrer Anwendung.' },
{ ...steps.find(s => s.id === 'api')!, title: 'Integrationen', description: 'Anbindung an bestehende Systeme.' },
steps.find(s => s.id === 'timeline')!,
steps.find(s => s.id === 'contact')!,
];
}, [state.projectType]); }, [state.projectType]);
useEffect(() => { useEffect(() => {
@@ -218,6 +234,8 @@ export function ContactForm() {
return <TimelineStep state={state} updateState={updateState} />; return <TimelineStep state={state} updateState={updateState} />;
case 'contact': case 'contact':
return <ContactStep state={state} updateState={updateState} />; return <ContactStep state={state} updateState={updateState} />;
case 'webapp':
return <WebAppStep state={state} updateState={updateState} />;
default: return null; default: return null;
} }
}; };
@@ -264,7 +282,7 @@ export function ContactForm() {
<div className="flex flex-col md:flex-row md:items-center gap-8"> <div className="flex flex-col md:flex-row md:items-center gap-8">
<div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4 flex items-center justify-center">{activeSteps[stepIndex].illustration}</div> <div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4 flex items-center justify-center">{activeSteps[stepIndex].illustration}</div>
<div className="space-y-2"> <div className="space-y-2">
<span className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400">Schritt {stepIndex + 1} von {activeSteps.length}</span> <span className="text-base font-bold uppercase tracking-[0.2em] text-slate-400">Schritt {stepIndex + 1} von {activeSteps.length}</span>
<h3 className="text-4xl font-bold tracking-tight text-slate-900">{activeSteps[stepIndex].title}</h3> <h3 className="text-4xl font-bold tracking-tight text-slate-900">{activeSteps[stepIndex].title}</h3>
<p className="text-xl text-slate-500 leading-relaxed">{activeSteps[stepIndex].description}</p> <p className="text-xl text-slate-500 leading-relaxed">{activeSteps[stepIndex].description}</p>
</div> </div>
@@ -291,7 +309,7 @@ export function ContactForm() {
initial={{ opacity: 0, y: 5, x: '-50%' }} initial={{ opacity: 0, y: 5, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }} animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: 5, x: '-50%' }} exit={{ opacity: 0, y: 5, x: '-50%' }}
className="absolute bottom-full left-1/2 mb-1 px-3 py-1.5 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded-lg whitespace-nowrap pointer-events-none z-50 shadow-xl" className="absolute bottom-full left-1/2 mb-1 px-4 py-2 bg-slate-900 text-white text-sm font-bold uppercase tracking-wider rounded-lg whitespace-nowrap pointer-events-none z-50 shadow-xl"
> >
{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 className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-slate-900" />
@@ -303,16 +321,16 @@ export function ContactForm() {
</div> </div>
</div> </div>
<div className="flex justify-between items-center py-4 border-y border-slate-50"> <div className="flex justify-between items-center py-6 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 rounded-full"> <button type="button" onClick={prevStep} className="text-base 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={18} /> Zurück
</button> </button>
) : <div />} ) : <div />}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{stepIndex < activeSteps.length - 1 && ( {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"> <button type="button" onClick={nextStep} className="text-base 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} /> Weiter <ChevronRight size={18} />
</button> </button>
)} )}
</div> </div>
@@ -321,8 +339,8 @@ export function ContactForm() {
<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 rounded-full"><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-xl 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>)} {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-xl 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-xl 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>

View File

@@ -15,7 +15,7 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) {
<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 rounded-[2rem] ${ className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex items-start gap-4 focus:outline-none overflow-hidden relative ${
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'
}`} }`}
> >
@@ -23,8 +23,8 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) {
{checked && <Check size={14} strokeWidth={4} />} {checked && <Check size={14} strokeWidth={4} />}
</div> </div>
<div> <div>
<h4 className={`font-bold mb-1 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4> <h4 className={`text-xl font-bold mb-1 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4>
<p className={`text-sm leading-relaxed ${checked ? 'text-slate-100' : 'text-slate-500'}`}>{desc}</p> <p className={`text-base leading-relaxed ${checked ? 'text-slate-200' : 'text-slate-500'}`}>{desc}</p>
</div> </div>
</button> </button>
); );

View File

@@ -90,7 +90,7 @@ export function PriceCalculation({
<ConceptAutomation className="w-12 h-12" /> <ConceptAutomation className="w-12 h-12" />
</div> </div>
<div className="space-y-2"> <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-slate-600 text-sm leading-relaxed">Web 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> <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>

View File

@@ -20,8 +20,8 @@ export function RepeatableList({
}: RepeatableListProps) { }: RepeatableListProps) {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
return ( return (
<div className="space-y-4"> <div className="space-y-6">
<div className="flex gap-2"> <div className="flex gap-3">
<input <input
type="text" type="text"
value={input} value={input}
@@ -36,7 +36,7 @@ export function RepeatableList({
} }
}} }}
placeholder={placeholder} placeholder={placeholder}
className="flex-1 p-6 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors text-sm" className="flex-1 p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors text-lg"
/> />
<button <button
type="button" type="button"
@@ -46,12 +46,12 @@ export function RepeatableList({
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 rounded-[2rem]" className="w-20 h-20 rounded-full bg-slate-900 text-white flex items-center justify-center hover:bg-slate-800 transition-colors shrink-0 focus:outline-none"
> >
<Plus size={24} /> <Plus size={32} />
</button> </button>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-3">
<AnimatePresence> <AnimatePresence>
{items.map((item, i) => ( {items.map((item, i) => (
<motion.div <motion.div
@@ -59,11 +59,11 @@ export function RepeatableList({
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }} exit={{ opacity: 0, scale: 0.8 }}
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-3 px-6 py-3 bg-slate-100 rounded-full text-base font-bold text-slate-700"
> >
<span className="truncate max-w-[200px]">{item}</span> <span className="truncate max-w-[300px]">{item}</span>
<button type="button" onClick={() => onRemove(i)} className="text-slate-400 hover:text-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full"> <button type="button" onClick={() => onRemove(i)} className="text-slate-400 hover:text-slate-900 transition-colors focus:outline-none">
<X size={14} /> <X size={18} />
</button> </button>
</motion.div> </motion.div>
))} ))}

View File

@@ -45,6 +45,10 @@ export const initialState: FormState = {
expectedAdjustments: 'low', expectedAdjustments: 'low',
languagesCount: 1, languagesCount: 1,
deadline: 'flexible', deadline: 'flexible',
targetAudience: 'internal',
userRoles: [],
dataSensitivity: 'standard',
platformType: 'web-only',
}; };
export const PAGE_SAMPLES = [ export const PAGE_SAMPLES = [

View File

@@ -2,10 +2,8 @@
import * as React from 'react'; import * as React from 'react';
import { FormState } from '../types'; import { FormState } from '../types';
import { API_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox'; import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList'; import { RepeatableList } from '../components/RepeatableList';
import { Info } from 'lucide-react';
interface ApiStepProps { interface ApiStepProps {
state: FormState; state: FormState;
@@ -14,33 +12,56 @@ interface ApiStepProps {
} }
export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) { export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
const isWebApp = state.projectType === 'web-app';
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start"> <div className="space-y-6">
<Info className="text-slate-400 shrink-0 mt-1" size={24} /> <h4 className="text-2xl font-bold text-slate-900">
<div className="text-sm text-slate-600 space-y-2 leading-relaxed"> {isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
<p className="font-bold text-slate-900">Wichtig zu wissen</p> </h4>
<p>Ich biete diese Drittsysteme nicht selbst an, sondern entwickle die <strong>Schnittstelle (API)</strong>, damit Ihre Website nahtlos mit ihnen kommunizieren kann.</p> <p className="text-lg text-slate-500 leading-relaxed">
{isWebApp
? 'Mit welchen Systemen soll die Web App kommunizieren?'
: 'Datenaustausch mit Drittsystemen zur Automatisierung.'}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Checkbox
label="CRM / ERP" desc="HubSpot, Salesforce, SAP, Xentral etc."
checked={state.apiSystems.includes('crm_erp')}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, 'crm_erp') })}
/>
<Checkbox
label="Payment" desc="Stripe, PayPal, Klarna Integration."
checked={state.apiSystems.includes('payment')}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, 'payment') })}
/>
<Checkbox
label="Marketing" desc="Newsletter (Mailchimp), Social Media Sync."
checked={state.apiSystems.includes('marketing')}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, 'marketing') })}
/>
<Checkbox
label="E-Commerce" desc="Shopify, WooCommerce, Lagerbestand-Sync."
checked={state.apiSystems.includes('ecommerce')}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, 'ecommerce') })}
/>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{API_OPTIONS.map(opt => ( <div className="space-y-6">
<Checkbox <p className="text-lg font-bold text-slate-900">Weitere Systeme oder eigene APIs?</p>
key={opt.id} label={opt.label} desc={opt.desc}
checked={state.apiSystems.includes(opt.id)}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, opt.id) })}
/>
))}
</div>
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Weitere Systeme?</p>
<RepeatableList <RepeatableList
items={state.otherTech} items={state.otherTech}
onAdd={(v) => updateState({ otherTech: [...state.otherTech, v] })} onAdd={(v) => updateState({ otherTech: [...state.otherTech, v] })}
onRemove={(i) => updateState({ otherTech: state.otherTech.filter((_, idx) => idx !== i) })} onRemove={(i) => updateTech(i)}
placeholder="z.B. Personio, DATEV, Salesforce..." placeholder="z.B. Microsoft Graph, Google Maps, Custom REST API..."
/> />
</div> </div>
</div> </div>
); );
function updateTech(index: number) {
updateState({ otherTech: state.otherTech.filter((_, idx) => idx !== index) });
}
} }

View File

@@ -14,8 +14,8 @@ interface AssetsStepProps {
export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) { export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{ASSET_OPTIONS.map(opt => ( {ASSET_OPTIONS.map(opt => (
<Checkbox <Checkbox
key={opt.id} label={opt.label} desc={opt.desc} key={opt.id} label={opt.label} desc={opt.desc}
@@ -24,13 +24,13 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
/> />
))} ))}
</div> </div>
<div className="space-y-4"> <div className="space-y-6">
<p className="text-sm font-bold text-slate-900">Weitere Materialien?</p> <p className="text-lg font-bold text-slate-900">Weitere vorhandene Unterlagen?</p>
<RepeatableList <RepeatableList
items={state.otherAssets} items={state.otherAssets}
onAdd={(v) => updateState({ otherAssets: [...state.otherAssets, v] })} onAdd={(v) => updateState({ otherAssets: [...state.otherAssets, v] })}
onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })} onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })}
placeholder="z.B. Stock-Fotos, Video-Footage, Präsentationen..." placeholder="z.B. Lastenheft, Wireframes, Bilddatenbank..."
/> />
</div> </div>
</div> </div>

View File

@@ -2,10 +2,8 @@
import * as React from 'react'; import * as React from 'react';
import { FormState } from '../types'; import { FormState } from '../types';
import { PAGE_SAMPLES } from '../constants';
import { Checkbox } from '../components/Checkbox'; import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList'; import { RepeatableList } from '../components/RepeatableList';
import { Info, FileText, Upload, X } from 'lucide-react';
interface BaseStepProps { interface BaseStepProps {
state: FormState; state: FormState;
@@ -15,66 +13,31 @@ interface BaseStepProps {
export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) { export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{PAGE_SAMPLES.map(p => ( {[
{ id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' },
{ id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' },
{ id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' },
{ id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' },
{ id: 'Landing', label: 'Landingpage', desc: 'Optimiert für Marketing-Kampagnen.' },
{ id: 'Legal', label: 'Rechtliches', desc: 'Impressum & Datenschutz.' },
].map(opt => (
<Checkbox <Checkbox
key={p.id} label={p.label} desc={p.desc} key={opt.id} label={opt.label} desc={opt.desc}
checked={state.selectedPages.includes(p.id)} checked={state.selectedPages.includes(opt.id)}
onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, p.id) })} onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })}
/> />
))} ))}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="space-y-6">
<div className="space-y-4"> <p className="text-lg font-bold text-slate-900">Weitere individuelle Seiten?</p>
<p className="text-sm font-bold text-slate-900">Weitere Seiten?</p> <RepeatableList
<RepeatableList items={state.otherPages}
items={state.otherPages} onAdd={(v) => updateState({ otherPages: [...state.otherPages, v] })}
onAdd={(v) => updateState({ otherPages: [...state.otherPages, v] })} onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })}
onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })} placeholder="z.B. Karriere, FAQ, Team-Detail..."
placeholder="z.B. Team-Detail, FAQ..." />
/>
</div>
<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] 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(); }}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files?.[0];
if (file) updateState({ sitemapFile: file });
}}
onClick={() => document.getElementById('sitemap-upload')?.click()}
>
<input id="sitemap-upload" type="file" className="hidden" onChange={(e) => {
const file = e.target.files?.[0];
if (file) updateState({ sitemapFile: file });
}} />
{state.sitemapFile ? (
<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 rounded-full"><X size={16} /></button>
</div>
) : (
<>
<Upload size={24} className="text-slate-400 group-hover:text-slate-900 transition-colors" />
<p className="text-xs text-slate-500 text-center">Sitemap hierher ziehen oder klicken</p>
</>
)}
</div>
</div>
</div>
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">
<Info className="text-slate-400 shrink-0 mt-1" size={24} />
<div className="text-sm text-slate-600 space-y-2 leading-relaxed">
<p className="font-bold text-slate-900">Was zählt als Seite?</p>
<p>Eine Seite ist ein eigenständiges Layout. Wenn Sie 10 Leistungen haben, die alle das gleiche Layout nutzen, zählt das als 1 Seite plus ein "System-Modul" für die Verwaltung der Leistungen.</p>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -11,18 +11,44 @@ interface ContactStepProps {
export function ContactStep({ state, updateState }: ContactStepProps) { export function ContactStep({ state, updateState }: ContactStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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
<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]" /> 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 text-lg"
/>
<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 text-lg"
/>
</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 rounded-[2rem]" /> <input
<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]" /> 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 text-lg"
/>
<textarea
placeholder="Erzählen Sie mir kurz von Ihrem Projekt..."
value={state.message}
onChange={(e) => updateState({ message: e.target.value })}
rows={5}
className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors resize-none text-lg"
/>
<div className="space-y-4"> <div className="space-y-6">
<p className="text-sm font-bold text-slate-900">Dateien hochladen (optional)</p> <p className="text-lg 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] rounded-[2rem] ${ className={`relative group border-2 border-dashed rounded-[3rem] p-10 transition-all duration-300 flex flex-col items-center justify-center gap-6 cursor-pointer min-h-[200px] ${
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(); }}
@@ -40,12 +66,12 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
}} /> }} />
{state.contactFiles.length > 0 ? ( {state.contactFiles.length > 0 ? (
<div className="w-full space-y-2"> <div className="w-full space-y-3">
{state.contactFiles.map((file, idx) => ( {state.contactFiles.map((file, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-white border border-slate-100 rounded-xl shadow-sm"> <div key={idx} className="flex items-center justify-between p-4 bg-white border border-slate-100 rounded-2xl shadow-sm">
<div className="flex items-center gap-3 text-slate-900"> <div className="flex items-center gap-4 text-slate-900">
<FileText size={20} className="text-slate-400" /> <FileText size={24} className="text-slate-400" />
<span className="font-bold text-sm truncate max-w-[200px]">{file.name}</span> <span className="font-bold text-base truncate max-w-[250px]">{file.name}</span>
</div> </div>
<button <button
type="button" type="button"
@@ -53,20 +79,20 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
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 rounded-full" className="p-2 hover:bg-slate-100 rounded-full transition-colors focus:outline-none"
> >
<X size={16} /> <X size={20} />
</button> </button>
</div> </div>
))} ))}
<p className="text-[10px] text-slate-400 text-center mt-4">Klicken oder ziehen, um weitere Dateien hinzuzufügen</p> <p className="text-xs text-slate-400 text-center mt-6">Klicken oder ziehen, um weitere Dateien hinzuzufügen</p>
</div> </div>
) : ( ) : (
<> <>
<Upload size={32} className="text-slate-400 group-hover:text-slate-900 transition-colors" /> <Upload size={48} className="text-slate-400 group-hover:text-slate-900 transition-colors" />
<div className="text-center"> <div className="text-center">
<p className="text-sm font-bold text-slate-900">Dateien hierher ziehen</p> <p className="text-lg font-bold text-slate-900">Dateien hierher ziehen</p>
<p className="text-xs text-slate-500 mt-1">oder klicken zum Auswählen</p> <p className="text-base text-slate-500 mt-1">oder klicken zum Auswählen</p>
</div> </div>
</> </>
)} )}

View File

@@ -11,24 +11,24 @@ interface ContentStepProps {
export function ContentStep({ state, updateState }: ContentStepProps) { export function ContentStep({ state, updateState }: ContentStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="flex items-center justify-between p-8 bg-white border border-slate-100 rounded-[2rem]"> <div className="flex items-center justify-between p-10 bg-white border border-slate-100 rounded-[3rem]">
<div className="max-w-[70%]"> <div className="max-w-[70%]">
<h4 className="text-xl font-bold text-slate-900">Inhalte selbst verwalten (CMS)</h4> <h4 className="text-2xl font-bold text-slate-900">Inhalte selbst verwalten (CMS)</h4>
<p className="text-sm text-slate-500 mt-1">Möchten Sie Datensätze (z.B. Blogartikel, Produkte) selbst über eine einfache Oberfläche pflegen?</p> <p className="text-lg text-slate-500 mt-2">Möchten Sie Datensätze (z.B. Blogartikel, Produkte) selbst über eine einfache Oberfläche pflegen?</p>
</div> </div>
<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 ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`} className={`w-20 h-11 rounded-full transition-colors relative focus:outline-none ${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.5 left-1.5 w-8 h-8 bg-white rounded-full transition-transform ${state.cmsSetup ? 'translate-x-9' : ''}`} />
</button> </button>
</div> </div>
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 space-y-4"> <div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-6">
<p className="text-sm font-bold text-slate-900">Wie oft ändern sich Ihre Inhalte?</p> <p className="text-lg font-bold text-slate-900">Wie oft ändern sich Ihre Inhalte?</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[ {[
{ id: 'low', label: 'Selten', desc: 'Wenige Male im Jahr.' }, { id: 'low', label: 'Selten', desc: 'Wenige Male im Jahr.' },
{ id: 'medium', label: 'Regelmäßig', desc: 'Monatliche Updates.' }, { id: 'medium', label: 'Regelmäßig', desc: 'Monatliche Updates.' },
@@ -38,44 +38,44 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
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 ${ className={`p-6 rounded-2xl border-2 text-left transition-all focus:outline-none ${
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-lg">{opt.label}</p>
<p className={`text-[10px] ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p> <p className={`text-sm mt-1 ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button> </button>
))} ))}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8">
<div className="p-6 bg-white rounded-2xl border border-slate-100 space-y-2"> <div className="p-8 bg-white rounded-[2rem] border border-slate-100 space-y-3">
<div className="flex items-center gap-2 text-slate-900 font-bold text-xs uppercase tracking-wider"> <div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-wider">
<Zap size={14} /> Vorteil CMS <Zap size={18} /> Vorteil CMS
</div> </div>
<p className="text-[11px] text-slate-500 leading-relaxed"> <p className="text-sm text-slate-500 leading-relaxed">
Volle Kontrolle über Ihre Inhalte und keine laufenden Kosten für kleine Textänderungen oder neue Blog-Beiträge. Volle Kontrolle über Ihre Inhalte und keine laufenden Kosten für kleine Textänderungen oder neue Blog-Beiträge.
</p> </p>
</div> </div>
<div className="p-6 bg-white rounded-2xl border border-slate-100 space-y-2"> <div className="p-8 bg-white rounded-[2rem] border border-slate-100 space-y-3">
<div className="flex items-center gap-2 text-slate-900 font-bold text-xs uppercase tracking-wider"> <div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-wider">
<AlertCircle size={14} /> Fokus Design <AlertCircle size={18} /> Fokus Design
</div> </div>
<p className="text-[11px] text-slate-500 leading-relaxed"> <p className="text-sm text-slate-500 leading-relaxed">
Ohne CMS bleibt die technische Komplexität geringer und das Design ist maximal geschützt vor ungewollten Änderungen. Ohne CMS bleibt die technische Komplexität geringer und das Design ist maximal geschützt vor ungewollten Änderungen.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-col gap-4 p-8 bg-white border border-slate-100 rounded-[2rem]"> <div className="flex flex-col gap-6 p-10 bg-white border border-slate-100 rounded-[3rem]">
<div> <div>
<h4 className="text-xl font-bold text-slate-900">Inhalte einpflegen</h4> <h4 className="text-2xl font-bold text-slate-900">Inhalte einpflegen</h4>
<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-lg 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-12 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"><Minus size={20} /></button> <button type="button" onClick={() => updateState({ newDatasets: Math.max(0, state.newDatasets - 1) })} className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"><Minus size={28} /></button>
<span className="text-3xl font-bold w-12 text-center">{state.newDatasets}</span> <span className="text-5xl font-bold w-16 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"><Plus size={20} /></button> <button type="button" onClick={() => updateState({ newDatasets: state.newDatasets + 1 })} className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"><Plus size={28} /></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import { FormState } from '../types'; import { FormState } from '../types';
import { DESIGN_VIBES, HARMONIOUS_PALETTES } from '../constants'; import { DESIGN_VIBES, HARMONIOUS_PALETTES } from '../constants';
import { RepeatableList } from '../components/RepeatableList'; import { motion } from 'framer-motion';
import { Palette, RefreshCw, Plus } from 'lucide-react';
interface DesignStepProps { interface DesignStepProps {
state: FormState; state: FormState;
@@ -12,103 +11,58 @@ interface DesignStepProps {
} }
export function DesignStep({ state, updateState }: DesignStepProps) { export function DesignStep({ state, updateState }: DesignStepProps) {
const randomizeColors = () => {
const palette = HARMONIOUS_PALETTES[Math.floor(Math.random() * HARMONIOUS_PALETTES.length)];
// 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 });
};
return ( return (
<div className="space-y-12"> <div className="space-y-12">
<div className="space-y-6"> <div className="space-y-6">
<p className="text-sm font-bold text-slate-900 flex items-center gap-2"><Palette size={18} /> Design-Vibe wählen</p> <h4 className="text-2xl font-bold text-slate-900">Design-Richtung</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{DESIGN_VIBES.map(vibe => ( {DESIGN_VIBES.map((vibe) => (
<button <button
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 rounded-[2rem] group ${ className={`p-8 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none overflow-hidden relative ${
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'
}`} }`}
> >
<div className="flex justify-between items-start mb-4"> <div className={`w-16 h-10 mb-4 ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.illustration}</div>
<div className="w-12 h-12 rounded-xl bg-slate-50 flex items-center justify-center text-slate-900 group-hover:bg-white transition-colors"> <h4 className={`text-xl font-bold mb-2 ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
{vibe.illustration} <p className={`text-base leading-relaxed ${state.designVibe === vibe.id ? 'text-slate-200' : 'text-slate-500'}`}>{vibe.desc}</p>
</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-white opacity-90' : 'text-slate-500'}`}>{vibe.desc}</p>
</button> </button>
))} ))}
</div> </div>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> <h4 className="text-2xl font-bold text-slate-900">Farbschema</h4>
<p className="text-sm font-bold text-slate-900">Farbschema</p> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<button {HARMONIOUS_PALETTES.map((palette, i) => (
type="button" <button
onClick={randomizeColors} key={i}
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" type="button"
> onClick={() => updateState({ colorScheme: palette })}
<RefreshCw size={20} className="group-hover:rotate-180 transition-transform duration-500" /> className={`p-4 rounded-2xl border-2 transition-all ${
<span>Farbschema würfeln</span> JSON.stringify(state.colorScheme) === JSON.stringify(palette) ? 'border-slate-900 bg-slate-50' : 'border-slate-100 bg-white hover:border-slate-200'
</button> }`}
</div> >
<div className="flex flex-wrap gap-6 p-8 bg-white border border-slate-100 rounded-[2rem]"> <div className="flex h-12 w-full rounded-lg overflow-hidden">
{state.colorScheme.map((color, i) => ( {palette.map((color, j) => (
<div key={i} className="flex flex-col items-center gap-3"> <div key={j} className="flex-1" style={{ backgroundColor: color }} />
<div className="relative w-16 h-16 rounded-2xl border border-slate-200 shadow-sm overflow-hidden rounded-2xl"> ))}
<input
type="color"
value={color}
onChange={(e) => {
const newColors = [...state.colorScheme];
newColors[i] = e.target.value;
updateState({ colorScheme: newColors });
}}
className="absolute inset-0 w-[200%] h-[200%] -translate-x-1/4 -translate-y-1/4 cursor-pointer"
/>
</div> </div>
<span className="text-[10px] font-mono text-slate-400 uppercase">{color}</span> </button>
</div>
))} ))}
<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 rounded-2xl"
>
<Plus size={24} />
</button>
</div> </div>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<p className="text-sm font-bold text-slate-900">Referenz-Webseiten</p> <h4 className="text-2xl font-bold text-slate-900">Individuelle Wünsche</h4>
<RepeatableList
items={state.references}
onAdd={(v) => updateState({ references: [...state.references, v] })}
onRemove={(i) => updateState({ references: state.references.filter((_, idx) => idx !== i) })}
placeholder="https://..."
/>
</div>
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Besondere Wünsche</p>
<textarea <textarea
placeholder="Haben Sie bereits konkrete Vorstellungen oder Referenzen?"
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..." rows={4}
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]" className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors resize-none text-lg"
rows={3}
/> />
</div> </div>
</div> </div>

View File

@@ -2,9 +2,9 @@
import * as React from 'react'; import * as React from 'react';
import { FormState } from '../types'; import { FormState } from '../types';
import { FEATURE_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox'; import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList'; import { RepeatableList } from '../components/RepeatableList';
import { FEATURE_OPTIONS } from '../constants';
interface FeaturesStepProps { interface FeaturesStepProps {
state: FormState; state: FormState;
@@ -14,8 +14,8 @@ interface FeaturesStepProps {
export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepProps) { export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{FEATURE_OPTIONS.map(opt => ( {FEATURE_OPTIONS.map(opt => (
<Checkbox <Checkbox
key={opt.id} label={opt.label} desc={opt.desc} key={opt.id} label={opt.label} desc={opt.desc}
@@ -24,13 +24,13 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
/> />
))} ))}
</div> </div>
<div className="space-y-4"> <div className="space-y-6">
<p className="text-sm font-bold text-slate-900">Weitere inhaltliche Bereiche?</p> <p className="text-lg font-bold text-slate-900">Weitere inhaltliche Module?</p>
<RepeatableList <RepeatableList
items={state.otherFeatures} items={state.otherFeatures}
onAdd={(v) => updateState({ otherFeatures: [...state.otherFeatures, v] })} onAdd={(v) => updateState({ otherFeatures: [...state.otherFeatures, v] })}
onRemove={(i) => updateState({ otherFeatures: state.otherFeatures.filter((_, idx) => idx !== i) })} onRemove={(i) => updateState({ otherFeatures: state.otherFeatures.filter((_, idx) => idx !== i) })}
placeholder="z.B. Partner-Portal, Download-Center..." placeholder="z.B. Glossar, Download-Center, Partner-Bereich..."
/> />
</div> </div>
</div> </div>

View File

@@ -2,7 +2,6 @@
import * as React from 'react'; import * as React from 'react';
import { FormState } from '../types'; import { FormState } from '../types';
import { FUNCTION_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox'; import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList'; import { RepeatableList } from '../components/RepeatableList';
import { Minus, Plus } from 'lucide-react'; import { Minus, Plus } from 'lucide-react';
@@ -14,40 +13,90 @@ interface FunctionsStepProps {
} }
export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepProps) { export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepProps) {
const isWebApp = state.projectType === 'web-app';
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-6">
{FUNCTION_OPTIONS.map(opt => ( <h4 className="text-2xl font-bold text-slate-900">
<Checkbox {isWebApp ? 'Funktionale Anforderungen' : 'Erweiterte Funktionen'}
key={opt.id} label={opt.label} desc={opt.desc} </h4>
checked={state.functions.includes(opt.id)} <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
onChange={() => updateState({ functions: toggleItem(state.functions, opt.id) })} {isWebApp ? (
/> <>
))} <Checkbox
label="Dashboard & Analytics" desc="Visualisierung von Daten und Kennzahlen."
checked={state.functions.includes('dashboard')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'dashboard') })}
/>
<Checkbox
label="Dateiverwaltung" desc="Upload, Download und Organisation von Dokumenten."
checked={state.functions.includes('files')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'files') })}
/>
<Checkbox
label="Benachrichtigungen" desc="E-Mail, Push oder In-App Alerts."
checked={state.functions.includes('notifications')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'notifications') })}
/>
<Checkbox
label="Export-Funktionen" desc="CSV, Excel oder PDF Generierung."
checked={state.functions.includes('export')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'export') })}
/>
</>
) : (
<>
<Checkbox
label="Suche" desc="Volltextsuche über alle Inhalte."
checked={state.functions.includes('search')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'search') })}
/>
<Checkbox
label="Filter-Systeme" desc="Kategorisierung und Sortierung."
checked={state.functions.includes('filter')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'filter') })}
/>
<Checkbox
label="PDF-Export" desc="Automatisierte PDF-Erstellung."
checked={state.functions.includes('pdf')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'pdf') })}
/>
<Checkbox
label="Erweiterte Formulare" desc="Komplexe Abfragen & Logik."
checked={state.functions.includes('forms')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'forms') })}
/>
</>
)}
</div>
</div> </div>
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Weitere Funktionen?</p> <div className="space-y-6">
<p className="text-lg font-bold text-slate-900">Weitere spezifische Wünsche?</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={isWebApp ? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..." : "z.B. Login-Bereich, Buchungssystem..."}
/> />
</div> </div>
<div className="p-8 bg-white border border-slate-100 rounded-[2rem] space-y-4"> {!isWebApp && (
<div className="flex justify-between items-start"> <div className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6">
<div> <div className="flex justify-between items-start">
<h4 className="text-xl font-bold text-slate-900">Besondere Interaktionen</h4> <div>
<p className="text-sm text-slate-500 mt-1">Aufwendige Animationen oder komplexe UI-Logik pro Abschnitt.</p> <h4 className="text-2xl font-bold text-slate-900">Besondere Interaktionen</h4>
</div> <p className="text-lg text-slate-500 mt-2">Aufwendige Animationen oder komplexe UI-Logik pro Abschnitt.</p>
<div className="flex items-center gap-6"> </div>
<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> <div className="flex items-center gap-8">
<span className="text-2xl font-bold w-8 text-center">{state.complexInteractions}</span> <button type="button" onClick={() => updateState({ complexInteractions: Math.max(0, state.complexInteractions - 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"><Minus size={24} /></button>
<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> <span className="text-4xl font-bold w-12 text-center">{state.complexInteractions}</span>
<button type="button" onClick={() => updateState({ complexInteractions: state.complexInteractions + 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"><Plus size={24} /></button>
</div>
</div> </div>
</div> </div>
</div> )}
</div> </div>
); );
} }

View File

@@ -14,38 +14,38 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
const basePriceExplanation = "Jede zusätzliche Sprache erhöht den Gesamtaufwand für Design, Entwicklung und Qualitätssicherung um ca. 20%. Dies deckt die technische Implementierung der Übersetzungsschicht sowie die Anpassung von Layouts für unterschiedliche Textlängen ab."; const basePriceExplanation = "Jede zusätzliche Sprache erhöht den Gesamtaufwand für Design, Entwicklung und Qualitätssicherung um ca. 20%. Dies deckt die technische Implementierung der Übersetzungsschicht sowie die Anpassung von Layouts für unterschiedliche Textlängen ab.";
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="p-8 bg-white border border-slate-100 rounded-[2rem] space-y-6"> <div className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-8">
<div className="flex items-center gap-4"> <div className="flex items-center gap-6">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-900"> <div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-900">
<Globe size={24} /> <Globe size={32} />
</div> </div>
<div> <div>
<h4 className="text-xl font-bold text-slate-900">Mehrsprachigkeit</h4> <h4 className="text-2xl font-bold text-slate-900">Mehrsprachigkeit</h4>
<p className="text-sm text-slate-500">In wie vielen Sprachen soll Ihre Website verfügbar sein?</p> <p className="text-lg text-slate-500">In wie vielen Sprachen soll Ihre Website verfügbar sein?</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-8 py-4"> <div className="flex items-center gap-12 py-6">
<button <button
type="button" type="button"
onClick={() => updateState({ languagesCount: Math.max(1, state.languagesCount - 1) })} onClick={() => updateState({ languagesCount: Math.max(1, state.languagesCount - 1) })}
className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none" className="w-20 h-20 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"
> >
<Minus size={24} /> <Minus size={32} />
</button> </button>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="text-5xl font-bold text-slate-900">{state.languagesCount}</span> <span className="text-7xl font-bold text-slate-900">{state.languagesCount}</span>
<span className="text-xs font-bold uppercase tracking-widest text-slate-400 mt-2"> <span className="text-sm font-bold uppercase tracking-widest text-slate-400 mt-3">
{state.languagesCount === 1 ? 'Sprache' : 'Sprachen'} {state.languagesCount === 1 ? 'Sprache' : 'Sprachen'}
</span> </span>
</div> </div>
<button <button
type="button" type="button"
onClick={() => updateState({ languagesCount: state.languagesCount + 1 })} onClick={() => updateState({ languagesCount: state.languagesCount + 1 })}
className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none" className="w-20 h-20 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"
> >
<Plus size={24} /> <Plus size={32} />
</button> </button>
</div> </div>
</div> </div>
@@ -53,35 +53,35 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="p-8 bg-slate-900 text-white rounded-[2rem] space-y-4" className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-6"
> >
<div className="flex items-center gap-3 text-slate-400"> <div className="flex items-center gap-4 text-slate-400">
<Info size={18} /> <Info size={24} />
<span className="text-xs font-bold uppercase tracking-widest">Warum dieser Faktor?</span> <span className="text-sm font-bold uppercase tracking-widest">Warum dieser Faktor?</span>
</div> </div>
<p className="text-sm leading-relaxed text-slate-300"> <p className="text-lg leading-relaxed text-slate-300">
{basePriceExplanation} {basePriceExplanation}
</p> </p>
{state.languagesCount > 1 && ( {state.languagesCount > 1 && (
<div className="pt-4 border-t border-white/10"> <div className="pt-6 border-t border-white/10">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium">Aktueller Aufschlagsfaktor:</span> <span className="text-lg font-medium">Aktueller Aufschlagsfaktor:</span>
<span className="text-xl font-bold text-white">+{((state.languagesCount - 1) * 20)}%</span> <span className="text-3xl font-bold text-white">+{((state.languagesCount - 1) * 20)}%</span>
</div> </div>
</div> </div>
)} )}
</motion.div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 bg-white border border-slate-100 rounded-2xl"> <div className="p-8 bg-white border border-slate-100 rounded-[2rem]">
<h5 className="font-bold text-slate-900 mb-2">Technische Basis</h5> <h5 className="text-lg font-bold text-slate-900 mb-3">Technische Basis</h5>
<p className="text-xs text-slate-500 leading-relaxed"> <p className="text-base text-slate-500 leading-relaxed">
Wir nutzen moderne i18n-Frameworks, die SEO-optimierte URLs für jede Sprache generieren (z.B. /en, /fr). Wir nutzen moderne i18n-Frameworks, die SEO-optimierte URLs für jede Sprache generieren (z.B. /en, /fr).
</p> </p>
</div> </div>
<div className="p-6 bg-white border border-slate-100 rounded-2xl"> <div className="p-8 bg-white border border-slate-100 rounded-[2rem]">
<h5 className="font-bold text-slate-900 mb-2">Content Management</h5> <h5 className="text-lg font-bold text-slate-900 mb-3">Content Management</h5>
<p className="text-xs text-slate-500 leading-relaxed"> <p className="text-base text-slate-500 leading-relaxed">
Falls ein CMS gewählt wurde, können Sie alle Übersetzungen bequem selbst pflegen. Falls ein CMS gewählt wurde, können Sie alle Übersetzungen bequem selbst pflegen.
</p> </p>
</div> </div>

View File

@@ -11,8 +11,8 @@ interface TimelineStepProps {
export function TimelineStep({ state, updateState }: TimelineStepProps) { export function TimelineStep({ state, updateState }: TimelineStepProps) {
return ( return (
<div className="space-y-8"> <div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[ {[
{ id: 'asap', label: 'So schnell wie möglich', desc: 'Priorisierter Start gewünscht.' }, { id: 'asap', label: 'So schnell wie möglich', desc: 'Priorisierter Start gewünscht.' },
{ id: '2-3-months', label: 'In 2-3 Monaten', desc: 'Normaler Projektvorlauf.' }, { id: '2-3-months', label: 'In 2-3 Monaten', desc: 'Normaler Projektvorlauf.' },
@@ -23,19 +23,19 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
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 rounded-[2rem] ${ className={`p-10 rounded-[2.5rem] border-2 text-left transition-all focus:outline-none overflow-hidden relative ${
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={`text-2xl font-bold mb-2 ${state.deadline === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</h4>
<p className={`text-sm ${state.deadline === opt.id ? 'text-white opacity-80' : 'text-slate-500'}`}>{opt.desc}</p> <p className={`text-lg ${state.deadline === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button> </button>
))} ))}
</div> </div>
{state.deadline === 'asap' && ( {state.deadline === 'asap' && (
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 flex gap-4 items-start"> <div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">
<AlertCircle className="text-slate-900 shrink-0 mt-1" size={20} /> <AlertCircle className="text-slate-900 shrink-0 mt-1" size={28} />
<p className="text-xs text-slate-600 leading-relaxed"> <p className="text-base text-slate-600 leading-relaxed">
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein Express-Zuschlag anfallen, um die Kapazitäten entsprechend zu priorisieren. <strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein Express-Zuschlag anfallen, um die Kapazitäten entsprechend zu priorisieren.
</p> </p>
</div> </div>

View File

@@ -14,19 +14,19 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[ {[
{ id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: <ConceptWebsite className="w-16 h-16 mb-4" /> }, { id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: <ConceptWebsite className="w-16 h-16 mb-4" /> },
{ id: 'app', label: 'App / Software', desc: 'Internes Tool, Dashboard oder Prozess-Logik.', illustration: <ConceptSystem className="w-16 h-16 mb-4" /> }, { id: 'web-app', label: 'Web App', desc: 'Internes Tool, Dashboard oder Prozess-Logik.', illustration: <ConceptSystem className="w-16 h-16 mb-4" /> },
].map((type) => ( ].map((type) => (
<button <button
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 rounded-[2.5rem] ${ className={`p-10 rounded-[2.5rem] border-2 text-left transition-all duration-300 focus:outline-none overflow-hidden relative ${
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'
}`} }`}
> >
<div className={state.projectType === type.id ? 'text-white' : 'text-slate-900'}>{type.illustration}</div> <div className={state.projectType === type.id ? 'text-white' : 'text-slate-900'}>{type.illustration}</div>
<h4 className={`text-2xl font-bold mb-3 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4> <h4 className={`text-3xl font-bold mb-3 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<p className={`text-lg leading-relaxed ${state.projectType === type.id ? 'text-slate-100' : 'text-slate-500'}`}>{type.desc}</p> <p className={`text-xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
</button> </button>
))} ))}
</div> </div>

View File

@@ -0,0 +1,139 @@
'use client';
import * as React from 'react';
import { FormState } from '../types';
import { Users, Shield, Monitor, Smartphone, Globe, Lock } from 'lucide-react';
interface WebAppStepProps {
state: FormState;
updateState: (updates: Partial<FormState>) => void;
}
export function WebAppStep({ state, updateState }: WebAppStepProps) {
const toggleUserRole = (role: string) => {
const current = state.userRoles || [];
const next = current.includes(role)
? current.filter(r => r !== role)
: [...current, role];
updateState({ userRoles: next });
};
return (
<div className="space-y-12">
{/* Target Audience */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Users size={24} /> Zielgruppe
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ id: 'internal', label: 'Internes Tool', desc: 'Für Mitarbeiter & Prozesse.' },
{ id: 'external', label: 'Kunden-Portal', desc: 'Für Ihre Endnutzer (B2B/B2C).' },
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => updateState({ targetAudience: opt.id })}
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
state.targetAudience === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
<p className="text-xl font-bold">{opt.label}</p>
<p className={`text-base mt-2 ${state.targetAudience === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
</div>
{/* User Roles */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900">Benutzerrollen</h4>
<p className="text-lg text-slate-500">Wer wird das System nutzen?</p>
<div className="flex flex-wrap gap-4">
{['Administratoren', 'Manager', 'Standard-Nutzer', 'Gäste', 'Read-Only'].map(role => (
<button
key={role}
type="button"
onClick={() => toggleUserRole(role)}
className={`px-8 py-4 rounded-full border-2 font-bold text-base transition-all ${
(state.userRoles || []).includes(role) ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
{role}
</button>
))}
</div>
</div>
{/* Platform Type */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Monitor size={24} /> Plattform-Fokus
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{ id: 'desktop', label: 'Desktop First', icon: <Monitor size={24} /> },
{ id: 'mobile', label: 'Mobile First', icon: <Smartphone size={24} /> },
{ id: 'pwa', label: 'PWA (Installierbar)', icon: <Globe size={24} /> },
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => updateState({ platformType: opt.id })}
className={`p-8 rounded-[2rem] border-2 flex flex-col items-center gap-4 transition-all ${
state.platformType === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
{opt.icon}
<span className="font-bold text-lg">{opt.label}</span>
</button>
))}
</div>
</div>
{/* Data Sensitivity */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Shield size={24} /> Datensicherheit
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ id: 'standard', label: 'Standard', desc: 'Normale Nutzerdaten & Profile.' },
{ id: 'high', label: 'Sensibel', desc: 'Finanzdaten, Gesundheitsdaten oder DSGVO-kritisch.' },
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => updateState({ dataSensitivity: opt.id })}
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
state.dataSensitivity === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
}`}
>
<p className="text-xl font-bold">{opt.label}</p>
<p className={`text-base mt-2 ${state.dataSensitivity === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
</div>
{/* Authentication */}
<div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Lock size={24} /> Authentifizierung
</h4>
<p className="text-lg text-slate-500">Wie sollen sich Nutzer anmelden?</p>
<div className="flex flex-wrap gap-4">
{['E-Mail / Passwort', 'Social Login', 'SSO / SAML', '2FA / MFA', 'Magic Links'].map(method => (
<div
key={method}
className="px-8 py-4 rounded-full border-2 border-white bg-white font-bold text-base text-slate-400"
>
{method}
</div>
))}
</div>
<p className="text-xs text-slate-400 italic">Details zur Authentifizierung besprechen wir im Erstgespräch.</p>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
export type ProjectType = 'website' | 'app'; export type ProjectType = 'website' | 'web-app';
export interface FormState { export interface FormState {
projectType: ProjectType; projectType: ProjectType;
@@ -32,6 +32,11 @@ export interface FormState {
languagesCount: number; languagesCount: number;
// Timeline // Timeline
deadline: string; deadline: string;
// Web App specific
targetAudience: string;
userRoles: string[];
dataSensitivity: string;
platformType: string;
} }
export interface Step { export interface Step {

View File

@@ -340,7 +340,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
} else { } else {
positions.push({ positions.push({
pos: pos++, pos: pos++,
title: 'App / Software Entwicklung', title: 'Web App / Software Entwicklung',
desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.', desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
qty: 1, qty: 1,
price: 0 price: 0
@@ -358,7 +358,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<View style={styles.quoteInfo}> <View style={styles.quoteInfo}>
<Text style={styles.quoteTitle}>Kostenschätzung</Text> <Text style={styles.quoteTitle}>Kostenschätzung</Text>
<Text style={styles.quoteDate}>{date}</Text> <Text style={styles.quoteDate}>{date}</Text>
<Text style={[styles.quoteDate, { marginTop: 2 }]}>Projekt: {state.projectType === 'website' ? 'Website' : 'Software'}</Text> <Text style={[styles.quoteDate, { marginTop: 2 }]}>Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}</Text>
</View> </View>
</View> </View>
@@ -439,14 +439,37 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<View style={styles.configSection}> <View style={styles.configSection}>
<Text style={styles.configTitle}>Konfiguration & Wünsche</Text> <Text style={styles.configTitle}>Konfiguration & Wünsche</Text>
<View style={styles.configGrid}> <View style={styles.configGrid}>
<View style={styles.configItem}> {state.projectType === 'website' ? (
<Text style={styles.configLabel}>Design-Vibe</Text> <>
<Text style={styles.configValue}>{vibeLabels[state.designVibe] || state.designVibe}</Text> <View style={styles.configItem}>
</View> <Text style={styles.configLabel}>Design-Vibe</Text>
<View style={styles.configItem}> <Text style={styles.configValue}>{vibeLabels[state.designVibe] || state.designVibe}</Text>
<Text style={styles.configLabel}>Farbschema</Text> </View>
<Text style={styles.configValue}>{state.colorScheme.join(', ')}</Text> <View style={styles.configItem}>
</View> <Text style={styles.configLabel}>Farbschema</Text>
<Text style={styles.configValue}>{state.colorScheme.join(', ')}</Text>
</View>
</>
) : (
<>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Zielgruppe</Text>
<Text style={styles.configValue}>{state.targetAudience === 'internal' ? 'Internes Tool' : 'Kunden-Portal'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Plattform</Text>
<Text style={styles.configValue}>{state.platformType.toUpperCase()}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Sicherheit</Text>
<Text style={styles.configValue}>{state.dataSensitivity === 'high' ? 'Sensibel' : 'Standard'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Rollen</Text>
<Text style={styles.configValue}>{state.userRoles.join(', ') || 'Keine'}</Text>
</View>
</>
)}
<View style={styles.configItem}> <View style={styles.configItem}>
<Text style={styles.configLabel}>Zeitplan</Text> <Text style={styles.configLabel}>Zeitplan</Text>
<Text style={styles.configValue}>{deadlineLabels[state.deadline] || state.deadline}</Text> <Text style={styles.configValue}>{deadlineLabels[state.deadline] || state.deadline}</Text>
@@ -465,17 +488,21 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<Text style={styles.configLabel}>Sprachen</Text> <Text style={styles.configLabel}>Sprachen</Text>
<Text style={styles.configValue}>{state.languagesCount}</Text> <Text style={styles.configValue}>{state.languagesCount}</Text>
</View> </View>
<View style={styles.configItem}> {state.projectType === 'website' && (
<Text style={styles.configLabel}>CMS (Inhaltsverwaltung)</Text> <>
<Text style={styles.configValue}>{state.cmsSetup ? 'Ja' : 'Nein'}</Text> <View style={styles.configItem}>
</View> <Text style={styles.configLabel}>CMS (Inhaltsverwaltung)</Text>
<View style={styles.configItem}> <Text style={styles.configValue}>{state.cmsSetup ? 'Ja' : 'Nein'}</Text>
<Text style={styles.configLabel}>Änderungsfrequenz</Text> </View>
<Text style={styles.configValue}> <View style={styles.configItem}>
{state.expectedAdjustments === 'low' ? 'Selten' : <Text style={styles.configLabel}>Änderungsfrequenz</Text>
state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'} <Text style={styles.configValue}>
</Text> {state.expectedAdjustments === 'low' ? 'Selten' :
</View> state.expectedAdjustments === 'medium' ? 'Regelmäßig' : 'Häufig'}
</Text>
</View>
</>
)}
</View> </View>
{state.designWishes && ( {state.designWishes && (