form
Some checks failed
Build & Deploy Mintel Blog / build-and-deploy (push) Failing after 58s

This commit is contained in:
2026-01-30 17:48:13 +01:00
parent 1f57bae339
commit 8c1a7f6b5a
23 changed files with 1709 additions and 651 deletions

View File

@@ -1,72 +0,0 @@
pipeline:
# Test stage
test:
image: node:22-alpine
commands:
- npm ci
- npm run test:smoke
- npm run test:links
# Build stage
build:
image: node:22-alpine
commands:
- npm ci
- npm run build
when:
branch: [main, master]
event: [push, tag]
# Build and push Docker image
docker-build:
image: docker:24-dind
environment:
- DOCKER_HOST=tcp://docker:2375
commands:
- docker build -t mintel-website:${CI_COMMIT_SHA} -f docker/Dockerfile .
- docker tag mintel-website:${CI_COMMIT_SHA} ${REGISTRY_URL}:latest
when:
branch: [main, master]
event: [push, tag]
# Deploy to Hetzner
deploy:
image: appleboy/ssh-action
environment:
- SSH_KEY=${SSH_PRIVATE_KEY}
commands:
- echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
- ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key ${DEPLOY_USER}@${DEPLOY_HOST} "
cd /opt/mintel &&
echo '${REGISTRY_URL}:latest' > .env.deploy &&
docker-compose pull &&
docker-compose up -d --remove-orphans &&
docker system prune -f"
when:
branch: [main, master]
event: [push]
# Services
services:
docker:
image: docker:24-dind
privileged: true
environment:
- DOCKER_TLS_CERTDIR=/certs
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Notifications
notify:
- name: slack
image: plugins/slack
settings:
webhook: ${SLACK_WEBHOOK}
channel: deployments
template: |
🚀 Mintel Blog Deployed!
Branch: {{CI_COMMIT_BRANCH}}
Commit: {{CI_COMMIT_SHA}}
Author: {{CI_COMMIT_AUTHOR}}
URL: https://mintel.me

View File

