web app form
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
139
src/components/ContactForm/steps/WebAppStep.tsx
Normal file
139
src/components/ContactForm/steps/WebAppStep.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user