This commit is contained in:
2026-01-30 19:15:39 +01:00
parent 8124e9cc95
commit d2d4f3be14
19 changed files with 325 additions and 252 deletions

View File

@@ -200,20 +200,28 @@ export function ContactForm() {
};
const steps: Step[] = [
{ id: 'type', title: 'Das Ziel', description: 'Was möchten Sie realisieren?', illustration: <ConceptTarget className="w-full h-full" /> },
{ id: 'company', title: 'Unternehmen', description: 'Wer sind Sie?', illustration: <ConceptCommunication className="w-full h-full" /> },
{ id: 'presence', title: 'Präsenz', description: 'Bestehende Kanäle.', illustration: <ConceptSystem className="w-full h-full" /> },
{ id: 'features', title: 'Die Systeme', description: 'Welche inhaltlichen Bereiche planen wir?', illustration: <ConceptPrototyping className="w-full h-full" /> },
{ id: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: <ConceptWebsite className="w-full h-full" /> },
{ id: 'design', title: 'Design-Wünsche', description: 'Wie soll die Seite wirken?', illustration: <ConceptCommunication className="w-full h-full" /> },
{ id: 'assets', title: 'Ihre Assets', description: 'Was bringen Sie bereits mit?', illustration: <ConceptSystem className="w-full h-full" /> },
{ id: 'functions', title: 'Die Logik', description: 'Welche Funktionen werden benötigt?', illustration: <ConceptCode className="w-full h-full" /> },
{ id: 'api', title: 'Schnittstellen', description: 'Datenaustausch mit Drittsystemen.', illustration: <ConceptAutomation className="w-full h-full" /> },
{ id: 'content', title: 'Die Pflege', description: 'Wer kümmert sich um die Daten?', illustration: <ConceptPrice className="w-full h-full" /> },
{ id: 'language', title: 'Sprachen', description: 'Globale Reichweite planen.', illustration: <ConceptCommunication className="w-full h-full" /> },
{ id: 'timeline', title: 'Zeitplan', description: 'Wann soll das Projekt live gehen?', illustration: <HeroArchitecture className="w-full h-full" /> },
{ id: '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" /> },
{ id: 'type', title: 'Das Ziel', description: 'Was möchten Sie realisieren?', illustration: <ConceptTarget className="w-full h-full" />, chapter: 'strategy' },
{ id: 'company', title: 'Unternehmen', description: 'Wer sind Sie?', illustration: <ConceptCommunication className="w-full h-full" />, chapter: 'strategy' },
{ id: 'presence', title: 'Präsenz', description: 'Bestehende Kanäle von {company}.', illustration: <ConceptSystem className="w-full h-full" />, chapter: 'strategy' },
{ id: 'features', title: 'Die Systeme', description: 'Welche inhaltlichen Bereiche planen wir für {company}?', illustration: <ConceptPrototyping className="w-full h-full" />, chapter: 'scope' },
{ id: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: <ConceptWebsite className="w-full h-full" />, chapter: 'scope' },
{ id: 'design', title: 'Design-Wünsche', description: 'Wie soll die neue Präsenz von {company} wirken?', illustration: <ConceptCommunication className="w-full h-full" />, chapter: 'creative' },
{ id: 'assets', title: 'Ihre Assets', description: 'Was bringen Sie bereits mit?', illustration: <ConceptSystem className="w-full h-full" />, chapter: 'creative' },
{ id: 'functions', title: 'Die Logik', description: 'Welche Funktionen werden benötigt?', illustration: <ConceptCode className="w-full h-full" />, chapter: 'tech' },
{ id: 'api', title: 'Schnittstellen', description: 'Datenaustausch mit Drittsystemen.', illustration: <ConceptAutomation className="w-full h-full" />, chapter: 'tech' },
{ id: 'content', title: 'Die Pflege', description: 'Wer kümmert sich um die Daten?', illustration: <ConceptPrice className="w-full h-full" />, chapter: 'tech' },
{ id: 'language', title: 'Sprachen', description: 'Globale Reichweite planen.', illustration: <ConceptCommunication className="w-full h-full" />, chapter: 'tech' },
{ id: 'timeline', title: 'Zeitplan', description: 'Wann soll das Projekt live gehen?', illustration: <HeroArchitecture className="w-full h-full" />, chapter: 'final' },
{ id: 'contact', title: 'Abschluss', description: 'Erzählen Sie mir mehr über Ihr Vorhaben.', illustration: <ConceptCommunication className="w-full h-full" />, chapter: 'final' },
{ id: 'webapp', title: 'Web App Details', description: 'Spezifische Anforderungen für {company}.', illustration: <ConceptSystem className="w-full h-full" />, chapter: 'scope' },
];
const chapters = [
{ id: 'strategy', title: 'Strategie' },
{ id: 'scope', title: 'Umfang' },
{ id: 'creative', title: 'Design' },
{ id: 'tech', title: 'Technik' },
{ id: 'final', title: 'Start' },
];
const activeSteps = useMemo(() => {
@@ -231,7 +239,7 @@ export function ContactForm() {
steps.find(s => s.id === 'timeline')!,
steps.find(s => s.id === 'contact')!,
];
}, [state.projectType]);
}, [state.projectType, state.companyName]);
useEffect(() => {
if (stepIndex >= activeSteps.length) setStepIndex(activeSteps.length - 1);
@@ -312,17 +320,31 @@ export function ContactForm() {
<div className="lg:col-span-8 space-y-12">
<div className="flex flex-col gap-10">
<div className="flex flex-col md:flex-row md:items-center gap-8">
<div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4 flex items-center justify-center">{activeSteps[stepIndex].illustration}</div>
<div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4 flex items-center justify-center relative">
{activeSteps[stepIndex].illustration}
<div className="absolute -bottom-2 -right-2 w-10 h-10 bg-slate-900 text-white rounded-full flex items-center justify-center font-bold text-sm border-4 border-white">
{stepIndex + 1}
</div>
</div>
<div className="space-y-2">
<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 className="flex items-center gap-3">
<span className="text-base font-bold uppercase tracking-[0.2em] text-slate-400">
Schritt {stepIndex + 1} von {activeSteps.length}
</span>
</div>
<h3 className="text-4xl font-bold tracking-tight text-slate-900">
{activeSteps[stepIndex].title.replace('{company}', state.companyName || 'Ihr Unternehmen')}
</h3>
<p className="text-xl text-slate-500 leading-relaxed">
{activeSteps[stepIndex].description.replace('{company}', state.companyName || 'Ihr Unternehmen')}
</p>
</div>
</div>
<div className="flex gap-3 h-4">
<div className="flex gap-2 h-2">
{activeSteps.map((step, i) => (
<div
key={i}
<div
key={i}
className="flex-1 h-full flex items-center relative"
onMouseEnter={() => setHoveredStep(i)}
onMouseLeave={() => setHoveredStep(null)}
@@ -333,21 +355,11 @@ export function ContactForm() {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`w-full h-full rounded-full transition-all duration-700 ${i <= stepIndex ? 'bg-slate-900' : 'bg-slate-100'} cursor-pointer focus:outline-none p-0 border-none`}
className={`w-full h-full rounded-full transition-all duration-700 ${
i === stepIndex ? 'bg-slate-900 scale-y-150' :
i < stepIndex ? 'bg-slate-400' : 'bg-slate-100'
} cursor-pointer focus:outline-none p-0 border-none`}
/>
<AnimatePresence>
{hoveredStep === i && (
<motion.div
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-3 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" />
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>

View File

@@ -22,7 +22,7 @@ export function Input({ label, icon: Icon, isTextArea, className = '', ...props
)}
<div className="relative group">
{Icon && (
<div className="absolute left-6 top-[1.8rem] -translate-y-1/2 text-black transition-colors">
<div className={`absolute left-6 ${isTextArea ? 'top-10' : 'top-1/2'} -translate-y-1/2 text-black transition-colors`}>
<Icon size={24} />
</div>
)}

View File

@@ -80,7 +80,11 @@ export function PriceCalculation({
document={<EstimationPDF state={state} totalPrice={totalPrice} monthlyPrice={monthlyPrice} totalPagesCount={totalPagesCount} pricing={PRICING} qrCodeData={qrCodeData} />}
fileName={`kalkulation-${state.name.toLowerCase().replace(/\s+/g, '-') || 'projekt'}.pdf`}
className="w-full flex items-center justify-center gap-3 px-8 py-4 rounded-full border border-slate-200 text-slate-900 font-bold text-sm uppercase tracking-widest hover:bg-white hover:border-slate-900 transition-all focus:outline-none overflow-hidden relative"
onClick={() => {
onClick={(e) => {
if (pdfLoading) {
e.preventDefault();
return;
}
setPdfLoading(true);
setTimeout(() => setPdfLoading(false), 2000);
}}

View File

@@ -69,6 +69,8 @@ export const initialState: FormState = {
platformType: 'web-only',
// Meta
dontKnows: [],
visualStaging: 'standard',
complexInteractions: 'standard',
};
export const PAGE_SAMPLES = [
@@ -107,7 +109,6 @@ export const API_OPTIONS = [
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' },
{ id: 'analytics', label: 'Custom Analytics', desc: 'Anbindung an spezialisierte Tracking-Tools.' },
{ id: 'auth', label: 'Auth-Provider', desc: 'NextAuth, Clerk, Auth0 Integration.' },
];
export const ASSET_OPTIONS = [

View File

@@ -35,16 +35,19 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Share2 size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
</h4>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
</h4>
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
</div>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('api')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('api') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -63,7 +66,6 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
{ id: 'marketing', label: 'Marketing', desc: 'Newsletter (Mailchimp), Social Media Sync.' },
{ id: 'ecommerce', label: 'E-Commerce', desc: 'Shopify, WooCommerce, Lagerbestand-Sync.' },
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' },
{ id: 'auth', label: 'Auth-Provider', desc: 'NextAuth, Clerk, Auth0 Integration.' },
].map((opt, index) => (
<motion.div
key={opt.id}

View File

@@ -41,7 +41,7 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('assets')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('assets') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -56,11 +56,18 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Checkbox
key={opt.id} label={opt.label} desc={opt.desc}
checked={state.assets.includes(opt.id)}
onChange={() => updateState({ assets: toggleItem(state.assets, opt.id) })}
/>
<div className="relative">
<Checkbox
key={opt.id} label={opt.label} desc={opt.desc}
checked={state.assets.includes(opt.id)}
onChange={() => updateState({ assets: toggleItem(state.assets, opt.id) })}
/>
{['logo', 'styleguide', 'content_concept'].includes(opt.id) && (
<div className="absolute top-4 right-4 px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">
Wichtig
</div>
)}
</div>
</motion.div>
))}
</div>
@@ -84,12 +91,13 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
/>
</div>
<motion.div
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500"
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500 relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-1 h-full bg-slate-900" />
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div>
<h4 className="text-xl font-bold text-slate-900">Anzahl weiterer Assets</h4>

View File

@@ -5,7 +5,7 @@ import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, FileText, ListPlus } from 'lucide-react';
import { Minus, Plus, FileText, ListPlus, HelpCircle } from 'lucide-react';
import { Input } from '../components/Input';
interface BaseStepProps {
@@ -31,12 +31,17 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
>
<Input
label="Thema der Website"
placeholder="z.B. Portfolio für Architektur, Onlineshop für Bio-Tee..."
value={state.websiteTopic}
onChange={(e) => updateState({ websiteTopic: e.target.value })}
/>
<div className="relative">
<Input
label="Thema der Website"
placeholder="z.B. Portfolio für Architektur, Onlineshop für Bio-Tee..."
value={state.websiteTopic}
onChange={(e) => updateState({ websiteTopic: e.target.value })}
/>
<div className="absolute top-0 right-4 px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">
Wichtig
</div>
</div>
</motion.div>
<div className="space-y-8">
@@ -45,14 +50,23 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<FileText size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Die Seiten</h4>
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-bold text-slate-900">Die Seiten</h4>
<span className="px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">Wichtig</span>
</div>
<div className="flex items-center gap-2 text-slate-400 mt-1">
<HelpCircle size={14} />
<span className="text-sm">Warum ist das wichtig? Die Anzahl der Seiten bestimmt maßgeblich den Design- und Umsetzungsaufwand.</span>
</div>
</div>
</div>
<motion.button
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('pages')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('pages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>

View File

@@ -23,6 +23,7 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
<Building2 size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Unternehmen</h4>
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Wichtig</span>
</div>
<Input
label="Name des Unternehmens"

View File

@@ -15,28 +15,48 @@ interface ContactStepProps {
export function ContactStep({ state, updateState }: ContactStepProps) {
return (
<div className="space-y-12">
<Reveal width="100%" delay={0.05}>
<div className="p-8 bg-slate-50 text-slate-900 rounded-[2.5rem] mb-8 border border-slate-100">
<h4 className="text-2xl font-bold mb-2">Fast geschafft! 🚀</h4>
<p className="text-slate-500 text-lg">
Ich habe alle Details für das Projekt von <span className="text-slate-900 font-bold">{state.companyName || 'Ihrem Unternehmen'}</span> erhalten.
Hinterlassen Sie mir noch Ihre Kontaktdaten, damit ich Ihnen ein konkretes Angebot erstellen kann.
</p>
</div>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Reveal width="100%" delay={0.1}>
<Input
label="Ihr Name"
icon={User}
placeholder="Max Mustermann"
required
value={state.name}
onChange={(e) => updateState({ name: e.target.value })}
/>
<div className="relative">
<Input
label="Ihr Name"
icon={User}
placeholder="Max Mustermann"
required
value={state.name}
onChange={(e) => updateState({ name: e.target.value })}
/>
<div className="absolute top-0 right-4 px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">
Wichtig
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.1}>
<Input
label="Ihre Email"
icon={Mail}
type="email"
placeholder="max@beispiel.de"
required
value={state.email}
onChange={(e) => updateState({ email: e.target.value })}
/>
<div className="relative">
<Input
label="Ihre Email"
icon={Mail}
type="email"
placeholder="max@beispiel.de"
required
value={state.email}
onChange={(e) => updateState({ email: e.target.value })}
/>
<div className="absolute top-0 right-4 px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">
Wichtig
</div>
</div>
</Reveal>
</div>

View File

@@ -32,7 +32,10 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
</div>
<h4 className="text-2xl font-bold text-slate-900">Inhalte selbst verwalten (CMS)</h4>
</div>
<p className="text-lg text-slate-500 leading-relaxed">Möchten Sie Datensätze (z.B. Blogartikel, Produkte) selbst über eine einfache Oberfläche pflegen?</p>
<p className="text-lg text-slate-500 leading-relaxed">
Ein CMS (Content Management System) ermöglicht es Ihnen, Texte, Bilder und Blogartikel selbst zu ändern, ohne programmieren zu müssen.
Ideal, wenn Sie Ihre Website aktuell halten möchten.
</p>
</div>
<div className="flex flex-col items-center md:items-end gap-6">
<motion.button
@@ -138,7 +141,10 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
<div className="flex flex-col gap-8 p-10 bg-white border border-slate-100 rounded-[3rem]">
<div className="space-y-2">
<h4 className="text-2xl font-bold text-slate-900">Inhalte einpflegen</h4>
<p className="text-lg text-slate-500 leading-relaxed">Für wie viele Datensätze soll ich die initiale Befüllung übernehmen?</p>
<p className="text-lg text-slate-500 leading-relaxed">
Wer kümmert sich um die erste Befüllung? Wenn ich das übernehmen soll, geben Sie hier die Anzahl der Datensätze (z.B. fertige Blogartikel oder Produkte) an.
Ansonsten übergeben wir Ihnen eine leere, aber einsatzbereite Struktur.
</p>
</div>
<div className="flex items-center gap-12 py-2">
<motion.button

View File

@@ -7,6 +7,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { Plus, X, Palette, Pipette, RefreshCw } from 'lucide-react';
import { Reveal } from '../../Reveal';
import { Input } from '../components/Input';
import { RepeatableList } from '../components/RepeatableList';
interface DesignStepProps {
state: FormState;
@@ -59,12 +60,13 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
return `#${f(0)}${f(8)}${f(4)}`;
};
const palette = [
hslToHex(hue, saturation, 95), // Light
hslToHex(hue, saturation, lightness), // Main
hslToHex((hue + 30) % 360, saturation, lightness - 10), // Analogous
hslToHex((hue + 180) % 360, saturation - 10, 20), // Complementary Dark
];
const count = state.colorScheme.length;
const palette = [];
for (let i = 0; i < count; i++) {
const h = (hue + (i * (360 / count))) % 360;
const l = i === 0 ? 95 : i === count - 1 ? 20 : lightness;
palette.push(hslToHex(h, saturation, l));
}
updateState({ colorScheme: palette });
};
@@ -83,7 +85,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('design_vibe')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('design_vibe') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -137,7 +139,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('color_scheme')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('color_scheme') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -206,15 +208,33 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
</div>
</Reveal>
{/* Wishes */}
{/* References */}
<Reveal width="100%" delay={0.3}>
<div className="space-y-8">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Referenz-Websites</h4>
<p className="text-slate-500">Gibt es Websites, die Ihnen besonders gut gefallen?</p>
</div>
<div className="p-10 bg-white border border-slate-100 rounded-[3rem]">
<RepeatableList
items={state.references || []}
onAdd={(v) => updateState({ references: [...(state.references || []), v] })}
onRemove={(i) => updateState({ references: (state.references || []).filter((_, idx) => idx !== i) })}
placeholder="https://beispiel.de"
/>
</div>
</div>
</Reveal>
{/* Wishes */}
<Reveal width="100%" delay={0.4}>
<Input
label="Individuelle Wünsche"
isTextArea
rows={4}
placeholder="Haben Sie bereits konkrete Vorstellungen oder Referenzen?"
value={state.designWishes}
onChange={(e) => updateState({ designWishes: e.target.value })}
placeholder="Haben Sie weitere konkrete Vorstellungen?"
value={state.designWishes}
onChange={(e) => updateState({ designWishes: e.target.value })}
/>
</Reveal>
</div>

View File

@@ -6,7 +6,7 @@ import { FEATURE_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, LayoutGrid, ListPlus } from 'lucide-react';
import { Minus, Plus, LayoutGrid, ListPlus, HelpCircle } from 'lucide-react';
interface FeaturesStepProps {
state: FormState;
@@ -32,14 +32,23 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<LayoutGrid size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">System-Module</h4>
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-bold text-slate-900">System-Module</h4>
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
</div>
<div className="flex items-center gap-2 text-slate-400 mt-1">
<HelpCircle size={14} />
<span className="text-sm">Module sind funktionale Einheiten, die über einfache Textseiten hinausgehen.</span>
</div>
</div>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('features')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('features') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -80,50 +89,6 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
/>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500"
>
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div>
<h4 className="text-xl font-bold text-slate-900">Anzahl weiterer Module</h4>
<p className="text-base text-slate-500 mt-1">Falls Sie weitere Systeme planen, diese aber noch nicht benennen können.</p>
</div>
<div className="flex items-center gap-8">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherFeaturesCount: Math.max(0, state.otherFeaturesCount - 1) })}
className="w-14 h-14 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} />
</motion.button>
<AnimatePresence mode="wait">
<motion.span
key={state.otherFeaturesCount}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="text-5xl font-bold w-12 text-center"
>
{state.otherFeaturesCount}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherFeaturesCount: state.otherFeaturesCount + 1 })}
className="w-14 h-14 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} />
</motion.button>
</div>
</div>
</motion.div>
</div>
</div>
);

View File

@@ -39,12 +39,12 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
{isWebApp ? 'Funktionale Anforderungen' : 'Erweiterte Funktionen'}
</h4>
</div>
<motion.button
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('functions')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('functions') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -77,25 +77,45 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
</>
) : (
<>
<Checkbox
label="Suche" desc="Volltextsuche über alle Inhalte."
checked={state.functions.includes('search')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'search') })}
<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="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="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') })}
<Checkbox
label="Erweiterte Formulare" desc="Komplexe Abfragen & Logik."
checked={state.functions.includes('forms')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'forms') })}
/>
<Checkbox
label="Mitgliederbereich" desc="Login-Bereich für exklusive Inhalte."
checked={state.functions.includes('members')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'members') })}
/>
<Checkbox
label="Event-Kalender" desc="Verwaltung und Anzeige von Terminen."
checked={state.functions.includes('calendar')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'calendar') })}
/>
<Checkbox
label="Mehrsprachigkeit" desc="Inhalte in mehreren Sprachen."
checked={state.functions.includes('multilang')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'multilang') })}
/>
<Checkbox
label="Echtzeit-Chat" desc="Direkte Kommunikation mit Besuchern."
checked={state.functions.includes('chat')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'chat') })}
/>
</>
)}
@@ -120,50 +140,6 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
/>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500"
>
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div>
<h4 className="text-xl font-bold text-slate-900">Anzahl weiterer Funktionen</h4>
<p className="text-base text-slate-500 mt-1">Falls Sie weitere Logik-Bausteine planen, diese aber noch nicht benennen können.</p>
</div>
<div className="flex items-center gap-8">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherFunctionsCount: Math.max(0, state.otherFunctionsCount - 1) })}
className="w-14 h-14 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} />
</motion.button>
<AnimatePresence mode="wait">
<motion.span
key={state.otherFunctionsCount}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="text-5xl font-bold w-12 text-center"
>
{state.otherFunctionsCount}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherFunctionsCount: state.otherFunctionsCount + 1 })}
className="w-14 h-14 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} />
</motion.button>
</div>
</div>
</motion.div>
</div>
</Reveal>
</div>

