From 8c1a7f6b5aee9c86b5e826f643bcbf7e86c811a7 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 30 Jan 2026 17:48:13 +0100 Subject: [PATCH] form --- .woodpecker.yml | 72 ----- deploy.sh | 63 ----- docker-compose.yml | 7 +- docker/Caddyfile | 2 +- src/components/ContactForm.tsx | 70 +++-- .../ContactForm/components/Checkbox.tsx | 6 +- .../components/PriceCalculation.tsx | 18 +- src/components/ContactForm/constants.tsx | 53 +++- src/components/ContactForm/steps/ApiStep.tsx | 167 ++++++++--- .../ContactForm/steps/AssetsStep.tsx | 133 +++++++-- src/components/ContactForm/steps/BaseStep.tsx | 155 +++++++++-- .../ContactForm/steps/CompanyStep.tsx | 67 +++++ .../ContactForm/steps/ContactStep.tsx | 247 ++++++++++------ .../ContactForm/steps/ContentStep.tsx | 213 ++++++++++---- .../ContactForm/steps/DesignStep.tsx | 263 ++++++++++++++---- .../ContactForm/steps/FeaturesStep.tsx | 128 +++++++-- .../ContactForm/steps/FunctionsStep.tsx | 225 +++++++++------ .../ContactForm/steps/LanguageStep.tsx | 139 ++++----- .../ContactForm/steps/PresenceStep.tsx | 151 ++++++++++ .../ContactForm/steps/TimelineStep.tsx | 53 +++- src/components/ContactForm/steps/TypeStep.tsx | 44 +-- src/components/ContactForm/types.ts | 23 +- src/components/EstimationPDF.tsx | 61 +++- 23 files changed, 1709 insertions(+), 651 deletions(-) delete mode 100644 .woodpecker.yml delete mode 100755 deploy.sh create mode 100644 src/components/ContactForm/steps/CompanyStep.tsx create mode 100644 src/components/ContactForm/steps/PresenceStep.tsx diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index c68b3f3..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 38ce241..0000000 --- a/deploy.sh +++ /dev/null @@ -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 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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 061d642..d8e912b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docker/Caddyfile b/docker/Caddyfile index 53731be..3bf3531 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -7,7 +7,7 @@ # Main website {$DOMAIN:-localhost} { # Reverse proxy to website container - reverse_proxy website:80 + reverse_proxy website:3000 # Security headers header { diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index 93893e4..1883e97 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -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: }, - { id: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: }, + { id: 'company', title: 'Unternehmen', description: 'Wer sind Sie?', illustration: }, + { id: 'presence', title: 'Präsenz', description: 'Bestehende Kanäle.', illustration: }, { id: 'features', title: 'Die Systeme', description: 'Welche inhaltlichen Bereiche planen wir?', illustration: }, + { id: 'base', title: 'Die Seiten', description: 'Welche Seiten benötigen wir?', illustration: }, { id: 'design', title: 'Design-Wünsche', description: 'Wie soll die Seite wirken?', illustration: }, { id: 'assets', title: 'Ihre Assets', description: 'Was bringen Sie bereits mit?', illustration: }, { id: 'functions', title: 'Die Logik', description: 'Welche Funktionen werden benötigt?', illustration: }, @@ -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 ; + case 'company': + return ; + case 'presence': + return ; case 'base': return ; case 'features': @@ -287,11 +319,11 @@ export function ContactForm() {

{activeSteps[stepIndex].description}