@@ -1,63 +0,0 @@
#!/bin/bash
# Simple deployment script for Hetzner
set -e
# Configuration
DOMAIN=${DOMAIN:-mintel.me}
ADMIN_EMAIL=${ADMIN_EMAIL:-marc@mintel.me}
DEPLOY_HOST=${DEPLOY_HOST:-$1}
DEPLOY_USER=${DEPLOY_USER:-root}
if [ -z "$DEPLOY_HOST" ]; then
echo "Usage: ./deploy.sh <host-ip> or set DEPLOY_HOST env var"
exit 1
fi
echo "🚀 Deploying Mintel Blog to $DEPLOY_HOST..."
# Create deployment directory
ssh $DEPLOY_USER@$DEPLOY_HOST "mkdir -p /opt/mintel"
# Copy files
echo "📦 Copying files..."
scp docker-compose.yml docker/Caddyfile docker/Dockerfile docker/nginx.conf $DEPLOY_USER@$DEPLOY_HOST:/opt/mintel/
# Create environment file
echo "🔧 Setting up environment..."
if [ -f .env ]; then
echo "Using local .env file..."
scp .env $DEPLOY_USER@$DEPLOY_HOST:/opt/mintel/
else
echo "Creating .env from variables..."
ssh $DEPLOY_USER@$DEPLOY_HOST "cat > /opt/mintel/.env << EOF
DOMAIN=${DOMAIN:-mintel.me}
ADMIN_EMAIL=${ADMIN_EMAIL:-admin@mintel.me}
REDIS_URL=${REDIS_URL:-redis://redis:6379}
PLAUSIBLE_DOMAIN=${PLAUSIBLE_DOMAIN:-$DOMAIN}
PLAUSIBLE_SCRIPT_URL=${PLAUSIBLE_SCRIPT_URL:-https://plausible.yourdomain.com/js/script.js}
EOF"
fi
# Deploy
echo "🚀 Starting services..."
ssh $DEPLOY_USER@$DEPLOY_HOST "
cd /opt/mintel &&
docker-compose down &&
docker-compose pull &&
docker-compose up -d --build &&
docker system prune -f
"
echo "✅ Deployment complete!"
echo "🌐 Website: https://$DOMAIN"
echo "📊 Health check: https://$DOMAIN/health"
# Wait for health check
echo "⏳ Waiting for health check..."
sleep 10
if curl -f https://$DOMAIN/health > /dev/null 2>&1; then
echo "✅ Health check passed!"
else
echo "⚠️ Health check failed. Check logs with: ssh $DEPLOY_USER@$DEPLOY_HOST 'docker logs mintel-website'"
fi

View File

@@ -3,6 +3,7 @@ version: '3.8'
services:
# Main website - Next.js standalone
website:
image: registry.infra.mintel.me/mintel/mintel.me:latest
build:
context: .
dockerfile: docker/Dockerfile
@@ -15,8 +16,10 @@ services:
- NEXT_PUBLIC_GLITCHTIP_DSN=${NEXT_PUBLIC_GLITCHTIP_DSN}
container_name: mintel-website
restart: unless-stopped
ports:
- "3000:3000"
# Port 3000 is internal to the docker network, Caddy will proxy to it.
# We can expose it for debugging if needed, but it's safer to keep it internal.
expose:
- "3000"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379

View File

@@ -7,7 +7,7 @@
# Main website
{$DOMAIN:-localhost} {
# Reverse proxy to website container
reverse_proxy website:80
reverse_proxy website:3000
# Security headers
header {

View File

@@ -4,16 +4,18 @@ import * as React from 'react';
import { useState, useMemo, useEffect, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronRight, ChevronLeft, Send, Check, Share2 } from 'lucide-react';
import { ChevronRight, ChevronLeft, Send, Check } from 'lucide-react';
import * as QRCode from 'qrcode';
import { FormState, Step, ProjectType } from './ContactForm/types';
import { FormState, Step } from './ContactForm/types';
import { PRICING, initialState } from './ContactForm/constants';
import { PriceCalculation } from './ContactForm/components/PriceCalculation';
import { ShareModal } from './ShareModal';
// Steps
import { TypeStep } from './ContactForm/steps/TypeStep';
import { CompanyStep } from './ContactForm/steps/CompanyStep';
import { PresenceStep } from './ContactForm/steps/PresenceStep';
import { BaseStep } from './ContactForm/steps/BaseStep';
import { FeaturesStep } from './ContactForm/steps/FeaturesStep';
import { DesignStep } from './ContactForm/steps/DesignStep';
@@ -77,19 +79,41 @@ export function ContactForm() {
const configData = {
projectType: state.projectType,
companyName: state.companyName,
employeeCount: state.employeeCount,
existingWebsite: state.existingWebsite,
socialMedia: state.socialMedia,
socialMediaUrls: state.socialMediaUrls,
existingDomain: state.existingDomain,
wishedDomain: state.wishedDomain,
websiteTopic: state.websiteTopic,
selectedPages: state.selectedPages,
otherPages: state.otherPages,
otherPagesCount: state.otherPagesCount,
features: state.features,
otherFeatures: state.otherFeatures,
otherFeaturesCount: state.otherFeaturesCount,
functions: state.functions,
otherFunctions: state.otherFunctions,
otherFunctionsCount: state.otherFunctionsCount,
apiSystems: state.apiSystems,
otherTech: state.otherTech,
otherTechCount: state.otherTechCount,
assets: state.assets,
otherAssets: state.otherAssets,
otherAssetsCount: state.otherAssetsCount,
cmsSetup: state.cmsSetup,
languagesCount: state.languagesCount,
languagesList: state.languagesList,
deadline: state.deadline,
designVibe: state.designVibe,
colorScheme: state.colorScheme,
targetAudience: state.targetAudience,
userRoles: state.userRoles,
dataSensitivity: state.dataSensitivity,
platformType: state.platformType
platformType: state.platformType,
dontKnows: state.dontKnows,
visualStaging: state.visualStaging,
complexInteractions: state.complexInteractions
};
const stateString = btoa(unescape(encodeURIComponent(JSON.stringify(configData))));
@@ -106,28 +130,28 @@ export function ContactForm() {
}, [currentUrl]);
const totalPagesCount = useMemo(() => {
return state.selectedPages.length + state.otherPages.length;
}, [state.selectedPages, state.otherPages]);
return state.selectedPages.length + state.otherPages.length + (state.otherPagesCount || 0);
}, [state.selectedPages, state.otherPages, state.otherPagesCount]);
const totalPrice = useMemo(() => {
if (state.projectType !== 'website') return 0;
let total = PRICING.BASE_WEBSITE;
total += totalPagesCount * PRICING.PAGE;
total += (state.features.length + state.otherFeatures.length) * PRICING.FEATURE;
total += (state.functions.length + state.otherFunctions.length) * PRICING.FUNCTION;
total += state.complexInteractions * PRICING.COMPLEX_INTERACTION;
total += state.newDatasets * PRICING.NEW_DATASET;
total += (state.apiSystems.length + state.otherTech.length) * PRICING.API_INTEGRATION;
total += (state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0)) * PRICING.FEATURE;
total += (state.functions.length + state.otherFunctions.length + (state.otherFunctionsCount || 0)) * PRICING.FUNCTION;
total += (state.apiSystems.length + state.otherTech.length + (state.otherTechCount || 0)) * PRICING.API_INTEGRATION;
total += (state.newDatasets || 0) * PRICING.NEW_DATASET;
if (state.cmsSetup) {
total += PRICING.CMS_SETUP;
total += (state.features.length + state.otherFeatures.length) * PRICING.CMS_CONNECTION_PER_FEATURE;
total += (state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0)) * PRICING.CMS_CONNECTION_PER_FEATURE;
}
// Multi-language factor (e.g. +20% per additional language)
if (state.languagesCount > 1) {
total *= (1 + (state.languagesCount - 1) * 0.2);
const languagesCount = state.languagesList.length || 1;
if (languagesCount > 1) {
total *= (1 + (languagesCount - 1) * 0.2);
}
return Math.round(total);
@@ -177,8 +201,10 @@ 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: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: <ConceptWebsite 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" /> },
@@ -197,6 +223,8 @@ export function ContactForm() {
// Web App flow
return [
steps.find(s => s.id === 'type')!,
steps.find(s => s.id === 'company')!,
steps.find(s => s.id === 'presence')!,
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.' },
@@ -214,6 +242,10 @@ export function ContactForm() {
switch (currentStep.id) {
case 'type':
return <TypeStep state={state} updateState={updateState} />;
case 'company':
return <CompanyStep state={state} updateState={updateState} />;
case 'presence':
return <PresenceStep state={state} updateState={updateState} toggleItem={toggleItem} />;
case 'base':
return <BaseStep state={state} updateState={updateState} toggleItem={toggleItem} />;
case 'features':
@@ -287,11 +319,11 @@ export function ContactForm() {
<p className="text-xl text-slate-500 leading-relaxed">{activeSteps[stepIndex].description}</p>
</div>
</div>
<div className="flex gap-3 h-1.5">
<div className="flex gap-3 h-4">
{activeSteps.map((step, i) => (
<div
key={i}
className="flex-1 h-8 -my-3.5 flex items-center relative"
className="flex-1 h-full flex items-center relative"
onMouseEnter={() => setHoveredStep(i)}
onMouseLeave={() => setHoveredStep(null)}
>
@@ -301,7 +333,7 @@ export function ContactForm() {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`w-full h-1.5 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' : 'bg-slate-100'} cursor-pointer focus:outline-none p-0 border-none`}
/>
<AnimatePresence>
{hoveredStep === i && (
@@ -309,7 +341,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-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"
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" />

View File

@@ -15,16 +15,16 @@ 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 ${
className={`w-full p-5 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'
}`}
>
<div className={`mt-1 w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 ${checked ? 'border-white bg-white text-slate-900' : 'border-slate-200'}`}>
{checked && <Check size={14} strokeWidth={4} />}
</div>
<div>
<div className="flex-grow">
<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>
{desc && <p className={`text-base leading-relaxed ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>}
</div>
</button>
);

View File

@@ -34,6 +34,11 @@ export function PriceCalculation({
qrCodeData,
onShare
}: PriceCalculationProps) {
const totalPages = totalPagesCount + (state.otherPagesCount || 0);
const totalFeatures = state.features.length + state.otherFeatures.length + (state.otherFeaturesCount || 0);
const totalFunctions = state.functions.length + state.otherFunctions.length + (state.otherFunctionsCount || 0);
const totalApis = state.apiSystems.length + state.otherTech.length + (state.otherTechCount || 0);
return (
<div className="lg:col-span-4 lg:sticky lg:top-24">
<div className="p-10 bg-slate-50 border border-slate-100 rounded-[3rem] space-y-10">
@@ -43,12 +48,13 @@ export function PriceCalculation({
<>
<div className="flex justify-between items-center py-4 border-b border-slate-200"><span className="text-slate-600 font-medium">Basis Website</span><span className="font-bold text-lg text-slate-900">{PRICING.BASE_WEBSITE.toLocaleString()} </span></div>
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2 hide-scrollbar">
{totalPagesCount > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalPagesCount}x Seite</span><span className="font-medium text-slate-900">{(totalPagesCount * PRICING.PAGE).toLocaleString()} </span></div>)}
{state.features.length + state.otherFeatures.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.features.length + state.otherFeatures.length}x System-Modul</span><span className="font-medium text-slate-900">{((state.features.length + state.otherFeatures.length) * PRICING.FEATURE).toLocaleString()} </span></div>)}
{state.functions.length + state.otherFunctions.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.functions.length + state.otherFunctions.length}x Logik-Funktion</span><span className="font-medium text-slate-900">{((state.functions.length + state.otherFunctions.length) * PRICING.FUNCTION).toLocaleString()} </span></div>)}
{state.complexInteractions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.complexInteractions}x Komplexes UI/Animation</span><span className="font-medium text-slate-900">{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} </span></div>)}
{state.apiSystems.length + state.otherTech.length > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.apiSystems.length + state.otherTech.length}x API Sync</span><span className="font-medium text-slate-900">{((state.apiSystems.length + state.otherTech.length) * PRICING.API_INTEGRATION).toLocaleString()} </span></div>)}
{state.cmsSetup && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">CMS Setup & Anbindung</span><span className="font-medium text-slate-900">{(PRICING.CMS_SETUP + (state.features.length + state.otherFeatures.length) * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} </span></div>)}
{totalPages > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalPages}x Seite</span><span className="font-medium text-slate-900">{(totalPages * PRICING.PAGE).toLocaleString()} </span></div>)}
{totalFeatures > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFeatures}x System-Modul</span><span className="font-medium text-slate-900">{(totalFeatures * PRICING.FEATURE).toLocaleString()} </span></div>)}
{totalFunctions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalFunctions}x Logik-Funktion</span><span className="font-medium text-slate-900">{(totalFunctions * PRICING.FUNCTION).toLocaleString()} </span></div>)}
{state.visualStaging > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.visualStaging}x Visuelle Inszenierung</span><span className="font-medium text-slate-900">{(state.visualStaging * PRICING.VISUAL_STAGING).toLocaleString()} </span></div>)}
{state.complexInteractions > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.complexInteractions}x Komplexe Interaktion</span><span className="font-medium text-slate-900">{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} </span></div>)}
{totalApis > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{totalApis}x API Sync</span><span className="font-medium text-slate-900">{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} </span></div>)}
{state.cmsSetup && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">CMS Setup & Anbindung</span><span className="font-medium text-slate-900">{(PRICING.CMS_SETUP + totalFeatures * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} </span></div>)}
{state.newDatasets > 0 && (<div className="flex justify-between items-center text-sm"><span className="text-slate-500">{state.newDatasets}x Inhalte einpflegen</span><span className="font-medium text-slate-900">{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} </span></div>)}
{state.languagesCount > 1 && (<div className="flex justify-between items-center text-sm text-slate-900 font-bold pt-2 border-t border-slate-100"><span className="text-slate-500">Mehrsprachigkeit ({state.languagesCount}x)</span><span>+{(totalPrice - (totalPrice / (1 + (state.languagesCount - 1) * 0.2))).toLocaleString()} </span></div>)}
</div>

View File

@@ -6,7 +6,6 @@ export const PRICING = {
PAGE: 800,
FEATURE: 2000,
FUNCTION: 1000,
COMPLEX_INTERACTION: 1500,
NEW_DATASET: 400,
HOSTING_MONTHLY: 120,
STORAGE_EXPANSION_MONTHLY: 10,
@@ -18,17 +17,32 @@ export const PRICING = {
export const initialState: FormState = {
projectType: 'website',
// Company
companyName: '',
employeeCount: '',
// Existing Presence
existingWebsite: '',
socialMedia: [],
socialMediaUrls: {},
existingDomain: '',
wishedDomain: '',
// Project
websiteTopic: '',
selectedPages: ['Home'],
otherPages: [],
otherPagesCount: 0,
features: [],
otherFeatures: [],
otherFeaturesCount: 0,
functions: [],
otherFunctions: [],
otherFunctionsCount: 0,
apiSystems: [],
otherTech: [],
otherTechCount: 0,
assets: [],
otherAssets: [],
complexInteractions: 0,
otherAssetsCount: 0,
newDatasets: 0,
cmsSetup: false,
storageExpansion: 0,
@@ -38,17 +52,23 @@ export const initialState: FormState = {
message: '',
sitemapFile: null,
contactFiles: [],
// Design
designVibe: 'minimal',
colorScheme: ['#ffffff', '#f8fafc', '#0f172a'],
references: [],
designWishes: '',
// Maintenance
expectedAdjustments: 'low',
languagesCount: 1,
languagesList: ['Deutsch'],
// Timeline
deadline: 'flexible',
// Web App specific
targetAudience: 'internal',
userRoles: [],
dataSensitivity: 'standard',
platformType: 'web-only',
// Meta
dontKnows: [],
};
export const PAGE_SAMPLES = [
@@ -85,9 +105,13 @@ export const API_OPTIONS = [
{ id: 'realestate', label: 'Immobilien', desc: 'OpenImmo, FlowFact, Immowelt Sync.' },
{ id: 'calendar', label: 'Termine / Booking', desc: 'Calendly, Shore, Doctolib etc.' },
{ id: 'social', label: 'Social Media Sync', desc: 'Automatisierte Posts oder Feeds.' },
{ id: '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 = [
{ id: 'existing_website', label: 'Bestehende Website', desc: 'Inhalte oder Struktur können übernommen werden.' },
{ id: 'logo', label: 'Logo', desc: 'Vektordatei Ihres Logos.' },
{ id: 'styleguide', label: 'Styleguide', desc: 'Farben, Schriften, Design-Vorgaben.' },
{ id: 'content_concept', label: 'Inhalts-Konzept', desc: 'Struktur und Texte sind bereits geplant.' },
@@ -146,13 +170,18 @@ export const DESIGN_VIBES = [
},
];
export const HARMONIOUS_PALETTES = [
['#ffffff', '#f8fafc', '#0f172a'],
['#000000', '#facc15', '#ffffff'],
['#fdfcfb', '#e2e8f0', '#1e293b'],
['#0f172a', '#38bdf8', '#ffffff'],
['#fafaf9', '#78716c', '#1c1917'],
['#f0fdf4', '#16a34a', '#064e3b'],
['#fff7ed', '#ea580c', '#7c2d12'],
['#f5f3ff', '#7c3aed', '#2e1065'],
export const EMPLOYEE_OPTIONS = [
{ id: '1-5', label: '1-5 Mitarbeiter' },
{ id: '6-20', label: '6-20 Mitarbeiter' },
{ id: '21-100', label: '21-100 Mitarbeiter' },
{ id: '100+', label: '100+ Mitarbeiter' },
];
export const SOCIAL_MEDIA_OPTIONS = [
{ id: 'instagram', label: 'Instagram' },
{ id: 'linkedin', label: 'LinkedIn' },
{ id: 'facebook', label: 'Facebook' },
{ id: 'twitter', label: 'Twitter / X' },
{ id: 'tiktok', label: 'TikTok' },
{ id: 'youtube', label: 'YouTube' },
];

View File

@@ -4,6 +4,9 @@ import * as React from 'react';
import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Minus, Plus, Share2, ListPlus } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface ApiStepProps {
state: FormState;
@@ -14,50 +17,134 @@ interface ApiStepProps {
export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
const isWebApp = state.projectType === 'web-app';
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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') })}
/>
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<Share2 size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
</h4>
</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 shadow-sm ${
state.dontKnows?.includes('api') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<p className="text-lg text-slate-500 leading-relaxed ml-2">
{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">
{[
{ id: 'crm_erp', label: 'CRM / ERP', desc: 'HubSpot, Salesforce, SAP, Xentral etc.' },
{ id: 'payment', label: 'Payment', desc: 'Stripe, PayPal, Klarna Integration.' },
{ 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}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Checkbox
label={opt.label} desc={opt.desc}
checked={state.apiSystems.includes(opt.id)}
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, opt.id) })}
/>
</motion.div>
))}
</div>
</div>
</div>
</Reveal>
<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) => updateTech(i)}
placeholder="z.B. Microsoft Graph, Google Maps, Custom REST API..."
/>
</div>
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<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 shadow-sm">
<ListPlus size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Weitere Systeme oder eigene APIs?</h4>
</div>
<RepeatableList
items={state.otherTech}
onAdd={(v) => updateState({ otherTech: [...state.otherTech, v] })}
onRemove={(i) => updateTech(i)}
placeholder="z.B. Microsoft Graph, Google Search Console, Custom REST API..."
/>
</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 shadow-sm 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 Schnittstellen</h4>
<p className="text-base text-slate-500 mt-1">Falls Sie weitere Integrationen 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({ otherTechCount: Math.max(0, state.otherTechCount - 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.otherTechCount}
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.otherTechCount}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherTechCount: state.otherTechCount + 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

@@ -5,6 +5,9 @@ import { FormState } from '../types';
import { ASSET_OPTIONS } from '../constants';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, Briefcase, ListPlus } from 'lucide-react';
import { Reveal } from '../../Reveal';
interface AssetsStepProps {
state: FormState;
@@ -13,26 +16,120 @@ interface AssetsStepProps {
}
export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) {
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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}
checked={state.assets.includes(opt.id)}
onChange={() => updateState({ assets: toggleItem(state.assets, opt.id) })}
/>
))}
</div>
<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. Lastenheft, Wireframes, Bilddatenbank..."
/>
</div>
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<Briefcase size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Vorhandene Assets</h4>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('assets')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${
state.dontKnows?.includes('assets') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{ASSET_OPTIONS.map((opt, index) => (
<motion.div
key={opt.id}
initial={{ opacity: 0, y: 10 }}
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) })}
/>
</motion.div>
))}
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<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 shadow-sm">
<ListPlus size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Weitere vorhandene Unterlagen?</h4>
</div>
<RepeatableList
items={state.otherAssets}
onAdd={(v) => updateState({ otherAssets: [...state.otherAssets, v] })}
onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })}
placeholder="z.B. Lastenheft, Wireframes, Bilddatenbank..."
/>
</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 shadow-sm 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 Assets</h4>
<p className="text-base text-slate-500 mt-1">Falls Sie weitere Unterlagen haben, 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({ otherAssetsCount: Math.max(0, state.otherAssetsCount - 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.otherAssetsCount}
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.otherAssetsCount}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherAssetsCount: state.otherAssetsCount + 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

@@ -4,6 +4,8 @@ import * as React from 'react';
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';
interface BaseStepProps {
state: FormState;
@@ -12,32 +14,137 @@ interface BaseStepProps {
}
export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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={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="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 className="space-y-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
>
<h4 className="text-2xl font-bold text-slate-900">Thema der Website</h4>
<input
type="text"
placeholder="z.B. Portfolio für Architektur, Onlineshop für Bio-Tee..."
value={state.websiteTopic}
onChange={(e) => updateState({ websiteTopic: e.target.value })}
className="w-full p-10 bg-white border border-slate-100 rounded-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</motion.div>
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<FileText size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Die Seiten</h4>
</div>
<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 shadow-sm ${
state.dontKnows?.includes('pages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<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, index) => (
<motion.div
key={opt.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Checkbox
label={opt.label} desc={opt.desc}
checked={state.selectedPages.includes(opt.id)}
onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })}
/>
</motion.div>
))}
</div>
</div>
<div className="space-y-12">
<div className="space-y-8">
<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 shadow-sm">
<ListPlus size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Weitere individuelle Seiten?</h4>
</div>
<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>
<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 shadow-sm 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 Seiten</h4>
<p className="text-base text-slate-500 mt-1">Falls Sie die Namen noch nicht wissen, aber die Menge schätzen 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({ otherPagesCount: Math.max(0, state.otherPagesCount - 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.otherPagesCount}
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.otherPagesCount}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => updateState({ otherPagesCount: state.otherPagesCount + 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

@@ -0,0 +1,67 @@
'use client';
import * as React from 'react';
import { FormState } from '../types';
import { EMPLOYEE_OPTIONS } from '../constants';
import { motion } from 'framer-motion';
import { Building2, Users } from 'lucide-react';
import { Reveal } from '../../Reveal';
interface CompanyStepProps {
state: FormState;
updateState: (updates: Partial<FormState>) => void;
}
export function CompanyStep({ state, updateState }: CompanyStepProps) {
return (
<div className="space-y-16">
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<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 shadow-sm">
<Building2 size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Unternehmen</h4>
</div>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Name des Unternehmens</label>
<input
type="text"
placeholder="z.B. Muster GmbH"
value={state.companyName}
onChange={(e) => updateState({ companyName: e.target.value })}
className="w-full p-8 bg-white border border-slate-100 rounded-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.2}>
<div className="space-y-8">
<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 shadow-sm">
<Users size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Mitarbeiteranzahl</h4>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{EMPLOYEE_OPTIONS.map((option, index) => (
<motion.button
key={option.id}
whileHover={{ y: -5, boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => updateState({ employeeCount: option.id })}
className={`p-6 rounded-[2rem] border-2 transition-all duration-300 font-bold text-lg ${
state.employeeCount === option.id ? 'border-slate-900 bg-slate-900 text-white shadow-lg' : 'border-slate-100 bg-white hover:border-slate-300 text-slate-600'
}`}
>
{option.label}
</motion.button>
))}
</div>
</div>
</Reveal>
</div>
);
}

View File

@@ -2,7 +2,9 @@
import * as React from 'react';
import { FormState } from '../types';
import { FileText, Upload, X } from 'lucide-react';
import { FileText, Upload, X, User, Mail, Briefcase, MessageSquare } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface ContactStepProps {
state: FormState;
@@ -11,93 +13,166 @@ interface ContactStepProps {
export function ContactStep({ state, updateState }: ContactStepProps) {
return (
<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 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-6">
<p className="text-lg font-bold text-slate-900">Dateien hochladen (optional)</p>
<div
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(); }}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
}}
onClick={() => document.getElementById('contact-upload')?.click()}
>
<input id="contact-upload" type="file" multiple className="hidden" onChange={(e) => {
const files = e.target.files ? Array.from(e.target.files) : [];
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
}} />
{state.contactFiles.length > 0 ? (
<div className="w-full space-y-3">
{state.contactFiles.map((file, idx) => (
<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"
onClick={(e) => {
e.stopPropagation();
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
}}
className="p-2 hover:bg-slate-100 rounded-full transition-colors focus:outline-none"
>
<X size={20} />
</button>
</div>
))}
<p className="text-xs text-slate-400 text-center mt-6">Klicken oder ziehen, um weitere Dateien hinzuzufügen</p>
</div>
) : (
<>
<Upload size={48} className="text-slate-400 group-hover:text-slate-900 transition-colors" />
<div className="text-center">
<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 className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Reveal width="100%" delay={0.1}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Ihr Name</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-slate-900 transition-colors">
<User size={24} />
</div>
</>
)}
</div>
<input
type="text"
placeholder="Max Mustermann"
required
value={state.name}
onChange={(e) => updateState({ name: e.target.value })}
className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.1}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Ihre Email</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-slate-900 transition-colors">
<Mail size={24} />
</div>
<input
type="email"
placeholder="max@beispiel.de"
required
value={state.email}
onChange={(e) => updateState({ email: e.target.value })}
className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</Reveal>
</div>
<Reveal width="100%" delay={0.2}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Ihre Rolle</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-slate-900 transition-colors">
<Briefcase size={24} />
</div>
<input
type="text"
placeholder="z.B. CEO, Marketing Manager..."
value={state.role}
onChange={(e) => updateState({ role: e.target.value })}
className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.3}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Nachricht</label>
<div className="relative group">
<div className="absolute left-6 top-10 text-slate-300 group-focus-within:text-slate-900 transition-colors">
<MessageSquare size={24} />
</div>
<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 pl-16 bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 resize-none text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</Reveal>
<Reveal width="100%" delay={0.4}>
<div className="space-y-6">
<p className="text-lg font-bold text-slate-900 ml-2">Dateien hochladen (optional)</p>
<div
className={`relative group border-2 border-dashed rounded-[3rem] p-12 transition-all duration-500 flex flex-col items-center justify-center gap-6 cursor-pointer min-h-[250px] ${
state.contactFiles.length > 0 ? 'border-slate-900 bg-slate-50 shadow-inner' : 'border-slate-200 hover:border-slate-400 bg-white hover:shadow-xl'
}`}
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
}}
onClick={() => document.getElementById('contact-upload')?.click()}
>
<input id="contact-upload" type="file" multiple className="hidden" onChange={(e) => {
const files = e.target.files ? Array.from(e.target.files) : [];
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
}} />
<AnimatePresence mode="wait">
{state.contactFiles.length > 0 ? (
<motion.div
key="files"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="w-full space-y-4"
>
{state.contactFiles.map((file, idx) => (
<motion.div
key={`${file.name}-${idx}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className="flex items-center justify-between p-5 bg-white border border-slate-100 rounded-2xl shadow-sm group/file"
>
<div className="flex items-center gap-4 text-slate-900">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-slate-400 group-hover/file:text-slate-900 transition-colors">
<FileText size={20} />
</div>
<div className="flex flex-col">
<span className="font-bold text-base truncate max-w-[250px]">{file.name}</span>
<span className="text-[10px] text-slate-400 uppercase font-bold">{(file.size / 1024 / 1024).toFixed(2)} MB</span>
</div>
</div>
<motion.button
whileHover={{ scale: 1.1, backgroundColor: '#fee2e2', color: '#ef4444' }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={(e) => {
e.stopPropagation();
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
}}
className="p-2 bg-slate-50 text-slate-400 rounded-full transition-colors focus:outline-none"
>
<X size={20} />
</motion.button>
</motion.div>
))}
<p className="text-xs text-slate-400 text-center mt-8 font-medium">Klicken oder ziehen, um weitere Dateien hinzuzufügen</p>
</motion.div>
) : (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-6"
>
<div className="w-20 h-20 bg-slate-50 rounded-[2rem] flex items-center justify-center text-slate-400 group-hover:text-slate-900 group-hover:scale-110 transition-all duration-500">
<Upload size={32} />
</div>
<div className="text-center">
<p className="text-xl font-bold text-slate-900">Dateien hierher ziehen</p>
<p className="text-lg text-slate-500 mt-1">oder klicken zum Auswählen</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</Reveal>
</div>
);
}

View File

@@ -2,7 +2,9 @@
import * as React from 'react';
import { FormState } from '../types';
import { Zap, AlertCircle, Minus, Plus } from 'lucide-react';
import { Zap, AlertCircle, Minus, Plus, Settings2, BarChart3 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface ContentStepProps {
state: FormState;
@@ -10,74 +12,167 @@ interface ContentStepProps {
}
export function ContentStep({ state, updateState }: ContentStepProps) {
return (
<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-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-20 h-11 rounded-full transition-colors relative focus:outline-none ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`}
>
<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>
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
<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.' },
{ id: 'high', label: 'Häufig', desc: 'Wöchentlich oder täglich.' },
].map(opt => (
<button
key={opt.id}
return (
<div className="space-y-16">
<Reveal width="100%" delay={0.1}>
<div className="flex flex-col md:flex-row items-center justify-between p-10 bg-white border border-slate-100 rounded-[3rem] shadow-sm gap-8">
<div className="max-w-2xl space-y-4">
<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 shadow-inner">
<Settings2 size={24} />
</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>
</div>
<div className="flex flex-col items-center md:items-end gap-6">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => updateState({ expectedAdjustments: opt.id })}
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'
onClick={() => toggleDontKnow('cms')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${
state.dontKnows?.includes('cms') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
<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>
Ich weiß es nicht
</motion.button>
<button
type="button"
onClick={() => updateState({ cmsSetup: !state.cmsSetup })}
className={`w-24 h-12 rounded-full transition-all duration-500 relative focus:outline-none shadow-inner ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`}
>
<motion.div
animate={{ x: state.cmsSetup ? 48 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="absolute top-1.5 left-1.5 w-9 h-9 bg-white rounded-full shadow-lg"
/>
</button>
))}
</div>
<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-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-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-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>
</Reveal>
<div className="flex flex-col gap-6 p-10 bg-white border border-slate-100 rounded-[3rem]">
<div>
<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>
<Reveal width="100%" delay={0.2}>
<div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-10">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-slate-900 shadow-sm">
<BarChart3 size={24} />
</div>
<p className="text-xl font-bold text-slate-900">Wie oft ändern sich Ihre Inhalte?</p>
</div>
<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.' },
{ id: 'high', label: 'Häufig', desc: 'Wöchentlich oder täglich.' },
].map((opt, index) => (
<motion.button
key={opt.id}
whileHover={{ y: -5, boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}
whileTap={{ scale: 0.98 }}
type="button"
onClick={() => updateState({ expectedAdjustments: opt.id })}
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none ${
state.expectedAdjustments === opt.id ? 'border-slate-900 bg-slate-900 text-white shadow-xl' : 'border-slate-200 bg-white hover:border-slate-400'
}`}
>
<p className={`font-bold text-lg ${state.expectedAdjustments === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</p>
<p className={`text-sm mt-2 leading-relaxed ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
</motion.button>
))}
</div>
<AnimatePresence>
{state.expectedAdjustments === 'high' && !state.cmsSetup && (
<motion.div
initial={{ opacity: 0, height: 0, y: 20 }}
animate={{ opacity: 1, height: 'auto', y: 0 }}
exit={{ opacity: 0, height: 0, y: 20 }}
className="p-8 bg-amber-50 rounded-[2.5rem] border border-amber-100 flex gap-6 items-start shadow-sm"
>
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shadow-sm shrink-0">
<AlertCircle size={24} />
</div>
<div className="space-y-2">
<p className="text-amber-900 text-xl font-bold">Empfehlung: CMS nutzen</p>
<p className="text-amber-800 text-base leading-relaxed max-w-3xl">
Bei täglichen oder wöchentlichen Änderungen sparen Sie mit einem CMS langfristig viel Geld, da Sie keine externen Entwickler für Inhalts-Updates benötigen.
</p>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
<div className="p-8 bg-white rounded-[2.5rem] border border-slate-100 space-y-4 shadow-sm">
<div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-[0.2em]">
<Zap size={18} /> Vorteil CMS
</div>
<p className="text-base 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-8 bg-white rounded-[2.5rem] border border-slate-100 space-y-4 shadow-sm">
<div className="flex items-center gap-3 text-slate-900 font-bold text-sm uppercase tracking-[0.2em]">
<AlertCircle size={18} /> Fokus Design
</div>
<p className="text-base 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 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>
</Reveal>
<Reveal width="100%" delay={0.3}>
<div className="flex flex-col gap-8 p-10 bg-white border border-slate-100 rounded-[3rem] shadow-sm">
<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>
</div>
<div className="flex items-center gap-12 py-2">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
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 shadow-sm"
>
<Minus size={28} />
</motion.button>
<AnimatePresence mode="wait">
<motion.span
key={state.newDatasets}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="text-7xl font-bold w-20 text-center tabular-nums"
>
{state.newDatasets}
</motion.span>
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
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 shadow-sm"
>
<Plus size={28} />
</motion.button>
</div>
</div>
</div>
</Reveal>
</div>
);
}

View File

@@ -2,8 +2,10 @@
import * as React from 'react';
import { FormState } from '../types';
import { DESIGN_VIBES, HARMONIOUS_PALETTES } from '../constants';
import { motion } from 'framer-motion';
import { DESIGN_VIBES } from '../constants';
import { motion, AnimatePresence } from 'framer-motion';
import { Plus, X, Palette, Pipette, RefreshCw } from 'lucide-react';
import { Reveal } from '../../Reveal';
interface DesignStepProps {
state: FormState;
@@ -11,60 +13,225 @@ interface DesignStepProps {
}
export function DesignStep({ state, updateState }: DesignStepProps) {
return (
<div className="space-y-12">
<div className="space-y-6">
<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-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={`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>
const addColor = () => {
if (state.colorScheme.length < 5) {
updateState({ colorScheme: [...state.colorScheme, '#000000'] });
}
};
<div className="space-y-6">
<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}
const removeColor = (index: number) => {
if (state.colorScheme.length > 1) {
const newScheme = [...state.colorScheme];
newScheme.splice(index, 1);
updateState({ colorScheme: newScheme });
}
};
const updateColor = (index: number, value: string) => {
const newScheme = [...state.colorScheme];
newScheme[index] = value;
updateState({ colorScheme: newScheme });
};
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
const generateHarmonicPalette = () => {
const hue = Math.floor(Math.random() * 360);
const saturation = 40 + Math.floor(Math.random() * 40);
const lightness = 40 + Math.floor(Math.random() * 40);
const hslToHex = (h: number, s: number, l: number) => {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
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
];
updateState({ colorScheme: palette });
};
return (
<div className="space-y-16">
{/* Design Vibe */}
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Design-Richtung</h4>
<p className="text-slate-500">Welche Ästhetik passt zu Ihrer Marke?</p>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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'
onClick={() => toggleDontKnow('design_vibe')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${
state.dontKnows?.includes('design_vibe') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
<div className="flex h-12 w-full rounded-lg overflow-hidden">
{palette.map((color, j) => (
Ich weiß es nicht
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{DESIGN_VIBES.map((vibe, index) => (
<motion.button
key={vibe.id}
whileHover={{ y: -5, boxShadow: '0 20px 25px -5px rgb(0 0 0 / 0.1)' }}
type="button"
onClick={() => updateState({ designVibe: vibe.id })}
className={`p-8 rounded-[2.5rem] border-2 text-left transition-all duration-500 focus:outline-none overflow-hidden relative group ${
state.designVibe === vibe.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-300'
}`}
>
<div className={`w-16 h-10 mb-6 transition-transform duration-500 group-hover:scale-110 ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.illustration}</div>
<h4 className={`text-2xl font-bold mb-3 ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
<p className={`text-lg leading-relaxed ${state.designVibe === vibe.id ? 'text-slate-200' : 'text-slate-500'}`}>{vibe.desc}</p>
{state.designVibe === vibe.id && (
<motion.div layoutId="activeVibe" className="absolute top-4 right-4 w-3 h-3 bg-white rounded-full" />
)}
</motion.button>
))}
</div>
</div>
</Reveal>
{/* Color Scheme */}
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="flex justify-between items-center">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Farbschema</h4>
<p className="text-slate-500">Generieren Sie eine harmonische Palette oder definieren Sie eigene Farben.</p>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('color_scheme')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all shadow-sm ${
state.dontKnows?.includes('color_scheme') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
{/* Generator */}
<div className="space-y-6">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
type="button"
onClick={generateHarmonicPalette}
className="w-full p-8 rounded-[3rem] border-2 border-slate-100 bg-white hover:border-slate-900 transition-all duration-500 flex items-center justify-between group shadow-sm hover:shadow-xl"
>
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-900 group-hover:rotate-180 transition-transform duration-700">
<RefreshCw size={28} />
</div>
<div className="text-left">
<p className="text-xl font-bold text-slate-900">Harmonischer Zufall</p>
<p className="text-slate-500">Erzeugt eine mathematisch abgestimmte Palette.</p>
</div>
</div>
<div className="flex h-12 w-48 rounded-xl overflow-hidden shadow-inner border border-slate-100">
{state.colorScheme.map((color, j) => (
<div key={j} className="flex-1" style={{ backgroundColor: color }} />
))}
</div>
</button>
))}
</div>
</div>
</motion.button>
</div>
<div className="space-y-6">
<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>
{/* Custom Picker */}
<div className="space-y-8 p-10 bg-slate-50 rounded-[3rem] border border-slate-100">
<div className="flex items-center gap-3 text-slate-400 font-bold uppercase tracking-widest text-xs">
<Pipette size={16} />
Individuelle Farben
</div>
<div className="flex flex-wrap gap-6">
<AnimatePresence mode="popLayout">
{state.colorScheme.map((color, i) => (
<motion.div
key={`${i}-${color}`}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="relative group"
>
<div className="relative">
<input
type="color"
value={color}
onChange={(e) => updateColor(i, e.target.value)}
className="w-24 h-24 rounded-3xl cursor-pointer border-4 border-white shadow-lg overflow-hidden transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 rounded-3xl pointer-events-none border border-black/5" />
</div>
<div className="mt-2 text-center font-mono text-[10px] text-slate-400 uppercase">{color}</div>
{state.colorScheme.length > 1 && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => removeColor(i)}
className="absolute -top-3 -right-3 w-8 h-8 bg-white text-red-500 rounded-full flex items-center justify-center shadow-xl border border-slate-100 opacity-0 group-hover:opacity-100 transition-all duration-300 z-10"
>
<X size={16} strokeWidth={3} />
</motion.button>
)}
</motion.div>
))}
</AnimatePresence>
{state.colorScheme.length < 5 && (
<motion.button
layout
whileHover={{ scale: 1.05, borderColor: '#0f172a', color: '#0f172a' }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={addColor}
className="w-24 h-24 rounded-3xl border-2 border-dashed border-slate-300 flex flex-col items-center justify-center text-slate-400 transition-all duration-300 bg-white/50 hover:bg-white"
>
<Plus size={32} />
<span className="text-[10px] font-bold uppercase mt-1">Add</span>
</motion.button>
)}
</div>
<p className="text-sm text-slate-400 font-medium">Klicken Sie auf eine Farbe, um sie anzupassen. Sie können bis zu 5 Farben definieren.</p>
</div>
</div>
</Reveal>
{/* Wishes */}
<Reveal width="100%" delay={0.3}>
<div className="space-y-6">
<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-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 resize-none text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</Reveal>
</div>
);
}

View File

@@ -2,9 +2,11 @@
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';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, LayoutGrid, ListPlus } from 'lucide-react';
interface FeaturesStepProps {
state: FormState;
@@ -13,25 +15,115 @@ interface FeaturesStepProps {
}
export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepProps) {
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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}
checked={state.features.includes(opt.id)}
onChange={() => updateState({ features: toggleItem(state.features, opt.id) })}
/>
))}
<div className="space-y-16">
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<LayoutGrid size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">System-Module</h4>
</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 shadow-sm ${
state.dontKnows?.includes('features') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{FEATURE_OPTIONS.map((opt, index) => (
<motion.div
key={opt.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Checkbox
label={opt.label} desc={opt.desc}
checked={state.features.includes(opt.id)}
onChange={() => updateState({ features: toggleItem(state.features, opt.id) })}
/>
</motion.div>
))}
</div>
</div>
<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. Glossar, Download-Center, Partner-Bereich..."
/>
<div className="space-y-12">
<div className="space-y-8">
<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 shadow-sm">
<ListPlus size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Weitere inhaltliche Module?</h4>
</div>
<RepeatableList
items={state.otherFeatures}
onAdd={(v) => updateState({ otherFeatures: [...state.otherFeatures, v] })}
onRemove={(i) => updateState({ otherFeatures: state.otherFeatures.filter((_, idx) => idx !== i) })}
placeholder="z.B. Glossar, Download-Center, Partner-Bereich..."
/>
</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 shadow-sm 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

@@ -4,7 +4,9 @@ import * as React from 'react';
import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Minus, Plus } from 'lucide-react';
import { Minus, Plus, Cpu, ListPlus } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface FunctionsStepProps {
state: FormState;
@@ -15,88 +17,155 @@ interface FunctionsStepProps {
export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepProps) {
const isWebApp = state.projectType === 'web-app';
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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-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={isWebApp ? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..." : "z.B. Login-Bereich, Buchungssystem..."}
/>
</div>
{!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>
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<Cpu size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
{isWebApp ? 'Funktionale Anforderungen' : 'Erweiterte Funktionen'}
</h4>
</div>
<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 shadow-sm ${
state.dontKnows?.includes('functions') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<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>
)}
</Reveal>
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<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 shadow-sm">
<ListPlus size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Weitere spezifische Wünsche?</h4>
</div>
<RepeatableList
items={state.otherFunctions}
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
placeholder={isWebApp ? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..." : "z.B. Mitgliederbereich, Event-Kalender, geschützte Downloads..."}
/>
</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 shadow-sm 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

@@ -2,8 +2,10 @@
import * as React from 'react';
import { FormState } from '../types';
import { Globe, Minus, Plus, Info } from 'lucide-react';
import { Globe, Info, ListPlus } from 'lucide-react';
import { motion } from 'framer-motion';
import { RepeatableList } from '../components/RepeatableList';
import { Reveal } from '../../Reveal';
interface LanguageStepProps {
state: FormState;
@@ -13,79 +15,86 @@ interface LanguageStepProps {
export function LanguageStep({ state, updateState }: LanguageStepProps) {
const basePriceExplanation = "Jede zusätzliche Sprache erhöht den Gesamtaufwand für Design, Entwicklung und Qualitätssicherung um ca. 20%. Dies deckt die technische Implementierung der Übersetzungsschicht sowie die Anpassung von Layouts für unterschiedliche Textlängen ab.";
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
const languagesCount = state.languagesList.length || 1;
return (
<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} />
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<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 shadow-sm">
<Globe size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Sprachen</h4>
</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 shadow-sm ${
state.dontKnows?.includes('languages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<div>
<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-12 py-6">
<button
type="button"
onClick={() => updateState({ languagesCount: Math.max(1, state.languagesCount - 1) })}
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={32} />
</button>
<div className="flex flex-col items-center">
<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-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={32} />
</button>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-6"
>
<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-lg leading-relaxed text-slate-300">
{basePriceExplanation}
</p>
{state.languagesCount > 1 && (
<div className="pt-6 border-t border-white/10">
<div className="flex justify-between items-center">
<span className="text-lg font-medium">Aktueller Aufschlagsfaktor:</span>
<span className="text-3xl font-bold text-white">+{((state.languagesCount - 1) * 20)}%</span>
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-slate-900 shadow-sm">
<ListPlus size={20} />
</div>
<p className="text-lg font-bold text-slate-900">Welche Sprachen planen Sie?</p>
</div>
<div className="p-2">
<RepeatableList
items={state.languagesList}
onAdd={(v) => updateState({ languagesList: [...state.languagesList, v] })}
onRemove={(i) => updateState({ languagesList: state.languagesList.filter((_, idx) => idx !== i) })}
placeholder="z.B. Englisch, Französisch, Spanisch..."
/>
</div>
</div>
)}
</motion.div>
</div>
</Reveal>
<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).
<Reveal width="100%" delay={0.3}>
<div className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-8 shadow-2xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
<div className="flex items-center gap-4 text-slate-400 relative z-10">
<Info size={24} />
<span className="text-sm font-bold uppercase tracking-widest">Warum dieser Faktor?</span>
</div>
<p className="text-lg leading-relaxed text-slate-300 relative z-10 max-w-3xl">
{basePriceExplanation}
</p>
{languagesCount > 1 && (
<div className="pt-8 border-t border-white/10 relative z-10">
<div className="flex justify-between items-center">
<span className="text-lg font-medium text-slate-400">Aktueller Aufschlagsfaktor:</span>
<motion.span
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
className="text-4xl font-bold text-white"
>
+{((languagesCount - 1) * 20)}%
</motion.span>
</div>
</div>
)}
</div>
<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>
</div>
</Reveal>
</div>
);
}

View File

@@ -0,0 +1,151 @@
'use client';
import * as React from 'react';
import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { Minus, Plus, Link2, Globe, Share2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface PresenceStepProps {
state: FormState;
updateState: (updates: Partial<FormState>) => void;
toggleItem: (list: string[], id: string) => string[];
}
export function PresenceStep({ state, updateState, toggleItem }: PresenceStepProps) {
const updateUrl = (id: string, url: string) => {
updateState({
socialMediaUrls: {
...state.socialMediaUrls,
[id]: url
}
});
};
const SOCIAL_PLATFORMS = [
{ id: 'instagram', label: 'Instagram' },
{ id: 'linkedin', label: 'LinkedIn' },
{ id: 'facebook', label: 'Facebook' },
{ id: 'twitter', label: 'Twitter / X' },
{ id: 'tiktok', label: 'TikTok' },
{ id: 'youtube', label: 'YouTube' },
];
return (
<div className="space-y-16">
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<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 shadow-sm">
<Globe size={24} />
</div>
<h4 className="text-2xl font-bold text-slate-900">Bestehende Website</h4>
</div>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">URL (falls vorhanden)</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-slate-900 transition-colors">
<Link2 size={24} />
</div>
<input
type="url"
placeholder="https://www.beispiel.de"
value={state.existingWebsite}
onChange={(e) => updateState({ existingWebsite: e.target.value })}
className="w-full p-8 pl-16 bg-white border border-slate-100 rounded-[3rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl shadow-sm focus:shadow-2xl"
/>
</div>
</div>
</div>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Reveal width="100%" delay={0.2}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Bestehende Domain</label>
<input
type="text"
placeholder="z.B. beispiel.de"
value={state.existingDomain}
onChange={(e) => updateState({ existingDomain: e.target.value })}
className="w-full p-6 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg shadow-sm focus:shadow-xl"
/>
</div>
</Reveal>
<Reveal width="100%" delay={0.2}>
<div className="space-y-4">
<label className="block text-sm font-bold uppercase tracking-widest text-slate-400 ml-2">Wunsch-Domain</label>
<input
type="text"
placeholder="z.B. neue-marke.de"
value={state.wishedDomain}
onChange={(e) => updateState({ wishedDomain: e.target.value })}
className="w-full p-6 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg shadow-sm focus:shadow-xl"
/>
</div>
</Reveal>
</div>
<Reveal width="100%" delay={0.3}>
<div className="space-y-8">
<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 shadow-sm">
<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);
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 shadow-md' : 'border-slate-100 bg-white hover:border-slate-300'
}`}
>
<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-slate-300 group-focus-within/input:text-slate-900 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 shadow-inner"
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
})}
</div>
</div>
</Reveal>
</div>
);
}

View File

@@ -10,27 +10,50 @@ interface TimelineStepProps {
}
export function TimelineStep({ state, updateState }: TimelineStepProps) {
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
return (
<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.' },
{ id: '3-6-months', label: 'In 3-6 Monaten', desc: 'Langfristige Planung.' },
{ id: 'flexible', label: 'Flexibel', desc: 'Kein fester Termindruck.' },
].map(opt => (
<button
key={opt.id}
<div className="space-y-6">
<div className="flex justify-between items-center">
<h4 className="text-2xl font-bold text-slate-900">Zeitplan</h4>
<button
type="button"
onClick={() => updateState({ deadline: opt.id })}
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'
onClick={() => toggleDontKnow('timeline')}
className={`px-4 py-2 rounded-full text-sm font-bold transition-all ${
state.dontKnows?.includes('timeline') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
<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>
Ich weiß es nicht
</button>
))}
</div>
<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.' },
{ id: '3-6-months', label: 'In 3-6 Monaten', desc: 'Langfristige Planung.' },
{ id: 'flexible', label: 'Flexibel', desc: 'Kein fester Termindruck.' },
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => updateState({ deadline: opt.id })}
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={`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>
</div>
{state.deadline === 'asap' && (
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">

View File

@@ -3,6 +3,8 @@
import * as React from 'react';
import { FormState, ProjectType } from '../types';
import { ConceptWebsite, ConceptSystem } from '../../Landing/ConceptIllustrations';
import { motion } from 'framer-motion';
import { Reveal } from '../../Reveal';
interface TypeStepProps {
state: FormState;
@@ -11,23 +13,33 @@ interface TypeStepProps {
export function TypeStep({ state, updateState }: TypeStepProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{[
{ id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: <ConceptWebsite 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 ${
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-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>
{ id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: <ConceptWebsite className="w-20 h-20 mb-6" /> },
{ id: 'web-app', label: 'Web App', desc: 'Internes Tool, Dashboard oder Prozess-Logik.', illustration: <ConceptSystem className="w-20 h-20 mb-6" /> },
].map((type, index) => (
<Reveal key={type.id} width="100%" delay={index * 0.1}>
<motion.button
whileHover={{ y: -8, boxShadow: '0 25px 50px -12px rgb(0 0 0 / 0.15)' }}
whileTap={{ scale: 0.98 }}
type="button"
onClick={() => updateState({ projectType: type.id as ProjectType })}
className={`w-full p-10 rounded-[3rem] border-2 text-left transition-all duration-500 focus:outline-none overflow-hidden relative group ${
state.projectType === type.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-300'
}`}
>
<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>
<p className={`text-xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
{state.projectType === type.id && (
<motion.div
layoutId="activeType"
className="absolute top-6 right-6 w-4 h-4 bg-white rounded-full"
/>
)}
</motion.button>
</Reveal>
))}
</div>
);

View File

@@ -1,18 +1,35 @@
import * as React from 'react';
export type ProjectType = 'website' | 'web-app';
export interface FormState {
projectType: ProjectType;
// Company
companyName: string;
employeeCount: string;
// Existing Presence
existingWebsite: string;
socialMedia: string[];
socialMediaUrls: Record<string, string>;
existingDomain: string;
wishedDomain: string;
// Project
websiteTopic: string;
selectedPages: string[];
otherPages: string[];
otherPagesCount: number;
features: string[];
otherFeatures: string[];
otherFeaturesCount: number;
functions: string[];
otherFunctions: string[];
otherFunctionsCount: number;
apiSystems: string[];
otherTech: string[];
otherTechCount: number;
assets: string[];
otherAssets: string[];
complexInteractions: number;
otherAssetsCount: number;
newDatasets: number;
cmsSetup: boolean;
storageExpansion: number;
@@ -29,7 +46,7 @@ export interface FormState {
designWishes: string;
// Maintenance
expectedAdjustments: string;
languagesCount: number;
languagesList: string[];
// Timeline
deadline: string;
// Web App specific
@@ -37,6 +54,8 @@ export interface FormState {
userRoles: string[];
dataSensitivity: string;
platformType: string;
// Meta
dontKnows: string[];
}
export interface Step {

View File

@@ -240,7 +240,18 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
hr: 'HR / Recruiting',
realestate: 'Immobilien',
calendar: 'Termine / Booking',
social: 'Social Media Sync'
social: 'Social Media Sync',
maps: 'Google Maps / Places',
auth: 'Auth-Provider'
};
const socialLabels: Record<string, string> = {
instagram: 'Instagram',
linkedin: 'LinkedIn',
facebook: 'Facebook',
twitter: 'Twitter / X',
tiktok: 'TikTok',
youtube: 'YouTube'
};
const positions = [];
@@ -317,11 +328,21 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
});
}
if (state.visualStaging > 0) {
positions.push({
pos: pos++,
title: 'Visuelle Inszenierung',
desc: `Umsetzung von ${state.visualStaging} Hero-Stories, Scroll-Effekten oder speziell inszenierten Sektionen.`,
qty: state.visualStaging,
price: state.visualStaging * pricing.VISUAL_STAGING
});
}
if (state.complexInteractions > 0) {
positions.push({
pos: pos++,
title: 'Besondere Interaktionen',
desc: `Umsetzung von ${state.complexInteractions} komplexen UI-Animationen oder interaktiven Logik-Abschnitten.`,
title: 'Komplexe Interaktion',
desc: `Umsetzung von ${state.complexInteractions} Konfiguratoren, Live-Previews oder mehrstufigen Auswahlprozessen.`,
qty: state.complexInteractions,
price: state.complexInteractions * pricing.COMPLEX_INTERACTION
});
@@ -365,6 +386,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<View style={styles.recipientSection}>
<Text style={styles.recipientLabel}>Ansprechpartner</Text>
<Text style={styles.recipientName}>{state.name || 'Interessent'}</Text>
{state.companyName && <Text style={styles.recipientRole}>{state.companyName}</Text>}
{state.role && <Text style={styles.recipientRole}>{state.role}</Text>}
<Text style={styles.recipientRole}>{state.email}</Text>
</View>
@@ -441,6 +463,10 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
<View style={styles.configGrid}>
{state.projectType === 'website' ? (
<>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Thema</Text>
<Text style={styles.configValue}>{state.websiteTopic || 'Nicht angegeben'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Design-Vibe</Text>
<Text style={styles.configValue}>{vibeLabels[state.designVibe] || state.designVibe}</Text>
@@ -470,6 +496,22 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
</View>
</>
)}
<View style={styles.configItem}>
<Text style={styles.configLabel}>Mitarbeiter</Text>
<Text style={styles.configValue}>{state.employeeCount || 'Nicht angegeben'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Bestehende Website</Text>
<Text style={styles.configValue}>{state.existingWebsite || 'Keine'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Bestehende Domain</Text>
<Text style={styles.configValue}>{state.existingDomain || 'Keine'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Wunsch-Domain</Text>
<Text style={styles.configValue}>{state.wishedDomain || 'Keine'}</Text>
</View>
<View style={styles.configItem}>
<Text style={styles.configLabel}>Zeitplan</Text>
<Text style={styles.configValue}>{deadlineLabels[state.deadline] || state.deadline}</Text>
@@ -486,7 +528,7 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
)}
<View style={styles.configItem}>
<Text style={styles.configLabel}>Sprachen</Text>
<Text style={styles.configValue}>{state.languagesCount}</Text>
<Text style={styles.configValue}>{state.languagesCount} ({state.languagesList.join(', ')})</Text>
</View>
{state.projectType === 'website' && (
<>
@@ -505,6 +547,17 @@ export const EstimationPDF = ({ state, totalPrice, monthlyPrice, totalPagesCount
)}
</View>
{state.socialMedia.length > 0 && (
<View style={{ marginTop: 15 }}>
<Text style={styles.configLabel}>Social Media Accounts</Text>
{state.socialMedia.map((id: string) => (
<Text key={id} style={[styles.configValue, { lineHeight: 1.4 }]}>
{socialLabels[id] || id}: {state.socialMediaUrls[id] || 'Keine URL angegeben'}
</Text>
))}
</View>
)}
{state.designWishes && (
<View style={{ marginTop: 15 }}>
<Text style={styles.configLabel}>Design-Vorstellungen</Text>