View File

@@ -52,13 +52,14 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
<Globe size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Sprachen</h4>
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('languages')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('languages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>

View File

@@ -42,6 +42,7 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
<Globe size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Bestehende Website</h4>
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
</div>
<Input
label="URL (falls vorhanden)"
@@ -74,61 +75,71 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
</div>
<Reveal width="100%" delay={0.3}>
<div className="space-y-8">
<div className="space-y-10">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Share2 size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Social Media Accounts</h4>
</div>
<p className="text-lg text-slate-500 ml-2">Welche Kanäle nutzen Sie bereits? Bitte geben Sie die URLs an.</p>
<div className="grid grid-cols-1 gap-4">
{SOCIAL_PLATFORMS.map((option, index) => {
const isSelected = state.socialMedia.includes(option.id);
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{SOCIAL_PLATFORMS.map((platform) => {
const isSelected = state.socialMedia.includes(platform.id);
return (
<motion.div
key={option.id}
className={`p-6 rounded-[2.5rem] border-2 transition-all duration-500 ${
isSelected ? 'border-slate-900 bg-slate-50' : 'border-slate-100 bg-white hover:border-slate-300'
<motion.button
key={platform.id}
whileHover={{ y: -4 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => updateState({ socialMedia: toggleItem(state.socialMedia, platform.id) })}
className={`flex flex-col items-center gap-3 p-6 rounded-[2rem] border-2 transition-all duration-300 ${
isSelected ? 'border-slate-900 bg-slate-900 text-white shadow-lg' : 'border-slate-100 bg-white text-slate-400 hover:border-slate-200'
}`}
>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<Checkbox
label={option.label}
desc=""
checked={isSelected}
onChange={() => updateState({ socialMedia: toggleItem(state.socialMedia, option.id) })}
/>
</div>
<AnimatePresence>
{isSelected && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="relative group/input pt-2">
<div className="absolute left-5 top-[calc(50%+4px)] -translate-y-1/2 text-black transition-colors">
<Link2 size={18} />
</div>
<input
type="url"
placeholder={`URL zu Ihrem ${option.label} Profil`}
value={state.socialMediaUrls[option.id] || ''}
onChange={(e) => updateUrl(option.id, e.target.value)}
className="w-full p-4 pl-14 bg-white border border-slate-200 rounded-2xl focus:outline-none focus:border-slate-900 transition-all duration-300 text-base"
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
<span className="font-bold text-sm">{platform.label}</span>
</motion.button>
);
})}
</div>
<div className="space-y-4">
<AnimatePresence mode="popLayout">
{state.socialMedia.map((id) => {
const platform = SOCIAL_PLATFORMS.find(p => p.id === id);
if (!platform) return null;
return (
<motion.div
key={id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
layout
className="relative group"
>
<div className="absolute left-6 top-1/2 -translate-y-1/2 flex items-center gap-3 text-slate-400 group-focus-within:text-slate-900 transition-colors">
<span className="font-bold text-xs uppercase tracking-widest w-20">{platform.label}</span>
<div className="w-[1px] h-4 bg-slate-200" />
<Link2 size={18} />
</div>
<input
type="url"
placeholder={`https://${platform.id}.com/ihr-profil`}
value={state.socialMediaUrls[id] || ''}
onChange={(e) => updateUrl(id, e.target.value)}
className="w-full p-6 pl-40 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg focus:shadow-xl"
/>
</motion.div>
);
})}
</AnimatePresence>
{state.socialMedia.length === 0 && (
<div className="p-12 border-2 border-dashed border-slate-100 rounded-[3rem] text-center">
<p className="text-slate-400 font-medium">Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.</p>
</div>
)}
</div>
</div>
</Reveal>
</div>

View File

@@ -10,6 +10,9 @@ interface TimelineStepProps {
}
export function TimelineStep({ state, updateState }: TimelineStepProps) {
const isMissingAssets = !state.assets.includes('logo') || !state.assets.includes('content_concept');
const isMissingPages = state.selectedPages.length === 0 && state.otherPages.length === 0 && state.otherPagesCount === 0;
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
@@ -24,10 +27,10 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
<div className="space-y-6">
<div className="flex justify-between items-center">
<h4 className="text-2xl font-bold text-slate-900">Zeitplan</h4>
<button
<button
type="button"
onClick={() => toggleDontKnow('timeline')}
className={`px-4 py-2 rounded-full text-sm font-bold transition-all ${
className={`px-4 py-2 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('timeline') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
@@ -63,6 +66,20 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
</p>
</div>
)}
{(isMissingAssets || isMissingPages) && (
<div className="p-8 bg-amber-50 rounded-[2rem] border border-amber-100 flex gap-6 items-start">
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shrink-0">
<AlertCircle size={24} />
</div>
<div className="space-y-2">
<p className="text-amber-900 text-xl font-bold">Mögliche Verzögerungen</p>
<p className="text-amber-800 text-base leading-relaxed">
Da noch wichtige Informationen (z.B. {isMissingAssets ? 'Logo/Inhaltskonzept' : ''} {isMissingAssets && isMissingPages ? 'und' : ''} {isMissingPages ? 'Seitenstruktur' : ''}) fehlen, kann sich der Projektstart verzögern, bis diese vorliegen.
</p>
</div>
</div>
)}
</div>
);
}

View File

@@ -29,7 +29,10 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
}`}
>
<div className={`transition-transform duration-500 group-hover:scale-110 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.illustration}</div>
<h4 className={`text-4xl font-bold mb-4 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<div className="flex items-center gap-4 mb-4">
<h4 className={`text-4xl font-bold ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<span className={`px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wider ${state.projectType === type.id ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-500'}`}>Wichtig</span>
</div>
<p className={`text-xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
{state.projectType === type.id && (

View File

@@ -22,9 +22,12 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
<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} className="text-black" /> Zielgruppe
</h4>
<div className="flex items-center gap-4">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Users size={24} className="text-black" /> Zielgruppe
</h4>
<span className="px-2 py-1 bg-slate-900 text-white text-[10px] font-bold uppercase tracking-wider rounded">Wichtig</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ id: 'internal', label: 'Internes Tool', desc: 'Für Mitarbeiter & Prozesse.' },

View File

@@ -56,6 +56,8 @@ export interface FormState {
platformType: string;
// Meta
dontKnows: string[];
visualStaging: string;
complexInteractions: string;
}
export interface Step {
@@ -63,4 +65,11 @@ export interface Step {
title: string;
description: string;
illustration: React.ReactNode;
chapter?: string;
}
export interface Chapter {
id: string;
title: string;
steps: string[];
}