diff --git a/docs/WEBSITES.md b/docs/WEBSITES.md new file mode 100644 index 0000000..4f19220 --- /dev/null +++ b/docs/WEBSITES.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 6728555..fce3699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 16b38e4..0570720 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index c970632..e48b56e 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -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(''); const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [hoveredStep, setHoveredStep] = useState(null); + const [isSticky, setIsSticky] = useState(false); const formContainerRef = useRef(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 (
-
-
-
- {activeSteps[stepIndex].illustration} -
- {stepIndex + 1} +
+
+
+
+ +
+ {activeSteps[stepIndex].illustration} +
+ + {!isSticky && ( + + {stepIndex + 1} + + )} + +
+
+ + + + Schritt {stepIndex + 1} / {activeSteps.length} + + + + {activeSteps[stepIndex].title.replace('{company}', state.companyName || 'Ihr Unternehmen')} + + + {activeSteps[stepIndex].description.replace('{company}', state.companyName || 'Ihr Unternehmen')} + +
+
+ +
+ {stepIndex > 0 ? ( + + Zurück + + ) :
} + + {stepIndex < activeSteps.length - 1 ? ( + + Weiter + + ) : ( + + Senden + + )}
-
-
- - Schritt {stepIndex + 1} von {activeSteps.length} - + +
+
+ {activeSteps.map((step, i) => ( +
setHoveredStep(i)} + onMouseLeave={() => setHoveredStep(null)} + > + +
+ ))}
-

- {activeSteps[stepIndex].title.replace('{company}', state.companyName || 'Ihr Unternehmen')} -

-

- {activeSteps[stepIndex].description.replace('{company}', state.companyName || 'Ihr Unternehmen')} + + {!isSticky && ( +

+ {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 ( +
+ {chapter.title} +
+ ); + })} +
+ )} +
+
+
+ +
+ + + + {renderStepContent()} + + + + {/* Contextual Help / Why this matters */} + +
+ +
+
+ +
+
+

Warum das wichtig ist

+

+ {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."}

-
- -
- {activeSteps.map((step, i) => ( -
setHoveredStep(i)} - onMouseLeave={() => setHoveredStep(null)} - > -
- ))} -
-
- -
- {stepIndex > 0 ? ( - - ) :
} -
- {stepIndex < activeSteps.length - 1 && ( - - )} -
-
- - - {renderStepContent()} -
- {stepIndex > 0 ? () :
} - {stepIndex < activeSteps.length - 1 ? () : ()} -
+
-
- {checked && } +
+ {checked && ( + + + + )}
-

{label}

- {desc &&

{desc}

} +

{label}

+ {desc &&

{desc}

}
+ {checked && ( + + )} ); } diff --git a/src/components/ContactForm/steps/AssetsStep.tsx b/src/components/ContactForm/steps/AssetsStep.tsx index 9830620..d81a412 100644 --- a/src/components/ContactForm/steps/AssetsStep.tsx +++ b/src/components/ContactForm/steps/AssetsStep.tsx @@ -91,51 +91,6 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) />
- -
-
-
-

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 6716d76..5ee25fb 100644 --- a/src/components/ContactForm/steps/BaseStep.tsx +++ b/src/components/ContactForm/steps/BaseStep.tsx @@ -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) {
-
+
-
- +
+
-

Die Seiten

- Planungsbasis +

Die Seitenstruktur

+ Essenziell
- Die Seitenstruktur hilft uns, den Umfang und die Komplexität Ihres Projekts präzise einzuschätzen. + Wählen Sie die Bausteine Ihrer neuen Website.
@@ -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" > -
+
+ +
+ +
-

Anzahl weiterer Seiten

-

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

+

Noch mehr 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" + 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" > - + - {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" + 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" > - +
diff --git a/src/components/ContactForm/steps/FunctionsStep.tsx b/src/components/ContactForm/steps/FunctionsStep.tsx index 79cd8d0..4ec93c1 100644 --- a/src/components/ContactForm/steps/FunctionsStep.tsx +++ b/src/components/ContactForm/steps/FunctionsStep.tsx @@ -107,11 +107,6 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP checked={state.functions.includes('calendar')} onChange={() => updateState({ functions: toggleItem(state.functions, 'calendar') })} /> - updateState({ functions: toggleItem(state.functions, 'multilang') })} - /> Social Media Accounts
-
+
{SOCIAL_PLATFORMS.map((platform) => { const isSelected = state.socialMedia.includes(platform.id); + const Icon = platform.icon; return ( 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' }`} > - {platform.label} +
+ +
+ {platform.label}
); })} diff --git a/src/components/ContactForm/steps/TypeStep.tsx b/src/components/ContactForm/steps/TypeStep.tsx index 2f6761e..b3a0d83 100644 --- a/src/components/ContactForm/steps/TypeStep.tsx +++ b/src/components/ContactForm/steps/TypeStep.tsx @@ -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' }`} > -
{type.illustration}
-
-

{type.label}

- Grundlage +
{type.illustration}
+
+

{type.label}

+ Grundlage
-

{type.desc}

+

{type.desc}

{state.projectType === type.id && ( - + className="absolute top-8 right-8 w-6 h-6 bg-white rounded-full shadow-lg flex items-center justify-center" + > +
+ )}