-
+
{activeSteps.map((step, i) => (
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`} /> {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}
diff --git a/src/components/ContactForm/components/Checkbox.tsx b/src/components/ContactForm/components/Checkbox.tsx index 1ceea8d..6c980c7 100644 --- a/src/components/ContactForm/components/Checkbox.tsx +++ b/src/components/ContactForm/components/Checkbox.tsx @@ -15,16 +15,16 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) { ); diff --git a/src/components/ContactForm/components/PriceCalculation.tsx b/src/components/ContactForm/components/PriceCalculation.tsx index 598aee2..6737137 100644 --- a/src/components/ContactForm/components/PriceCalculation.tsx +++ b/src/components/ContactForm/components/PriceCalculation.tsx @@ -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 (
@@ -43,12 +48,13 @@ export function PriceCalculation({ <>
Basis Website{PRICING.BASE_WEBSITE.toLocaleString()} €
- {totalPagesCount > 0 && (
{totalPagesCount}x Seite{(totalPagesCount * PRICING.PAGE).toLocaleString()} €
)} - {state.features.length + state.otherFeatures.length > 0 && (
{state.features.length + state.otherFeatures.length}x System-Modul{((state.features.length + state.otherFeatures.length) * PRICING.FEATURE).toLocaleString()} €
)} - {state.functions.length + state.otherFunctions.length > 0 && (
{state.functions.length + state.otherFunctions.length}x Logik-Funktion{((state.functions.length + state.otherFunctions.length) * PRICING.FUNCTION).toLocaleString()} €
)} - {state.complexInteractions > 0 && (
{state.complexInteractions}x Komplexes UI/Animation{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} €
)} - {state.apiSystems.length + state.otherTech.length > 0 && (
{state.apiSystems.length + state.otherTech.length}x API Sync{((state.apiSystems.length + state.otherTech.length) * PRICING.API_INTEGRATION).toLocaleString()} €
)} - {state.cmsSetup && (
CMS Setup & Anbindung{(PRICING.CMS_SETUP + (state.features.length + state.otherFeatures.length) * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} €
)} + {totalPages > 0 && (
{totalPages}x Seite{(totalPages * PRICING.PAGE).toLocaleString()} €
)} + {totalFeatures > 0 && (
{totalFeatures}x System-Modul{(totalFeatures * PRICING.FEATURE).toLocaleString()} €
)} + {totalFunctions > 0 && (
{totalFunctions}x Logik-Funktion{(totalFunctions * PRICING.FUNCTION).toLocaleString()} €
)} + {state.visualStaging > 0 && (
{state.visualStaging}x Visuelle Inszenierung{(state.visualStaging * PRICING.VISUAL_STAGING).toLocaleString()} €
)} + {state.complexInteractions > 0 && (
{state.complexInteractions}x Komplexe Interaktion{(state.complexInteractions * PRICING.COMPLEX_INTERACTION).toLocaleString()} €
)} + {totalApis > 0 && (
{totalApis}x API Sync{(totalApis * PRICING.API_INTEGRATION).toLocaleString()} €
)} + {state.cmsSetup && (
CMS Setup & Anbindung{(PRICING.CMS_SETUP + totalFeatures * PRICING.CMS_CONNECTION_PER_FEATURE).toLocaleString()} €
)} {state.newDatasets > 0 && (
{state.newDatasets}x Inhalte einpflegen{(state.newDatasets * PRICING.NEW_DATASET).toLocaleString()} €
)} {state.languagesCount > 1 && (
Mehrsprachigkeit ({state.languagesCount}x)+{(totalPrice - (totalPrice / (1 + (state.languagesCount - 1) * 0.2))).toLocaleString()} €
)}
diff --git a/src/components/ContactForm/constants.tsx b/src/components/ContactForm/constants.tsx index 19aa4a2..7786e73 100644 --- a/src/components/ContactForm/constants.tsx +++ b/src/components/ContactForm/constants.tsx @@ -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' }, ]; diff --git a/src/components/ContactForm/steps/ApiStep.tsx b/src/components/ContactForm/steps/ApiStep.tsx index 3dce25d..3f86f3d 100644 --- a/src/components/ContactForm/steps/ApiStep.tsx +++ b/src/components/ContactForm/steps/ApiStep.tsx @@ -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 (
-
-

- {isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'} -

-

- {isWebApp - ? 'Mit welchen Systemen soll die Web App kommunizieren?' - : 'Datenaustausch mit Drittsystemen zur Automatisierung.'} -

-
- updateState({ apiSystems: toggleItem(state.apiSystems, 'crm_erp') })} - /> - updateState({ apiSystems: toggleItem(state.apiSystems, 'payment') })} - /> - updateState({ apiSystems: toggleItem(state.apiSystems, 'marketing') })} - /> - updateState({ apiSystems: toggleItem(state.apiSystems, 'ecommerce') })} - /> + +
+
+
+
+ +
+

+ {isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'} +

+
+ 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 + +
+

+ {isWebApp + ? 'Mit welchen Systemen soll die Web App kommunizieren?' + : 'Datenaustausch mit Drittsystemen zur Automatisierung.'} +

+
+ {[ + { 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) => ( + + updateState({ apiSystems: toggleItem(state.apiSystems, opt.id) })} + /> + + ))} +
-
+ -
-

Weitere Systeme oder eigene APIs?

- updateState({ otherTech: [...state.otherTech, v] })} - onRemove={(i) => updateTech(i)} - placeholder="z.B. Microsoft Graph, Google Maps, Custom REST API..." - /> -
+ +
+
+
+
+ +
+

Weitere Systeme oder eigene APIs?

+
+ updateState({ otherTech: [...state.otherTech, v] })} + onRemove={(i) => updateTech(i)} + placeholder="z.B. Microsoft Graph, Google Search Console, Custom REST API..." + /> +
+ + +
+
+

Anzahl weiterer Schnittstellen

+

Falls Sie weitere Integrationen planen, diese aber noch nicht benennen können.

+
+
+ 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" + > + + + + + {state.otherTechCount} + + + 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" + > + + +
+
+
+
+
); diff --git a/src/components/ContactForm/steps/AssetsStep.tsx b/src/components/ContactForm/steps/AssetsStep.tsx index f49bff2..4b5feb0 100644 --- a/src/components/ContactForm/steps/AssetsStep.tsx +++ b/src/components/ContactForm/steps/AssetsStep.tsx @@ -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 (
-
- {ASSET_OPTIONS.map(opt => ( - updateState({ assets: toggleItem(state.assets, opt.id) })} - /> - ))} -
-
-

Weitere vorhandene Unterlagen?

- updateState({ otherAssets: [...state.otherAssets, v] })} - onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })} - placeholder="z.B. Lastenheft, Wireframes, Bilddatenbank..." - /> -
+ +
+
+
+
+ +
+

Vorhandene Assets

+
+ 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 + +
+
+ {ASSET_OPTIONS.map((opt, index) => ( + + updateState({ assets: toggleItem(state.assets, opt.id) })} + /> + + ))} +
+
+
+ + +
+
+
+
+ +
+

Weitere vorhandene Unterlagen?

+
+ updateState({ otherAssets: [...state.otherAssets, v] })} + onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })} + placeholder="z.B. Lastenheft, Wireframes, Bilddatenbank..." + /> +
+ + +
+
+

Anzahl weiterer Assets

+

Falls Sie weitere Unterlagen haben, diese aber noch nicht benennen können.

+
+
+ 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" + > + + + + + {state.otherAssetsCount} + + + 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" + > + + +
+
+
+
+
); } diff --git a/src/components/ContactForm/steps/BaseStep.tsx b/src/components/ContactForm/steps/BaseStep.tsx index 40340fe..9ee6c52 100644 --- a/src/components/ContactForm/steps/BaseStep.tsx +++ b/src/components/ContactForm/steps/BaseStep.tsx @@ -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 ( -
-
- {[ - { 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 => ( - updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })} - /> - ))} -
-
-

Weitere individuelle Seiten?

- updateState({ otherPages: [...state.otherPages, v] })} - onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })} - placeholder="z.B. Karriere, FAQ, Team-Detail..." +
+ +

Thema der Website

+ 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" /> +
+ +
+
+
+
+ +
+

Die Seiten

+
+ 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 + +
+
+ {[ + { 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) => ( + + updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })} + /> + + ))} +
+
+ +
+
+
+
+ +
+

Weitere individuelle Seiten?

+
+ updateState({ otherPages: [...state.otherPages, v] })} + onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })} + placeholder="z.B. Karriere, FAQ, Team-Detail..." + /> +
+ + +
+
+

Anzahl weiterer Seiten

+

Falls Sie die Namen noch nicht wissen, aber die Menge schätzen können.

+
+
+ 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" + > + + + + + {state.otherPagesCount} + + + 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" + > + + +
+
+
); diff --git a/src/components/ContactForm/steps/CompanyStep.tsx b/src/components/ContactForm/steps/CompanyStep.tsx new file mode 100644 index 0000000..5594130 --- /dev/null +++ b/src/components/ContactForm/steps/CompanyStep.tsx @@ -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) => void; +} + +export function CompanyStep({ state, updateState }: CompanyStepProps) { + return ( +
+ +
+
+
+ +
+

Unternehmen

+
+
+ + 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" + /> +
+
+
+ + +
+
+
+ +
+

Mitarbeiteranzahl

+
+
+ {EMPLOYEE_OPTIONS.map((option, index) => ( + 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} + + ))} +
+
+
+
+ ); +} diff --git a/src/components/ContactForm/steps/ContactStep.tsx b/src/components/ContactForm/steps/ContactStep.tsx index 8e5a0fb..1f8ed19 100644 --- a/src/components/ContactForm/steps/ContactStep.tsx +++ b/src/components/ContactForm/steps/ContactStep.tsx @@ -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 ( -
-
- 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" - /> - 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" - /> -
- 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" - /> -