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 { TimelineStep } from './ContactForm/steps/TimelineStep';
import { ContactStep } from './ContactForm/steps/ContactStep';
import { WebAppStep } from './ContactForm/steps/WebAppStep';
import {
ConceptTarget,
@@ -84,7 +85,11 @@ export function ContactForm() {
languagesCount: state.languagesCount,
deadline: state.deadline,
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))));
@@ -182,11 +187,22 @@ export function ContactForm() {
{ 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: '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(() => {
if (state.projectType === 'website') return steps;
return [steps[0], steps[9], steps[10]];
if (state.projectType === 'website') {
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]);
useEffect(() => {
@@ -218,6 +234,8 @@ export function ContactForm() {
return <TimelineStep state={state} updateState={updateState} />;
case 'contact':
return <ContactStep state={state} updateState={updateState} />;
case 'webapp':
return <WebAppStep state={state} updateState={updateState} />;
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="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">
<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>
<p className="text-xl text-slate-500 leading-relaxed">{activeSteps[stepIndex].description}</p>
</div>
@@ -291,7 +309,7 @@ export function ContactForm() {
initial={{ opacity: 0, y: 5, x: '-50%' }}
animate={{ opacity: 1, y: 0, 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}
<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 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 ? (
<button type="button" onClick={prevStep} className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
<ChevronLeft size={14} /> Zurück
<button 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={18} /> Zurück
</button>
) : <div />}
<div className="flex items-center gap-4">
{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 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={18} />
</button>
)}
</div>
@@ -321,8 +339,8 @@ export function ContactForm() {
<form onSubmit={handleSubmit} className="min-h-[450px]">
<AnimatePresence mode="wait"><motion.div key={activeSteps[stepIndex].id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}>{renderStepContent()}</motion.div></AnimatePresence>
<div className="flex justify-between mt-16">
{stepIndex > 0 ? (<button type="button" onClick={prevStep} className="flex items-center gap-3 px-10 py-5 rounded-full border border-slate-200 hover:border-slate-900 transition-all font-bold text-lg focus:outline-none overflow-hidden relative 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 > 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-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>
</form>
</div>

View File

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

View File

@@ -90,7 +90,7 @@ export function PriceCalculation({
<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-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>
</div>
</div>

View File

@@ -20,8 +20,8 @@ export function RepeatableList({
}: RepeatableListProps) {
const [input, setInput] = useState('');
return (
<div className="space-y-4">
<div className="flex gap-2">
<div className="space-y-6">
<div className="flex gap-3">
<input
type="text"
value={input}
@@ -36,7 +36,7 @@ export function RepeatableList({
}
}}
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
type="button"
@@ -46,12 +46,12 @@ export function RepeatableList({
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>
</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-3">
<AnimatePresence>
{items.map((item, i) => (
<motion.div
@@ -59,11 +59,11 @@ export function RepeatableList({
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
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>
<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} />
<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">
<X size={18} />
</button>
</motion.div>
))}

View File

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

View File

@@ -2,10 +2,8 @@
import * as React from 'react';
import { FormState } from '../types';
import { API_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Info } from 'lucide-react';
interface ApiStepProps {
state: FormState;
@@ -14,33 +12,56 @@ interface ApiStepProps {
}
export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
const isWebApp = state.projectType === 'web-app';
return (
<div className="space-y-8">
<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">Wichtig zu wissen</p>
<p>Ich biete diese Drittsysteme nicht selbst an, sondern entwickle die <strong>Schnittstelle (API)</strong>, damit Ihre Website nahtlos mit ihnen kommunizieren kann.</p>
<div className="space-y-12">
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
</h4>
<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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
{API_OPTIONS.map(opt => (
<Checkbox
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
items={state.otherTech}
<div className="space-y-6">
<p className="text-lg font-bold text-slate-900">Weitere Systeme oder eigene APIs?</p>
<RepeatableList
items={state.otherTech}
onAdd={(v) => updateState({ otherTech: [...state.otherTech, v] })}
onRemove={(i) => updateState({ otherTech: state.otherTech.filter((_, idx) => idx !== i) })}
placeholder="z.B. Personio, DATEV, Salesforce..."
onRemove={(i) => updateTech(i)}
placeholder="z.B. Microsoft Graph, Google Maps, Custom REST API..."
/>
</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) {
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{ASSET_OPTIONS.map(opt => (
<Checkbox
key={opt.id} label={opt.label} desc={opt.desc}
@@ -24,13 +24,13 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
/>
))}
</div>
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Weitere Materialien?</p>
<RepeatableList
items={state.otherAssets}
<div className="space-y-6">
<p className="text-lg font-bold text-slate-900">Weitere vorhandene Unterlagen?</p>
<RepeatableList
items={state.otherAssets}
onAdd={(v) => updateState({ otherAssets: [...state.otherAssets, v] })}
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>

View File

@@ -2,10 +2,8 @@
import * as React from 'react';
import { FormState } from '../types';
import { PAGE_SAMPLES } from '../constants';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Info, FileText, Upload, X } from 'lucide-react';
interface BaseStepProps {
state: FormState;
@@ -15,66 +13,31 @@ interface BaseStepProps {
export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{PAGE_SAMPLES.map(p => (
<div className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ 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
key={p.id} label={p.label} desc={p.desc}
checked={state.selectedPages.includes(p.id)}
onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, p.id) })}
key={opt.id} label={opt.label} desc={opt.desc}
checked={state.selectedPages.includes(opt.id)}
onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })}
/>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-4">
<p className="text-sm font-bold text-slate-900">Weitere Seiten?</p>
<RepeatableList
items={state.otherPages}
onAdd={(v) => updateState({ otherPages: [...state.otherPages, v] })}
onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })}
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 className="space-y-6">
<p className="text-lg font-bold text-slate-900">Weitere individuelle Seiten?</p>
<RepeatableList
items={state.otherPages}
onAdd={(v) => updateState({ otherPages: [...state.otherPages, v] })}
onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })}
placeholder="z.B. Karriere, FAQ, Team-Detail..."
/>
</div>
</div>
);

View File

@@ -11,18 +11,44 @@ interface ContactStepProps {
export function ContactStep({ state, updateState }: ContactStepProps) {
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input type="text" placeholder="Ihr Name" required value={state.name} onChange={(e) => updateState({ name: e.target.value })} className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors rounded-[2rem]" />
<input type="email" placeholder="Ihre Email" required value={state.email} onChange={(e) => updateState({ email: e.target.value })} className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors rounded-[2rem]" />
<div className="space-y-10">
<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 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>
<input type="text" placeholder="Ihre Rolle (z.B. CEO, Marketing Manager...)" value={state.role} onChange={(e) => updateState({ role: e.target.value })} className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors rounded-[2rem]" />
<textarea placeholder="Erzählen Sie mir kurz von Ihrem Projekt..." value={state.message} onChange={(e) => updateState({ message: e.target.value })} rows={4} className="w-full p-8 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-colors resize-none rounded-[2rem]" />
<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 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">
<p className="text-sm font-bold text-slate-900">Dateien hochladen (optional)</p>
<div className="space-y-6">
<p className="text-lg font-bold text-slate-900">Dateien hochladen (optional)</p>
<div
className={`relative group border-2 border-dashed rounded-[2rem] p-8 transition-all duration-300 flex flex-col items-center justify-center gap-4 cursor-pointer min-h-[160px] 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'
}`}
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
@@ -40,12 +66,12 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
}} />
{state.contactFiles.length > 0 ? (
<div className="w-full space-y-2">
<div className="w-full space-y-3">
{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 className="flex items-center gap-3 text-slate-900">
<FileText size={20} className="text-slate-400" />
<span className="font-bold text-sm truncate max-w-[200px]">{file.name}</span>
<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-4 text-slate-900">
<FileText size={24} className="text-slate-400" />
<span className="font-bold text-base truncate max-w-[250px]">{file.name}</span>
</div>
<button
type="button"
@@ -53,20 +79,20 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
e.stopPropagation();
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
}}
className="p-1 hover:bg-slate-100 rounded-full transition-colors focus:outline-none overflow-hidden relative rounded-full"
className="p-2 hover:bg-slate-100 rounded-full transition-colors focus:outline-none"
>
<X size={16} />
<X size={20} />
</button>
</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>
) : (
<>
<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">
<p className="text-sm 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-lg font-bold text-slate-900">Dateien hierher ziehen</p>
<p className="text-base text-slate-500 mt-1">oder klicken zum Auswählen</p>
</div>
</>
)}

View File

@@ -11,24 +11,24 @@ interface ContentStepProps {
export function ContentStep({ state, updateState }: ContentStepProps) {
return (
<div className="space-y-8">
<div className="flex items-center justify-between p-8 bg-white border border-slate-100 rounded-[2rem]">
<div className="space-y-12">
<div className="flex items-center justify-between p-10 bg-white border border-slate-100 rounded-[3rem]">
<div className="max-w-[70%]">
<h4 className="text-xl 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>
<h4 className="text-2xl font-bold text-slate-900">Inhalte selbst verwalten (CMS)</h4>
<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>
<button
type="button"
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>
</div>
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 space-y-4">
<p className="text-sm 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="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-6">
<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-6">
{[
{ id: 'low', label: 'Selten', desc: 'Wenige Male im Jahr.' },
{ id: 'medium', label: 'Regelmäßig', desc: 'Monatliche Updates.' },
@@ -38,44 +38,44 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
key={opt.id}
type="button"
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'
}`}
>
<p className="font-bold text-sm">{opt.label}</p>
<p className={`text-[10px] ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
<p className="font-bold text-lg">{opt.label}</p>
<p className={`text-sm mt-1 ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
<div className="p-6 bg-white rounded-2xl border border-slate-100 space-y-2">
<div className="flex items-center gap-2 text-slate-900 font-bold text-xs uppercase tracking-wider">
<Zap size={14} /> Vorteil CMS
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8">
<div className="p-8 bg-white rounded-[2rem] border border-slate-100 space-y-3">
<div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-wider">
<Zap size={18} /> Vorteil CMS
</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.
</p>
</div>
<div className="p-6 bg-white rounded-2xl border border-slate-100 space-y-2">
<div className="flex items-center gap-2 text-slate-900 font-bold text-xs uppercase tracking-wider">
<AlertCircle size={14} /> Fokus Design
<div className="p-8 bg-white rounded-[2rem] border border-slate-100 space-y-3">
<div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-wider">
<AlertCircle size={18} /> Fokus Design
</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.
</p>
</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>
<h4 className="text-xl 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>
<h4 className="text-2xl font-bold text-slate-900">Inhalte einpflegen</h4>
<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 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"><Minus size={20} /></button>
<span className="text-3xl font-bold w-12 text-center">{state.newDatasets}</span>
<button type="button" onClick={() => updateState({ newDatasets: state.newDatasets + 1 })} className="w-12 h-12 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"><Plus size={20} /></button>
<div className="flex items-center gap-12 mt-2">
<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-5xl font-bold w-16 text-center">{state.newDatasets}</span>
<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>

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
import * as React from 'react';
import { FormState } from '../types';
import { FUNCTION_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Minus, Plus } from 'lucide-react';
@@ -14,40 +13,90 @@ interface FunctionsStepProps {
}
export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepProps) {
const isWebApp = state.projectType === 'web-app';
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{FUNCTION_OPTIONS.map(opt => (
<Checkbox
key={opt.id} label={opt.label} desc={opt.desc}
checked={state.functions.includes(opt.id)}
onChange={() => updateState({ functions: toggleItem(state.functions, opt.id) })}
/>
))}
<div className="space-y-12">
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Funktionale Anforderungen' : 'Erweiterte Funktionen'}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{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 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
items={state.otherFunctions}
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
placeholder="z.B. Login-Bereich, Buchungssystem..."
placeholder={isWebApp ? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..." : "z.B. Login-Bereich, Buchungssystem..."}
/>
</div>
<div className="p-8 bg-white border border-slate-100 rounded-[2rem] space-y-4">
<div className="flex justify-between items-start">
<div>
<h4 className="text-xl font-bold text-slate-900">Besondere Interaktionen</h4>
<p className="text-sm text-slate-500 mt-1">Aufwendige Animationen oder komplexe UI-Logik pro Abschnitt.</p>
</div>
<div className="flex items-center gap-6">
<button type="button" onClick={() => updateState({ complexInteractions: Math.max(0, state.complexInteractions - 1) })} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full"><Minus size={18} /></button>
<span className="text-2xl font-bold w-8 text-center">{state.complexInteractions}</span>
<button type="button" onClick={() => updateState({ complexInteractions: state.complexInteractions + 1 })} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none overflow-hidden relative rounded-full"><Plus size={18} /></button>
{!isWebApp && (
<div className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6">
<div className="flex justify-between items-start">
<div>
<h4 className="text-2xl font-bold text-slate-900">Besondere Interaktionen</h4>
<p className="text-lg text-slate-500 mt-2">Aufwendige Animationen oder komplexe UI-Logik pro Abschnitt.</p>
</div>
<div className="flex items-center gap-8">
<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>
<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>
);
}

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.";
return (
<div className="space-y-8">
<div className="p-8 bg-white border border-slate-100 rounded-[2rem] space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-900">
<Globe size={24} />
<div className="space-y-12">
<div className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-8">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-900">
<Globe size={32} />
</div>
<div>
<h4 className="text-xl 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>
<h4 className="text-2xl font-bold text-slate-900">Mehrsprachigkeit</h4>
<p className="text-lg text-slate-500">In wie vielen Sprachen soll Ihre Website verfügbar sein?</p>
</div>
</div>
<div className="flex items-center gap-8 py-4">
<div className="flex items-center gap-12 py-6">
<button
type="button"
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>
<div className="flex flex-col items-center">
<span className="text-5xl 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-7xl font-bold text-slate-900">{state.languagesCount}</span>
<span className="text-sm font-bold uppercase tracking-widest text-slate-400 mt-3">
{state.languagesCount === 1 ? 'Sprache' : 'Sprachen'}
</span>
</div>
<button
type="button"
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>
</div>
</div>
@@ -53,35 +53,35 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
<motion.div
initial={{ opacity: 0, y: 10 }}
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">
<Info size={18} />
<span className="text-xs font-bold uppercase tracking-widest">Warum dieser Faktor?</span>
<div className="flex items-center gap-4 text-slate-400">
<Info size={24} />
<span className="text-sm font-bold uppercase tracking-widest">Warum dieser Faktor?</span>
</div>
<p className="text-sm leading-relaxed text-slate-300">
<p className="text-lg leading-relaxed text-slate-300">
{basePriceExplanation}
</p>
{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">
<span className="text-sm font-medium">Aktueller Aufschlagsfaktor:</span>
<span className="text-xl font-bold text-white">+{((state.languagesCount - 1) * 20)}%</span>
<span className="text-lg font-medium">Aktueller Aufschlagsfaktor:</span>
<span className="text-3xl font-bold text-white">+{((state.languagesCount - 1) * 20)}%</span>
</div>
</div>
)}
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-6 bg-white border border-slate-100 rounded-2xl">
<h5 className="font-bold text-slate-900 mb-2">Technische Basis</h5>
<p className="text-xs text-slate-500 leading-relaxed">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-8 bg-white border border-slate-100 rounded-[2rem]">
<h5 className="text-lg font-bold text-slate-900 mb-3">Technische Basis</h5>
<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).
</p>
</div>
<div className="p-6 bg-white border border-slate-100 rounded-2xl">
<h5 className="font-bold text-slate-900 mb-2">Content Management</h5>
<p className="text-xs text-slate-500 leading-relaxed">
<div className="p-8 bg-white border border-slate-100 rounded-[2rem]">
<h5 className="text-lg font-bold text-slate-900 mb-3">Content Management</h5>
<p className="text-base text-slate-500 leading-relaxed">
Falls ein CMS gewählt wurde, können Sie alle Übersetzungen bequem selbst pflegen.
</p>
</div>

View File

@@ -11,8 +11,8 @@ interface TimelineStepProps {
export function TimelineStep({ state, updateState }: TimelineStepProps) {
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-12">
<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: '2-3-months', label: 'In 2-3 Monaten', desc: 'Normaler Projektvorlauf.' },
@@ -23,19 +23,19 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
key={opt.id}
type="button"
onClick={() => updateState({ deadline: opt.id })}
className={`p-8 rounded-[2rem] border-2 text-left transition-all focus:outline-none overflow-hidden relative 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'
}`}
>
<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-white opacity-80' : 'text-slate-500'}`}>{opt.desc}</p>
<h4 className={`text-2xl font-bold mb-2 ${state.deadline === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</h4>
<p className={`text-lg ${state.deadline === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</button>
))}
</div>
{state.deadline === 'asap' && (
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 flex gap-4 items-start">
<AlertCircle className="text-slate-900 shrink-0 mt-1" size={20} />
<p className="text-xs text-slate-600 leading-relaxed">
<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={28} />
<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.
</p>
</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">
{[
{ 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) => (
<button
key={type.id}
type="button"
onClick={() => updateState({ projectType: type.id as ProjectType })}
className={`p-10 rounded-[2.5rem] border-2 text-left transition-all duration-300 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'
}`}
>
<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>
<p className={`text-lg leading-relaxed ${state.projectType === type.id ? 'text-slate-100' : 'text-slate-500'}`}>{type.desc}</p>
<h4 className={`text-3xl font-bold mb-3 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<p className={`text-xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
</button>
))}
</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 {
projectType: ProjectType;
@@ -32,6 +32,11 @@ export interface FormState {
languagesCount: number;
// Timeline
deadline: string;
// Web App specific
targetAudience: string;
userRoles: string[];
dataSensitivity: string;
platformType: string;
}
export interface Step {

View File

@@ -340,7 +340,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
} else {
positions.push({
pos: pos++,
title: 'App / Software Entwicklung',
title: 'Web App / Software Entwicklung',
desc: 'Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.',
qty: 1,
price: 0
@@ -358,7 +358,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<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>
<Text style={[styles.quoteDate, { marginTop: 2 }]}>Projekt: {state.projectType === 'website' ? 'Website' : 'Web App'}</Text>
</View>
</View>
@@ -439,14 +439,37 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<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>
{state.projectType === 'website' ? (
<>
<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}>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}>
<Text style={styles.configLabel}>Zeitplan</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.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>
{state.projectType === 'website' && (
<>
<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 && (