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

136
docs/WEBSITES.md Normal file
View File

@@ -0,0 +1,136 @@
Wie ich Websites baue und warum Sie damit Ruhe haben
Die meisten Websites funktionieren.
Bis jemand sie anfasst.
Oder Google etwas ändert.
Oder ein Plugin ein Update macht.
Oder die Agentur nicht mehr antwortet.
Ich baue Websites so, dass das alles egal ist.
Ich baue Websites wie Systeme nicht wie Broschüren
Eine Website ist kein Flyer.
Sie ist ein System, das jeden Tag arbeitet.
Deshalb baue ich sie auch so:
• stabil
• schnell
• vorhersehbar
• ohne Überraschungen
Sie müssen nichts warten.
Sie müssen nichts lernen.
Sie müssen nichts pflegen, wenn Sie nicht wollen.
Geschwindigkeit ist kein Extra. Sie ist Standard.
Viele Websites sind langsam, weil sie zusammengeklickt sind.
Meine sind schnell, weil sie gebaut sind.
Das bedeutet für Sie:
• Seiten laden sofort
• Google mag sie
• Besucher bleiben
• weniger Absprünge
• bessere Sichtbarkeit
90+ Pagespeed ist bei mir kein Ziel.
Es ist der Normalzustand.
Keine Plugins. Keine Updates. Keine Wartungshölle.
Ich nutze keine Baukästen.
Keine Plugin-Sammlungen.
Keine Systeme, die sich selbst zerstören.
Ihre Website besteht aus:
• sauberem Code
• klarer Struktur
• festen Bausteinen
Das heißt:
Wenn etwas geändert wird, geht nichts kaputt.
Inhalte und Technik sind getrennt (absichtlich)
Wenn Sie Inhalte selbst pflegen wollen, können Sie das.
Aber nur Inhalte.
Kein Design.
Keine Struktur.
Keine Technik.
Sie können nichts kaputt machen.
Ich verspreche es.
Und wenn Sie nichts selbst pflegen wollen:
Dann schreiben Sie mir einfach.
Ich erledige das.
Änderungen sind einfach. Wirklich.
Neue Seite?
Neue Funktion?
Neue Idee?
Kein Ticket.
Kein Formular.
Kein Projektplan.
Sie schreiben mir, was Sie brauchen.
Ich setze es um.
Fertig.
Warum das alles so gebaut ist
Weil ich 15 Jahre Agenturen gesehen habe.
Zu viele Meetings.
Zu viele Konzepte.
Zu viele Übergaben.
Zu viele „eigentlich müsste man mal“.
Meine Websites sind dafür gebaut,
dass Dinge einfach passieren.
Das Ergebnis für Sie
• schnelle Website
• keine Pflegepflicht
• keine Überraschungen
• keine Abhängigkeit
• keine Agentur
• kein Stress
Oder anders gesagt:
Eine Website, die sich wie eine erledigte Aufgabe anfühlt.
Und technisch?
Technisch ist das alles sehr modern.
Aber das ist mein Problem, nicht Ihres.
Wenn Sie wollen, erkläre ich Ihnen das gerne.
Wenn nicht, funktioniert es trotzdem.

18
package-lock.json generated
View File

@@ -9,10 +9,12 @@
"version": "0.1.0",
"dependencies": {
"@react-pdf/renderer": "^4.3.2",
"@types/canvas-confetti": "^1.9.0",
"@types/ioredis": "^4.28.10",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vercel/og": "^0.8.6",
"canvas-confetti": "^1.9.4",
"framer-motion": "^12.29.2",
"ioredis": "^5.9.1",
"lucide-react": "^0.468.0",
@@ -1129,6 +1131,12 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@types/canvas-confetti": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
"integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==",
"license": "MIT"
},
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
@@ -1783,6 +1791,16 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvas-confetti": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz",
"integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==",
"license": "ISC",
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",

View File

@@ -15,10 +15,12 @@
},
"dependencies": {
"@react-pdf/renderer": "^4.3.2",
"@types/canvas-confetti": "^1.9.0",
"@types/ioredis": "^4.28.10",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vercel/og": "^0.8.6",
"canvas-confetti": "^1.9.4",
"framer-motion": "^12.29.2",
"ioredis": "^5.9.1",
"lucide-react": "^0.468.0",

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,12 +118,16 @@ 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
@@ -131,17 +135,17 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
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"
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
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>
@@ -151,9 +155,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
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"
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
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>