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

This commit is contained in:
2026-01-30 22:19:40 +01:00
parent 1cfc92a922
commit 668af74c2a
10 changed files with 467 additions and 164 deletions

View File

@@ -4,8 +4,9 @@ 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 } from 'lucide-react';
import { ChevronRight, ChevronLeft, Send, Check, Sparkles, Info, ArrowRight } from 'lucide-react';
import * as QRCode from 'qrcode';
import * as confetti from 'canvas-confetti';
import { FormState, Step } from './ContactForm/types';
import { PRICING, initialState } from './ContactForm/constants';
@@ -50,8 +51,23 @@ export function ContactForm() {
const [qrCodeData, setQrCodeData] = useState<string>('');
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [hoveredStep, setHoveredStep] = useState<number | null>(null);
const [isSticky, setIsSticky] = useState(false);
const formContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (formContainerRef.current) {
const rect = formContainerRef.current.getBoundingClientRect();
// Stick when the container top reaches the sticky position
setIsSticky(rect.top <= 80);
}
};
window.addEventListener('scroll', handleScroll);
// Initial check
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
setIsClient(true);
}, []);
@@ -295,6 +311,26 @@ export function ContactForm() {
${state.message}
`;
window.location.href = `mailto:marc@mintel.me?subject=Projektanfrage: ${state.name}&body=${encodeURIComponent(mailBody)}`;
// Celebration!
const duration = 5 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min;
const interval: any = setInterval(function() {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
}, 250);
setIsSubmitted(true);
} else {
nextStep();
@@ -318,74 +354,209 @@ export function ContactForm() {
return (
<div ref={formContainerRef} className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
<div className="lg:col-span-8 space-y-12">
<div className="flex flex-col gap-10">
<div className="flex flex-col md:flex-row md:items-center gap-8">
<div className="w-32 h-32 shrink-0 bg-slate-50 rounded-[2.5rem] p-4 flex items-center justify-center relative">
{activeSteps[stepIndex].illustration}
<div className="absolute -bottom-2 -right-2 w-10 h-10 bg-slate-900 text-white rounded-full flex items-center justify-center font-bold text-sm border-4 border-white">
{stepIndex + 1}
<div
className={`sticky top-[80px] z-40 transition-all duration-500 ${isSticky ? 'bg-white/80 backdrop-blur-md py-4 -mx-12 px-12 -mt-px border-b border-slate-50' : 'bg-transparent py-6 border-none'}`}
>
<div className={`flex flex-col ${isSticky ? 'gap-4' : 'gap-8'}`}>
<div className="flex flex-row items-center justify-between gap-8">
<div className="flex items-center gap-6">
<motion.div
animate={{
scale: isSticky ? 0.7 : 1,
width: isSticky ? 80 : 128,
height: isSticky ? 80 : 128,
borderRadius: isSticky ? '1.75rem' : '2.5rem'
}}
className="shrink-0 bg-slate-50 flex items-center justify-center relative shadow-inner z-10"
>
<div className="p-3 w-full h-full flex items-center justify-center overflow-hidden rounded-[inherit]">
{activeSteps[stepIndex].illustration}
</div>
<AnimatePresence>
{!isSticky && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="absolute -bottom-2 -right-2 w-10 h-10 bg-slate-900 text-white rounded-full flex items-center justify-center font-bold text-sm border-4 border-white shadow-xl z-20"
>
{stepIndex + 1}
</motion.div>
)}
</AnimatePresence>
</motion.div>
<div className="space-y-1 min-w-0">
<motion.div
animate={{ opacity: isSticky ? 0 : 1, height: isSticky ? 0 : 'auto', marginBottom: isSticky ? 0 : 4 }}
className="flex items-center gap-3 overflow-hidden"
>
<span className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 flex items-center gap-2">
<Sparkles size={12} className="text-slate-300" />
Schritt {stepIndex + 1} / {activeSteps.length}
</span>
</motion.div>
<motion.h3
animate={{
fontSize: isSticky ? '1.5rem' : '2.25rem',
lineHeight: isSticky ? '2rem' : '2.5rem',
color: isSticky ? '#0f172a' : '#0f172a'
}}
className="font-black tracking-tight truncate"
>
{activeSteps[stepIndex].title.replace('{company}', state.companyName || 'Ihr Unternehmen')}
</motion.h3>
<motion.p
animate={{
fontSize: isSticky ? '0.875rem' : '1.125rem',
lineHeight: isSticky ? '1.25rem' : '1.75rem'
}}
className="text-slate-500 leading-relaxed max-w-2xl truncate overflow-hidden"
>
{activeSteps[stepIndex].description.replace('{company}', state.companyName || 'Ihr Unternehmen')}
</motion.p>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
{stepIndex > 0 ? (
<motion.button
whileHover={{ x: -3, backgroundColor: '#f8fafc' }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={prevStep}
className={`flex items-center justify-center gap-2 rounded-full border-2 border-slate-100 transition-all font-bold focus:outline-none whitespace-nowrap ${isSticky ? 'px-5 py-2 text-sm' : 'px-8 py-4 text-lg'}`}
>
<ChevronLeft size={isSticky ? 16 : 20} /> Zurück
</motion.button>
) : <div className={isSticky ? 'w-0' : 'w-32'} />}
{stepIndex < activeSteps.length - 1 ? (
<motion.button
whileHover={{ x: 3, scale: 1.02 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={nextStep}
className={`flex items-center justify-center gap-2 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold shadow-xl shadow-slate-200 focus:outline-none group whitespace-nowrap ${isSticky ? 'px-6 py-2 text-sm' : 'px-10 py-4 text-lg'}`}
>
Weiter <ChevronRight size={isSticky ? 16 : 20} className="transition-transform group-hover:translate-x-1" />
</motion.button>
) : (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="submit"
form="contact-form"
disabled={!state.email || !state.name}
className={`flex items-center justify-center gap-2 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-slate-200 focus:outline-none group whitespace-nowrap ${isSticky ? 'px-6 py-2 text-sm' : 'px-10 py-4 text-lg'}`}
>
Senden <Send size={isSticky ? 16 : 20} className="transition-transform group-hover:translate-x-1 group-hover:-translate-y-1" />
</motion.button>
)}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-3">
<span className="text-base font-bold uppercase tracking-[0.2em] text-slate-400">
Schritt {stepIndex + 1} von {activeSteps.length}
</span>
<div className="relative">
<div className={`flex gap-1.5 transition-all duration-500 ${isSticky ? 'h-1' : 'h-2.5'}`}>
{activeSteps.map((step, i) => (
<div
key={i}
className="flex-1 h-full flex items-center relative"
onMouseEnter={() => setHoveredStep(i)}
onMouseLeave={() => setHoveredStep(null)}
>
<button
type="button"
onClick={() => {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`w-full h-full rounded-full transition-all duration-700 ${
i === stepIndex ? 'bg-slate-900 scale-y-150 shadow-lg shadow-slate-200' :
i < stepIndex ? 'bg-slate-400' : 'bg-slate-100'
} cursor-pointer focus:outline-none p-0 border-none relative group`}
>
<AnimatePresence>
{hoveredStep === i && (
<motion.div
initial={{ opacity: 0, y: 5, x: "-50%", scale: 0.9 }}
animate={{ opacity: 1, y: isSticky ? -35 : -40, x: "-50%", scale: 1 }}
exit={{ opacity: 0, y: 5, x: "-50%", scale: 0.9 }}
className="absolute left-1/2 px-3 py-1.5 bg-white text-slate-900 text-[10px] font-black uppercase tracking-[0.15em] rounded-md whitespace-nowrap pointer-events-none z-50 shadow-[0_10px_30px_rgba(0,0,0,0.1)] border border-slate-100"
>
{step.title.replace('{company}', state.companyName || 'Unternehmen')}
</motion.div>
)}
</AnimatePresence>
</button>
</div>
))}
</div>
<h3 className="text-4xl font-bold tracking-tight text-slate-900">
{activeSteps[stepIndex].title.replace('{company}', state.companyName || 'Ihr Unternehmen')}
</h3>
<p className="text-xl text-slate-500 leading-relaxed">
{activeSteps[stepIndex].description.replace('{company}', state.companyName || 'Ihr Unternehmen')}
{!isSticky && (
<div className="flex justify-between mt-4 px-1">
{chapters.map((chapter, idx) => {
const chapterSteps = activeSteps.filter(s => s.chapter === chapter.id);
if (chapterSteps.length === 0) return null;
const firstStepIdx = activeSteps.indexOf(chapterSteps[0]);
const lastStepIdx = activeSteps.indexOf(chapterSteps[chapterSteps.length - 1]);
const isActive = stepIndex >= firstStepIdx && stepIndex <= lastStepIdx;
return (
<div
key={chapter.id}
className={`text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-500 ${
isActive ? 'text-slate-900' : 'text-slate-300'
}`}
>
{chapter.title}
</div>
);
})}
</div>
)}
</div>
</div>
</div>
<form id="contact-form" onSubmit={handleSubmit} className="min-h-[450px] relative pt-12">
<AnimatePresence mode="wait">
<motion.div
key={activeSteps[stepIndex].id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
>
{renderStepContent()}
</motion.div>
</AnimatePresence>
{/* Contextual Help / Why this matters */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="mt-24 p-10 bg-slate-50 rounded-[3rem] border border-slate-100 flex gap-8 items-start relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-12 opacity-[0.03] pointer-events-none">
<Sparkles size={160} />
</div>
<div className="w-14 h-14 shrink-0 bg-white rounded-2xl flex items-center justify-center text-slate-900 shadow-sm relative z-10">
<Info size={28} />
</div>
<div className="space-y-2 relative z-10">
<h4 className="text-xl font-bold text-slate-900">Warum das wichtig ist</h4>
<p className="text-lg text-slate-500 leading-relaxed max-w-2xl">
{stepIndex === 0 && "Die Wahl zwischen Website und Web App bestimmt die technologische Basis. Web Apps nutzen oft Frameworks wie React oder Next.js für hochgradig interaktive Prozesse, während Websites auf Content-Präsentation optimiert sind."}
{stepIndex === 1 && "Ihr Unternehmen und dessen Größe helfen mir, die Skalierbarkeit und die notwendige Infrastruktur von Beginn an richtig zu dimensionieren."}
{stepIndex === 2 && "Bestehende Kanäle geben Aufschluss über Ihre aktuelle Markenidentität. Wir können diese entweder konsequent weiterführen oder einen bewussten Neuanfang wagen."}
{stepIndex > 2 && "Jede Auswahl beeinflusst die Komplexität und damit auch die Entwicklungszeit. Mein Ziel ist es, für Sie das beste Preis-Leistungs-Verhältnis zu finden."}
</p>
</div>
</div>
<div className="flex gap-2 h-2">
{activeSteps.map((step, i) => (
<div
key={i}
className="flex-1 h-full flex items-center relative"
onMouseEnter={() => setHoveredStep(i)}
onMouseLeave={() => setHoveredStep(null)}
>
<button
type="button"
onClick={() => {
setStepIndex(i);
setTimeout(scrollToTop, 50);
}}
className={`w-full h-full rounded-full transition-all duration-700 ${
i === stepIndex ? 'bg-slate-900 scale-y-150' :
i < stepIndex ? 'bg-slate-400' : 'bg-slate-100'
} cursor-pointer focus:outline-none p-0 border-none`}
/>
</div>
))}
</div>
</div>
<div className="flex justify-between items-center py-6 border-y border-slate-50">
{stepIndex > 0 ? (
<button type="button" onClick={prevStep} className="text-base font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
<ChevronLeft size={18} /> Zurück
</button>
) : <div />}
<div className="flex items-center gap-4">
{stepIndex < activeSteps.length - 1 && (
<button type="button" onClick={nextStep} className="text-base font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2 focus:outline-none relative overflow-hidden rounded-full">
Weiter <ChevronRight size={18} />
</button>
)}
</div>
</div>
<form onSubmit={handleSubmit} className="min-h-[450px]">
<AnimatePresence mode="wait"><motion.div key={activeSteps[stepIndex].id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}>{renderStepContent()}</motion.div></AnimatePresence>
<div className="flex justify-between mt-16">
{stepIndex > 0 ? (<button type="button" onClick={prevStep} className="flex items-center gap-3 px-10 py-5 rounded-full border border-slate-200 hover:border-slate-900 transition-all font-bold text-xl focus:outline-none overflow-hidden relative rounded-full"><ChevronLeft size={24} /> Zurück</button>) : <div />}
{stepIndex < activeSteps.length - 1 ? (<button type="button" onClick={nextStep} className="flex items-center gap-3 px-10 py-5 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold text-xl shadow-xl shadow-slate-200 focus:outline-none overflow-hidden relative rounded-full">Weiter <ChevronRight size={24} /></button>) : (<button type="submit" disabled={!state.email || !state.name} className="flex items-center gap-3 px-10 py-5 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition-all font-bold text-xl disabled:opacity-50 disabled:cursor-not-allowed shadow-xl shadow-slate-200 focus:outline-none overflow-hidden relative rounded-full">Anfrage senden <Send size={24} /></button>)}
</div>
</motion.div>
</form>
</div>
<PriceCalculation

View File

@@ -2,6 +2,7 @@
import * as React from 'react';
import { Check } from 'lucide-react';
import { motion } from 'framer-motion';
interface CheckboxProps {
label: string;
@@ -19,13 +20,29 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) {
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 className={`mt-1 w-8 h-8 rounded-full border-2 flex items-center justify-center shrink-0 transition-all duration-500 ${checked ? 'border-white bg-white text-slate-900 scale-110 shadow-lg' : 'border-slate-200'}`}>
{checked && (
<motion.div
initial={{ scale: 0, rotate: -45 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
<Check size={18} strokeWidth={4} />
</motion.div>
)}
</div>
<div className="flex-grow">
<h4 className={`text-xl font-bold mb-1 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4>
{desc && <p className={`text-base leading-relaxed ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>}
<h4 className={`text-2xl font-bold mb-1 transition-colors duration-500 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4>
{desc && <p className={`text-lg leading-relaxed transition-colors duration-500 ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>}
</div>
{checked && (
<motion.div
layoutId={`check-bg-${label}`}
className="absolute inset-0 bg-gradient-to-br from-slate-800 to-slate-900 -z-10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
/>
)}
</button>
);
}

View File

@@ -91,51 +91,6 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
/>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500 relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-1 h-full bg-slate-900" />
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div>
<h4 className="text-xl font-bold text-slate-900">Anzahl weiterer Assets</h4>
<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

@@ -5,7 +5,7 @@ import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { RepeatableList } from '../components/RepeatableList';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, FileText, ListPlus, HelpCircle } from 'lucide-react';
import { Minus, Plus, FileText, ListPlus, HelpCircle, ArrowRight } from 'lucide-react';
import { Input } from '../components/Input';
interface BaseStepProps {
@@ -45,19 +45,19 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
</motion.div>
<div className="space-y-8">
<div className="flex justify-between items-center">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<FileText size={24} />
<div className="w-14 h-14 bg-slate-900 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-slate-200">
<FileText size={28} />
</div>
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-bold text-slate-900">Die Seiten</h4>
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Planungsbasis</span>
<h4 className="text-3xl font-bold text-slate-900 tracking-tight">Die Seitenstruktur</h4>
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Essenziell</span>
</div>
<div className="flex items-center gap-2 text-slate-400 mt-1">
<HelpCircle size={14} className="shrink-0" />
<span className="text-sm">Die Seitenstruktur hilft uns, den Umfang und die Komplexität Ihres Projekts präzise einzuschätzen.</span>
<span className="text-base">Wählen Sie die Bausteine Ihrer neuen Website.</span>
</div>
</div>
</div>
@@ -118,42 +118,46 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="p-10 bg-white border border-slate-100 rounded-[3rem] space-y-6 hover:shadow-xl transition-all duration-500"
className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-6 shadow-2xl shadow-slate-200 relative overflow-hidden group"
>
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
<ListPlus size={120} />
</div>
<div className="flex flex-col md:flex-row justify-between items-center gap-8 relative z-10">
<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>
<h4 className="text-2xl font-bold text-white">Noch mehr Seiten?</h4>
<p className="text-lg text-slate-400 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"
onClick={() => updateState({ otherPagesCount: Math.max(0, state.otherPagesCount - 1) })}
className="w-16 h-16 rounded-full bg-white/10 border border-white/10 flex items-center justify-center hover:bg-white/20 transition-colors focus:outline-none"
>
<Minus size={24} />
<Minus size={28} />
</motion.button>
<AnimatePresence mode="wait">
<motion.span
<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"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.5 }}
className="text-6xl font-bold w-16 text-center"
>
{state.otherPagesCount}
</motion.span>
</AnimatePresence>
<motion.button
<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"
type="button"
onClick={() => updateState({ otherPagesCount: state.otherPagesCount + 1 })}
className="w-16 h-16 rounded-full bg-white/10 border border-white/10 flex items-center justify-center hover:bg-white/20 transition-colors focus:outline-none"
>
<Plus size={24} />
<Plus size={28} />
</motion.button>
</div>
</div>

View File

@@ -107,11 +107,6 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
checked={state.functions.includes('calendar')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'calendar') })}
/>
<Checkbox
label="Mehrsprachigkeit" desc="Inhalte in mehreren Sprachen."
checked={state.functions.includes('multilang')}
onChange={() => updateState({ functions: toggleItem(state.functions, 'multilang') })}
/>
<Checkbox
label="Echtzeit-Chat" desc="Direkte Kommunikation mit Besuchern."
checked={state.functions.includes('chat')}

View File

@@ -3,7 +3,7 @@
import * as React from 'react';
import { FormState } from '../types';
import { Checkbox } from '../components/Checkbox';
import { Link2, Globe, Share2 } from 'lucide-react';
import { Link2, Globe, Share2, Instagram, Linkedin, Facebook, Twitter, Youtube } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Reveal } from '../../Reveal';
import { Input } from '../components/Input';
@@ -25,12 +25,11 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
};
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' },
{ id: 'instagram', label: 'Instagram', icon: Instagram },
{ id: 'linkedin', label: 'LinkedIn', icon: Linkedin },
{ id: 'facebook', label: 'Facebook', icon: Facebook },
{ id: 'twitter', label: 'Twitter / X', icon: Twitter },
{ id: 'youtube', label: 'YouTube', icon: Youtube },
];
return (
@@ -83,21 +82,25 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
<h4 className="text-2xl font-bold text-slate-900">Social Media Accounts</h4>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
{SOCIAL_PLATFORMS.map((platform) => {
const isSelected = state.socialMedia.includes(platform.id);
const Icon = platform.icon;
return (
<motion.button
key={platform.id}
whileHover={{ y: -4 }}
whileHover={{ y: -8, scale: 1.02 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => updateState({ socialMedia: toggleItem(state.socialMedia, platform.id) })}
className={`flex flex-col items-center gap-3 p-6 rounded-[2rem] border-2 transition-all duration-300 ${
isSelected ? 'border-slate-900 bg-slate-900 text-white shadow-lg' : 'border-slate-100 bg-white text-slate-400 hover:border-slate-200'
className={`flex flex-col items-center gap-4 p-8 rounded-[2.5rem] border-2 transition-all duration-500 ${
isSelected ? 'border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400' : 'border-slate-100 bg-white text-slate-400 hover:border-slate-300 hover:shadow-xl'
}`}
>
<span className="font-bold text-sm">{platform.label}</span>
<div className={`p-4 rounded-2xl transition-colors duration-500 ${isSelected ? 'bg-white/10 text-white' : 'bg-slate-50 text-slate-400'}`}>
<Icon size={32} />
</div>
<span className="font-bold text-base tracking-tight">{platform.label}</span>
</motion.button>
);
})}

View File

@@ -24,22 +24,24 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
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'
className={`w-full p-12 rounded-[4rem] border-2 text-left transition-all duration-700 focus:outline-none overflow-hidden relative group ${
state.projectType === type.id ? 'border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400' : 'border-slate-100 bg-white hover:border-slate-300 hover:shadow-xl'
}`}
>
<div className={`transition-transform duration-500 group-hover:scale-110 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.illustration}</div>
<div className="flex items-center gap-4 mb-4">
<h4 className={`text-4xl font-bold ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<span className={`px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wider ${state.projectType === type.id ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-500'}`}>Grundlage</span>
<div className={`transition-all duration-700 group-hover:scale-110 group-hover:rotate-3 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.illustration}</div>
<div className="flex items-center gap-4 mb-6">
<h4 className={`text-5xl font-bold tracking-tight ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${state.projectType === type.id ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-500'}`}>Grundlage</span>
</div>
<p className={`text-xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
<p className={`text-2xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
{state.projectType === type.id && (
<motion.div
<motion.div
layoutId="activeType"
className="absolute top-6 right-6 w-4 h-4 bg-white rounded-full"
/>
className="absolute top-8 right-8 w-6 h-6 bg-white rounded-full shadow-lg flex items-center justify-center"
>
<div className="w-2 h-2 bg-slate-900 rounded-full" />
</motion.div>
)}
</motion.button>
</Reveal>