diff --git a/apps/web/app/about/page.tsx b/apps/web/app/about/page.tsx index 3fc1ccd..9d6d189 100644 --- a/apps/web/app/about/page.tsx +++ b/apps/web/app/about/page.tsx @@ -1,7 +1,6 @@ "use client"; import Image from "next/image"; -import { PageHeader } from "../../src/components/PageHeader"; import { Section } from "../../src/components/Section"; import { Reveal } from "../../src/components/Reveal"; import { @@ -15,6 +14,7 @@ import { } from "../../src/components/Landing"; import { Check } from "lucide-react"; import { + H1, H3, H4, LeadText, @@ -36,17 +36,16 @@ import { Marker } from "../../src/components/Marker"; export default function AboutPage() { return (
- + {/* Background decoration removed per user request */} {/* Hero Section */}
- +
{/* Structural rings around avatar */} -
-
+ {/* Structural rings removed per user request */}
@@ -75,15 +74,17 @@ export default function AboutPage() {
- - Über mich. - - } - description="15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen halten." - className="pt-0 md:pt-0" - /> + +

+ Über mich. +

+
+ +

+ 15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen + halten. +

+
@@ -237,8 +238,8 @@ export default function AboutPage() { -
-
+
+
Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt, @@ -270,7 +271,7 @@ export default function AboutPage() {
{/* Decorative terminal */} - +
diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index f3055ea..72774cd 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import { blogPosts } from "../../../src/data/blogPosts"; import { PageHeader } from "../../../src/components/PageHeader"; import { Section } from "../../../src/components/Section"; +import { Reveal } from "../../../src/components/Reveal"; import { BlogPostClient } from "../../../src/components/BlogPostClient"; import { PostComponents } from "../../../src/components/blog/posts"; import { Card } from "../../../src/components/Layout"; @@ -50,54 +51,58 @@ export default async function BlogPostPage({
- - {/* Decorative background grid inside the card */} -
+ + + {/* Decorative background grid inside the card */} +
-
-
-
- - -
-
-
- | - {readingTime} min Lesezeit +
+
+
+ +
- - {slug.substring(0, 4).toUpperCase()}- - {Math.floor(Math.random() * 999)} - -
-
- - {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((tag, index) => ( - - #{tag} +
+
+ + | + + {readingTime} min Lesezeit +
+ + {slug.substring(0, 4).toUpperCase()}- + {Math.floor(Math.random() * 999)} - ))} +
- )} - {PostContent ? ( - - ) : ( -
- Inhalt wird bald veröffentlicht... -
- )} -
- + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag, index) => ( + + #{tag} + + ))} +
+ )} + + {PostContent ? ( + + ) : ( +
+ Inhalt wird bald veröffentlicht... +
+ )} +
+ +
diff --git a/apps/web/app/blog/page.tsx b/apps/web/app/blog/page.tsx index 90400af..31b822c 100644 --- a/apps/web/app/blog/page.tsx +++ b/apps/web/app/blog/page.tsx @@ -5,7 +5,6 @@ import { useState, useEffect } from "react"; import { MediumCard } from "../../src/components/MediumCard"; import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar"; import { blogPosts } from "../../src/data/blogPosts"; -import { PageHeader } from "../../src/components/PageHeader"; import { SectionHeader } from "../../src/components/SectionHeader"; import { Reveal } from "../../src/components/Reveal"; import { Section } from "../../src/components/Section"; @@ -124,7 +123,7 @@ export default function BlogPage() { ) : (
{postsToShow.map((post, i) => ( - + ))} @@ -134,7 +133,7 @@ export default function BlogPage() { {/* Pagination */} {hasMore && (
- +
- +
))}
diff --git a/apps/web/app/case-studies/page.tsx b/apps/web/app/case-studies/page.tsx index 2086b05..bb80788 100644 --- a/apps/web/app/case-studies/page.tsx +++ b/apps/web/app/case-studies/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { PageHeader } from "../../src/components/PageHeader"; +import Image from "next/image"; import { Section } from "../../src/components/Section"; import { Reveal } from "../../src/components/Reveal"; -import { H3, LeadText, BodyText, Label } from "../../src/components/Typography"; +import { H3, LeadText, Label, BodyText } from "../../src/components/Typography"; import { Card } from "../../src/components/Layout"; import { Button } from "../../src/components/Button"; -import { GradientMesh, AbstractCircuit } from "../../src/components/Effects"; +import { AbstractCircuit } from "../../src/components/Effects"; import { ArrowRight } from "lucide-react"; import { motion } from "framer-motion"; @@ -15,131 +15,132 @@ export default function CaseStudiesPage() {
- - Case Studies. - - } - description="Ergebnisse statt Versprechen. Was ich gebaut habe und was es bewirkt." - backgroundSymbol="C" - /> + {/* Featured Case Study Hero */} +
+
+ +
+

+ Case Studies. +

+ + Ergebnisse statt Versprechen. Dokumentierte Architektur-Lösungen + für komplexe Anforderungen. + +
+
- {/* Featured Case Study */} -
} - > - - - - {/* Brand Gradient Background */} -
+ + + + {/* Brand Gradient Background */} +
- {/* Left Column: Content */} -
-
-
- KLZ Logo -
- -
- -
-

- KLZ Cables -

- - Engineering eines industriellen B2B-Systems mit - - {" "} - automatisierter Asset-Pipeline - {" "} - und hochperformantem Headless-Stack. - -
- -
- {["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map( - (tag, i) => ( - - {tag} - - ), - )} -
-
- -
-
- EXPLORE PROJECT - -
-
-
- - {/* Right Column: Visual/Technical Decor */} -
-
- {Array.from({ length: 40 }).map((_, i) => ( -
- {Array.from({ length: 15 }) - .map((_, j) => ( - 0.5 - ? "text-slate-900" - : "text-slate-400" - } - > - {Math.floor(Math.random() * 2)} - - )) - .join(" ")} -
- ))} -
- - {/* Abstract "Cable" lines */} -
{/* Coming Soon */} diff --git a/apps/web/app/contact/page.tsx b/apps/web/app/contact/page.tsx index 78404c4..5b1b855 100644 --- a/apps/web/app/contact/page.tsx +++ b/apps/web/app/contact/page.tsx @@ -1,101 +1,19 @@ -import { PageHeader } from "../../src/components/PageHeader"; -import { Reveal } from "../../src/components/Reveal"; import { Section } from "../../src/components/Section"; -import { H3, LeadText, Label } from "../../src/components/Typography"; -import { Card } from "../../src/components/Layout"; import { ContactForm } from "../../src/components/ContactForm"; -import { GradientMesh, AbstractCircuit } from "../../src/components/Effects"; +import { AbstractCircuit } from "../../src/components/Effects"; export default function ContactPage() { return (
- - Kontakt. - - } - description="Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich zeitnah bei Ihnen." - backgroundSymbol="@" - /> -
- - - } + containerVariant="wide" + effects={<>} + className="pt-24 pb-12 md:pt-32 md:pb-20" > -
- {/* Form */} -
- - - - - -
- - {/* Sidebar */} -
- - -
-
-
- -
- - Aktuell nehme ich Projekte für{" "} - Q2 2026{" "} - an. - -
-
-
- - - -
- - - marc@mintel.me - -
-
-
- - -
- -

- < 24h an Werktagen. -

-
-
-
-
+ {/* Full-width Form */} +
); diff --git a/apps/web/app/tags/[tag]/page.tsx b/apps/web/app/tags/[tag]/page.tsx index 155be3c..375f352 100644 --- a/apps/web/app/tags/[tag]/page.tsx +++ b/apps/web/app/tags/[tag]/page.tsx @@ -1,41 +1,60 @@ -import * as React from 'react'; -import Link from 'next/link'; -import { blogPosts } from '../../../src/data/blogPosts'; -import { MediumCard } from '../../../src/components/MediumCard'; +import * as React from "react"; +import Link from "next/link"; +import { blogPosts } from "../../../src/data/blogPosts"; +import { MediumCard } from "../../../src/components/MediumCard"; +import { Reveal } from "../../../src/components/Reveal"; export async function generateStaticParams() { - const allTags = Array.from(new Set(blogPosts.flatMap(post => post.tags || []))); - return allTags.map(tag => ({ + const allTags = Array.from( + new Set(blogPosts.flatMap((post) => post.tags || [])), + ); + return allTags.map((tag) => ({ tag, })); } -export default async function TagPage({ params }: { params: Promise<{ tag: string }> }) { +export default async function TagPage({ + params, +}: { + params: Promise<{ tag: string }>; +}) { const { tag } = await params; - const posts = blogPosts.filter(post => post.tags?.includes(tag)); + const posts = blogPosts.filter((post) => post.tags?.includes(tag)); return (
-

- Posts tagged {tag} -

-

- {posts.length} post{posts.length === 1 ? '' : 's'} -

+ +

+ Posts tagged{" "} + {tag} +

+
+ +

+ {posts.length} post{posts.length === 1 ? "" : "s"} +

+
- {posts.map(post => ( - + {posts.map((post, i) => ( + + + ))}
-
- - ← Back to blog - -
+ +
+ + ← Back to blog + +
+
); } diff --git a/apps/web/app/technologies/[slug]/content.tsx b/apps/web/app/technologies/[slug]/content.tsx index 1c755d3..7970633 100644 --- a/apps/web/app/technologies/[slug]/content.tsx +++ b/apps/web/app/technologies/[slug]/content.tsx @@ -1,99 +1,141 @@ -'use client'; +"use client"; -import React from 'react'; -import Link from 'next/link'; -import { Container } from '../../../src/components/Layout'; -import { Label } from '../../../src/components/Typography'; -import { Check, ArrowLeft, Zap, ExternalLink } from 'lucide-react'; -import { technologies } from './data'; +import React from "react"; +import Link from "next/link"; +import { Container } from "../../../src/components/Layout"; +import { Label } from "../../../src/components/Typography"; +import { Check, ArrowLeft, Zap, ExternalLink } from "lucide-react"; +import { technologies } from "./data"; +import { Reveal } from "../../../src/components/Reveal"; export default function TechnologyContent({ slug }: { slug: string }) { - const tech = technologies[slug]; - - if (!tech) { - return ( -
-
-

Technology Not Found

- Return Home -
-
- ); - } - - const Icon = tech.icon; + const tech = technologies[slug]; + if (!tech) { return ( -
-
- - - Back to Case Study - - -
-
- -
-
- -

{tech.title}

-

{tech.subtitle}

-
-
-
-
- - -
-
-
-

What is it?

-

{tech.description}

-
- -
-

- Why I use it -

-
- {tech.benefits.map((benefit, i) => ( -
- - {benefit} -
- ))} -
-
- -
- -

What does this mean for you?

-

- {tech.customerValue} -

-
-
- -
-
-

Related Technologies

-
- {tech.related.map((item) => ( - - {item.name} - - - ))} -
-
-
-
-
+
+
+

Technology Not Found

+ + Return Home +
+
); + } + + const Icon = tech.icon; + + return ( +
+
+ + + + Back to Case Study + + + +
+ +
+ +
+
+
+ + + + +

+ {tech.title} +

+
+ +

+ {tech.subtitle} +

+
+
+
+
+
+ + +
+
+
+ +

What is it?

+

+ {tech.description} +

+
+
+ +
+ +

+ Why I use it +

+
+
+ {tech.benefits.map((benefit, i) => ( + +
+ + + {benefit} + +
+
+ ))} +
+
+ + +
+ +

+ What does this mean for you? +

+

+ {tech.customerValue} +

+
+
+
+ +
+ +
+

+ Related Technologies +

+
+ {tech.related.map((item) => ( + + + {item.name} + + + + ))} +
+
+
+
+
+
+
+ ); } diff --git a/apps/web/app/websites/page.tsx b/apps/web/app/websites/page.tsx index f81743d..29d2fc0 100644 --- a/apps/web/app/websites/page.tsx +++ b/apps/web/app/websites/page.tsx @@ -1,22 +1,19 @@ "use client"; -import { PageHeader } from "../../src/components/PageHeader"; import { Reveal } from "../../src/components/Reveal"; import { Section } from "../../src/components/Section"; import { - SystemArchitecture, SpeedPerformance, SolidFoundation, LayerSeparation, - DirectService, TaskDone, } from "../../src/components/Landing"; import { H3, - H4, LeadText, BodyText, Label, + MonoLabel, } from "../../src/components/Typography"; import { Card } from "../../src/components/Layout"; import { Button } from "../../src/components/Button"; @@ -25,6 +22,9 @@ import { GradientMesh, CodeSnippet, AbstractCircuit, + CMSVisualizer, + ArchitectureVisualizer, + ResultVisualizer, } from "../../src/components/Effects"; import { Marker } from "../../src/components/Marker"; @@ -33,86 +33,77 @@ export default function WebsitesPage() {
- - Websites, die
- - - einfach funktionieren. - - - - } - description="Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur für maximale Performance." - backgroundSymbol="W" - className="px-5 md:px-0" - /> +
+
+
+ +
+ + SYSTEM ENGINEERING + +

+ Websites, die
+ + + einfach funktionieren. + + +

+
+
+ + + Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur + für{" "} + + maximale Performance + + . + + +
- {/* 01: Architektur – WIE ich baue */} -
} - > -
- -

- Systeme, nicht Broschüren.
- - Jede Website ist Ingenieursarbeit. - -

-
- - - Ich entwickle Websites von Grund auf – mit modernen Frameworks, - eigener Infrastruktur und einem Deployment-Prozess, der{" "} - - - automatisiert und reproduzierbar - - {" "} - ist. - - +
+ + + - {/* Tech Stack Visual */} - -
+
{[ - { label: "Next.js", sub: "Framework" }, - { label: "TypeScript", sub: "Sprache" }, - { label: "Docker", sub: "Infrastruktur" }, - { label: "Directus", sub: "CMS" }, + { + label: "Next.js", + sub: "Architecture", + desc: "React-Framework für maximale SEO & Speed.", + }, + { + label: "Docker", + sub: "Infrastructure", + desc: "Reproduzierbare Umgebungen überall.", + }, + { + label: "Directus", + sub: "Management", + desc: "Headless CMS für flexible Datenabfrage.", + }, + { + label: "Gitea", + sub: "Pipeline", + desc: "Self-hosted Git & CI/CD Pipelines.", + }, ].map((item, i) => ( - -
-
- - - {/* Decorative Code Snippet */} - -
- -
-
+
@@ -139,7 +130,7 @@ export default function WebsitesPage() {
- + Jede Seite wird vorab gerendert und über ein CDN ausgeliefert. Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "} Reproduzierbar. @@ -199,7 +190,7 @@ export default function WebsitesPage() { - + Ihre Website besteht aus{" "} Ihrem Code. Kein WordPress, kein Wix, keine Blackbox. Alles versioniert, alles @@ -249,58 +240,90 @@ export default function WebsitesPage() { illustration={} effects={} > -
- -

- Inhalte pflegen
- ohne Angst. -

-
-
-
- - - Technik und Inhalt sind{" "} - - - strikt getrennt - +
+
+ +
+ + ARCHITECTURAL SEPARATION + +

+ Inhalte pflegen
+ + ohne Angst. - . Sie bearbeiten Texte und Bilder in einem intuitiven System – - das Design bleibt geschützt. +

+
+
+ + +
+ + Vergessen Sie zerschossene Layouts nach einem Textupdate. + Meine Websites trennen{" "} + + Daten von Design + + . +
+
+
+ +
+
+ +
-
- - -
-
-
- -
- - Texte, Bilder und Inhalte frei bearbeiten. - + +
+ +
+ + Durch eine krisenfeste Headless-Architektur (Directus) + bewegen Sie sich in einem geschützten Sandkasten – während + das Frontend-System die visuelle Integrität Ihrer Marke + garantiert. + + +
+ {["Layout-Schutz", "Live-Vorschau", "Role-RBAC"].map( + (tag, i) => ( +
+ {tag} +
+ ), + )}
-
-
-
- -
- - Design, Layout, Code-Struktur. - +
+ + + +
+
+ PROTOCOL + ENFORCED
- +

+ Website architecture validates all CMS payloads against the + design schema before rendering. +

+
+ INTEGRITY + 100% +
+
+ + +
+
@@ -311,52 +334,40 @@ export default function WebsitesPage() { borderTop illustration={} > -
- -

- Was Sie konkret
- bekommen. -

-
- -
- {[ - { - title: "Ihr Code", - desc: "Vollständiger Quellcode, versioniert auf GitHub. Kein Vendor Lock-in.", - }, - { - title: "Ihre Infrastruktur", - desc: "Docker-Container, CI/CD-Pipeline, automatisches Deployment.", - }, - { - title: "Ihr CMS", - desc: "Eigenes Content-Management-System. Volle Kontrolle über Ihre Inhalte.", - }, - ].map((item, i) => ( - -
-
-

- {item.title} -

- - {item.desc} - -
- - ))} +
+
+ +

+ Was Sie konkret
+ bekommen. +

+
+ + + Keine halben Sachen. Ich liefere Ihnen ein schlüsselfertiges + System mit voller Kontrolle und Transparenz. + +
- -
-
- - + + + + + +
+
+ + BEREIT FÜR DEN NÄCHSTEN SCHRITT? + +
Lassen Sie uns über Ihr Projekt sprechen. - +
-
diff --git a/apps/web/package.json b/apps/web/package.json index 80b9491..c24d751 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -58,6 +58,7 @@ "lucide-react": "^0.468.0", "mermaid": "^11.12.2", "next": "^16.1.6", + "nodemailer": "^8.0.1", "playwright": "^1.58.1", "prismjs": "^1.30.0", "puppeteer": "^24.36.1", @@ -86,6 +87,7 @@ "@next/eslint-plugin-next": "^16.1.6", "@tailwindcss/typography": "^0.5.15", "@types/node": "^25.0.6", + "@types/nodemailer": "^7.0.10", "@types/prismjs": "^1.26.5", "@types/qrcode": "^1.5.6", "autoprefixer": "^10.4.20", diff --git a/apps/web/src/actions/contact.ts b/apps/web/src/actions/contact.ts new file mode 100644 index 0000000..885d7b5 --- /dev/null +++ b/apps/web/src/actions/contact.ts @@ -0,0 +1,48 @@ +"use server"; + +import { sendEmail } from "../lib/mail/mailer"; +import { + getInquiryEmailHtml, + getConfirmationEmailHtml, +} from "../components/ContactForm/EmailTemplates"; + +export async function sendContactInquiry(data: { + name: string; + email: string; + companyName: string; + projectType: string; + message: string; + isFreeText: boolean; + config?: any; +}) { + try { + // 1. Send Inquiry to Marc + const inquiryResult = await sendEmail({ + subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`, + html: getInquiryEmailHtml(data), + replyTo: data.email, + }); + + if (!inquiryResult.success) { + throw new Error(inquiryResult.error); + } + + // 2. Send Confirmation to Customer + await sendEmail({ + to: data.email, + subject: `Kopie deiner Anfrage: ${data.companyName || "mintel.me"}`, + html: getConfirmationEmailHtml(data), + }); + + return { success: true }; + } catch (error) { + console.error("Server Action Error:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Unknown error during submission", + }; + } +} diff --git a/apps/web/src/components/ContactForm.tsx b/apps/web/src/components/ContactForm.tsx index db2d088..efa95e6 100644 --- a/apps/web/src/components/ContactForm.tsx +++ b/apps/web/src/components/ContactForm.tsx @@ -2,842 +2,397 @@ 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, - Sparkles, - Info, -} from "lucide-react"; -import * as QRCode from "qrcode"; +import { motion } from "framer-motion"; import * as confetti from "canvas-confetti"; - -import { FormState, Step } from "./ContactForm/types"; -import { PRICING, initialState } from "./ContactForm/constants"; -import { calculateTotals } from "@mintel/pdf"; -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"; -/* eslint-disable no-unused-vars */ - -import { FeaturesStep } from "./ContactForm/steps/FeaturesStep"; -import { DesignStep } from "./ContactForm/steps/DesignStep"; -import { AssetsStep } from "./ContactForm/steps/AssetsStep"; -import { FunctionsStep } from "./ContactForm/steps/FunctionsStep"; -import { ApiStep } from "./ContactForm/steps/ApiStep"; -import { ContentStep } from "./ContactForm/steps/ContentStep"; -import { LanguageStep } from "./ContactForm/steps/LanguageStep"; -import { TimelineStep } from "./ContactForm/steps/TimelineStep"; -import { ContactStep } from "./ContactForm/steps/ContactStep"; -import { WebAppStep } from "./ContactForm/steps/WebAppStep"; - import { - ConceptTarget, - ConceptWebsite, - ConceptPrototyping, - ConceptCommunication, - ConceptSystem, - ConceptCode, - ConceptAutomation, - ConceptPrice, - HeroArchitecture, -} from "./Landing/ConceptIllustrations"; + Layers, + BrainCircuit, + Workflow, + Plug, + CheckCircle2, + AlertCircle, +} from "lucide-react"; -export interface ContactFormProps { +import { FormState } from "./ContactForm/types"; +import { + PRICING, + initialState, + FEATURE_OPTIONS, + FUNCTION_OPTIONS, + API_OPTIONS, + PAGE_SAMPLES, + ASSET_OPTIONS, +} from "./ContactForm/constants"; +import { calculateTotals } from "@mintel/pdf"; +import { sendContactInquiry } from "../actions/contact"; + +// Configurator Components +import { ConfiguratorLayout } from "./ContactForm/Configurator/ConfiguratorLayout"; +import { NarrativeInput } from "./ContactForm/Configurator/NarrativeInput"; +import { ModuleGrid } from "./ContactForm/Configurator/ModuleGrid"; +import { ReferenceInput } from "./ContactForm/Configurator/ReferenceInput"; +import { Launchpad } from "./ContactForm/Configurator/Launchpad"; +import { Reveal } from "./Reveal"; + +interface ContactFormProps { initialStepIndex?: number; - initialState?: FormState; - onStepChange?: (_index: number) => void; - onStateChange?: (_state: FormState) => void; + initialState?: Partial; } +const CONFIGURATOR_STEPS = [ + { id: "intro", title: "INITIALISIERUNG" }, + { id: "scope", title: "PROJEKT_UMFANG" }, + { id: "refs", title: "INSPIRATIONS_QUELLE" }, + { id: "assets", title: "BESTANDS_AUFNAHME" }, + { id: "features", title: "KERN_MODULE" }, + { id: "functions", title: "FUNKTIONALE_ERW" }, + { id: "api", title: "SYSTEM_INTEGRATION" }, + { id: "launch", title: "FINALE_SEQUENZ" }, +]; + +import { ContactGateway } from "./ContactForm/ContactGateway"; +import { DirectMessageFlow } from "./ContactForm/DirectMessageFlow"; + +type FlowState = "discovery" | "configurator" | "direct-message"; + export function ContactForm({ - initialStepIndex, - initialState: propState, - onStepChange, - onStateChange, -}: ContactFormProps = {}) { - // Use a safe version of useRouter/useSearchParams that doesn't crash if not in a router context - let router: any = null; - let searchParams: any = null; - try { - router = useRouter(); - } catch (_e) { - /* ignore */ - } - try { - searchParams = useSearchParams(); - } catch (_e) { - /* ignore */ - } - - const [internalStepIndex, setInternalStepIndex] = useState(0); - const [internalState, setInternalState] = useState(initialState); - - // Sync with props if provided - const stepIndex = - initialStepIndex !== undefined ? initialStepIndex : internalStepIndex; - const state = propState !== undefined ? propState : internalState; - - const setStepIndex = (val: number) => { - setInternalStepIndex(val); - onStepChange?.(val); - }; - - const setState = (val: any) => { - if (typeof val === "function") { - setInternalState((prev) => { - const next = val(prev); - onStateChange?.(next); - return next; - }); - } else { - setInternalState(val); - onStateChange?.(val); - } - }; - + initialStepIndex = 0, + initialState: injectedState, +}: ContactFormProps) { + const [flow, setFlow] = useState("discovery"); + const [stepIndex, setStepIndex] = useState(initialStepIndex); + const [state, setState] = useState({ + ...initialState, + ...injectedState, + }); const [isSubmitted, setIsSubmitted] = useState(false); - const [qrCodeData, setQrCodeData] = useState(""); - const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const [hoveredStep, setHoveredStep] = useState(null); - const [isSticky, setIsSticky] = useState(false); - const formContainerRef = useRef(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); - const isRemotion = - typeof window !== "undefined" && (window as any).isRemotion; - const [isClient, setIsClient] = useState(isRemotion); + const containerRef = useRef(null); + // Scroll to top on flow or step change useEffect(() => { - if (isRemotion) return; - const handleScroll = () => { - if (formContainerRef.current) { - const rect = formContainerRef.current.getBoundingClientRect(); - setIsSticky(rect.top <= 80); + if (containerRef.current) { + containerRef.current.scrollTo({ top: 0, behavior: "smooth" }); + } else { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + }, [flow, stepIndex]); + + // Keyboard Navigation (only for configurator) + useEffect(() => { + if (flow !== "configurator") return; + const handleKeyDown = (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) + return; + if (e.key === "Enter" && stepIndex < CONFIGURATOR_STEPS.length - 1) { + handleNext(); + } else if (e.key === "Backspace" && stepIndex > 0) { + handlePrev(); } }; - window.addEventListener("scroll", handleScroll); - handleScroll(); - return () => window.removeEventListener("scroll", handleScroll); - }, [isRemotion]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [stepIndex, flow]); - useEffect(() => { - if (!isRemotion) setIsClient(true); - }, [isRemotion]); - - // URL Binding - useEffect(() => { - if (!searchParams) return; - const step = searchParams.get("step"); - if (step) setStepIndex(parseInt(step)); - - const config = searchParams.get("config"); - if (config) { - try { - const decoded = JSON.parse(decodeURIComponent(escape(atob(config)))); - setInternalState((s: FormState) => ({ ...s, ...decoded })); - } catch (e) { - console.error("Failed to decode config", e); - } - } - }, [searchParams]); - - const currentUrl = useMemo(() => { - if (!isClient) return ""; - const params = new URLSearchParams(); - params.set("step", stepIndex.toString()); - - 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, - languagesList: state.languagesList, - deadline: state.deadline, - designVibe: state.designVibe, - colorScheme: state.colorScheme, - targetAudience: state.targetAudience, - userRoles: state.userRoles, - dataSensitivity: state.dataSensitivity, - platformType: state.platformType, - dontKnows: state.dontKnows, - visualStaging: state.visualStaging, - complexInteractions: state.complexInteractions, - }; - - const stateString = btoa( - unescape(encodeURIComponent(JSON.stringify(configData))), - ); - params.set("config", stateString); - - return `${window.location.origin}${window.location.pathname}?${params.toString()}`; - }, [state, stepIndex, isClient]); - - useEffect(() => { - if (isRemotion) return; - if (currentUrl && router) { - router.replace(currentUrl, { scroll: false }); - QRCode.toDataURL(currentUrl, { margin: 1, width: 200 }).then( - setQrCodeData, - ); - } - }, [currentUrl, router, isRemotion]); - - const totals = useMemo(() => { - return calculateTotals(state, PRICING); - }, [state]); - - // Destructuring moved to PriceCalculation if only used there - // const { totalPrice, monthlyPrice, totalPagesCount } = totals; + const totals = useMemo(() => calculateTotals(state as any, PRICING), [state]); const updateState = (updates: Partial) => { - setState((s: FormState) => ({ ...s, ...updates })); + setState((prev) => ({ ...prev, ...updates })); }; const toggleItem = (list: string[], id: string) => { return list.includes(id) ? list.filter((i) => i !== id) : [...list, id]; }; - const scrollToTop = () => { - if (isRemotion) return; - if (formContainerRef.current) { - const offset = 120; - const bodyRect = document.body.getBoundingClientRect().top; - const elementRect = formContainerRef.current.getBoundingClientRect().top; - const elementPosition = elementRect - bodyRect; - const offsetPosition = elementPosition - offset; + const handleNext = () => setStepIndex((prev) => prev + 1); + const handlePrev = () => setStepIndex((prev) => Math.max(0, prev - 1)); - window.scrollTo({ - top: offsetPosition, - behavior: "smooth", - }); + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + setIsSubmitting(true); + setError(null); + + const result = await sendContactInquiry({ + name: state.name, + email: state.email, + companyName: state.companyName, + projectType: state.projectType, + message: state.message, + isFreeText: flow === "direct-message", + config: flow === "configurator" ? state : undefined, + }); + + setIsSubmitting(false); + + if (result.success) { + setIsSubmitted(true); + // Celebration + const duration = 3 * 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 as any)({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, + }); + (confetti as any)({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, + }); + }, 250); + } else { + setError(result.error || "Unerwarteter Fehler bei der Übertragung."); } }; - const nextStep = () => { - if (stepIndex < activeSteps.length - 1) { - setStepIndex(stepIndex + 1); - if (!isRemotion) setTimeout(scrollToTop, 50); - } - }; + // Success view (unified) + if (isSubmitted) { + return ( +
+ +
+ + + +

+ SEQUENZ_INITIIERT +

+

+ Das System wird Sie in Kürze unter {state.email} kontaktieren. +

+ +
+
+
+ ); + } - const prevStep = () => { - if (stepIndex > 0) { - setStepIndex(stepIndex - 1); - if (!isRemotion) setTimeout(scrollToTop, 50); - } - }; + // Gateway Flow + if (flow === "discovery") { + return ( + updateState({ name: v })} + company={state.companyName} + setCompany={(v) => updateState({ companyName: v })} + projectType={state.projectType} + setProjectType={(v) => updateState({ projectType: v })} + onChooseConfigurator={() => setFlow("configurator")} + onChooseDirectMessage={() => setFlow("direct-message")} + /> + ); + } - const steps: Step[] = [ - { - id: "type", - title: "Das Ziel", - description: "Was möchten Sie realisieren?", - illustration: , - chapter: "strategy", - }, - { - id: "company", - title: "Unternehmen", - description: "Wer sind Sie?", - illustration: , - chapter: "strategy", - }, - { - id: "presence", - title: "Präsenz", - description: "Bestehende Kanäle von {company}.", - illustration: , - chapter: "strategy", - }, - { - id: "features", - title: "Die Systeme", - description: "Welche inhaltlichen Bereiche planen wir für {company}?", - illustration: , - chapter: "scope", - }, - { - id: "base", - title: "Die Seiten", - description: "Welche Seiten benötigen wir?", - illustration: , - chapter: "scope", - }, - { - id: "design", - title: "Design-Wünsche", - description: "Wie soll die neue Präsenz von {company} wirken?", - illustration: , - chapter: "creative", - }, - { - id: "assets", - title: "Ihre Assets", - description: "Was bringen Sie bereits mit?", - illustration: , - chapter: "creative", - }, - { - id: "functions", - title: "Die Logik", - description: "Welche Funktionen werden benötigt?", - illustration: , - chapter: "tech", - }, - { - id: "api", - title: "Schnittstellen", - description: "Datenaustausch mit Drittsystemen.", - illustration: , - chapter: "tech", - }, - { - id: "content", - title: "Die Pflege", - description: "Wer kümmert sich um die Daten?", - illustration: , - chapter: "tech", - }, - { - id: "language", - title: "Sprachen", - description: "Globale Reichweite planen.", - illustration: , - chapter: "tech", - }, - { - id: "timeline", - title: "Zeitplan", - description: "Wann soll das Projekt live gehen?", - illustration: , - chapter: "final", - }, - { - id: "contact", - title: "Abschluss", - description: "Erzählen Sie mir mehr über Ihr Vorhaben.", - illustration: , - chapter: "final", - }, - { - id: "webapp", - title: "Web App Details", - description: "Spezifische Anforderungen für {company}.", - illustration: , - chapter: "scope", - }, - ]; + // Direct Message Flow + if (flow === "direct-message") { + return ( + updateState({ email: v })} + company={state.companyName} + message={state.message} + setMessage={(v) => updateState({ message: v })} + onBack={() => setFlow("discovery")} + onSubmit={handleSubmit} + isSubmitting={isSubmitting} + /> + ); + } - const chapters = [ - { id: "strategy", title: "Strategie" }, - { id: "scope", title: "Umfang" }, - { id: "creative", title: "Design" }, - { id: "tech", title: "Technik" }, - { id: "final", title: "Start" }, - ]; - - const activeSteps = useMemo(() => { - if (state.projectType === "website") { - return steps.filter((s) => s.id !== "webapp"); - } - // 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.", - }, - steps.find((s) => s.id === "timeline")!, - steps.find((s) => s.id === "contact")!, - ]; - }, [state.projectType, state.companyName]); - - useEffect(() => { - if (stepIndex >= activeSteps.length) setStepIndex(activeSteps.length - 1); - }, [activeSteps, stepIndex]); - - const renderStepContent = () => { - const currentStep = activeSteps[stepIndex]; - switch (currentStep.id) { - case "type": - return ; - case "company": - return ; - case "presence": + // Configurator Flow + const renderConfiguratorContent = () => { + switch (CONFIGURATOR_STEPS[stepIndex].id) { + case "intro": return ( - updateState({ name: v })} + company={state.companyName} + setCompany={(v) => updateState({ companyName: v })} + projectType={state.projectType} + setProjectType={(v) => updateState({ projectType: v })} /> ); - case "base": + case "scope": return ( - ({ + id: p.id, + title: p.label, + description: p.desc, + icon: , + }))} + selected={state.selectedPages} + onToggle={(id) => + updateState({ + selectedPages: toggleItem(state.selectedPages, id), + }) + } + otherCount={state.otherPagesCount} + onOtherCountChange={(val) => updateState({ otherPagesCount: val })} + otherLabel="Weitere Seiten" + /> + ); + case "refs": + return ( + updateState({ references: refs })} + /> + ); + case "assets": + return ( + ({ + id: a.id, + title: a.label, + description: a.desc, + icon: , + }))} + selected={state.assets} + onToggle={(id) => + updateState({ assets: toggleItem(state.assets, id) }) + } /> ); case "features": return ( - - ); - case "design": - return ; - case "assets": - return ( - ({ + id: f.id, + title: f.label, + description: f.desc, + priceEstimate: "€€", + icon: , + }))} + selected={state.features} + onToggle={(id) => + updateState({ features: toggleItem(state.features, id) }) + } + otherCount={state.otherFeaturesCount} + onOtherCountChange={(val) => + updateState({ otherFeaturesCount: val }) + } + otherLabel="Zusätzliche Features" /> ); case "functions": return ( - ({ + id: f.id, + title: f.label, + description: f.desc, + priceEstimate: "€€€", + icon: , + }))} + selected={state.functions} + onToggle={(id) => + updateState({ functions: toggleItem(state.functions, id) }) + } /> ); case "api": return ( - ({ + id: a.id, + title: a.label, + description: a.desc, + priceEstimate: "€€€", + icon: , + }))} + selected={state.apiSystems} + onToggle={(id) => + updateState({ apiSystems: toggleItem(state.apiSystems, id) }) + } /> ); - case "content": - return ; - case "language": - return ; - case "timeline": - return ; - case "contact": - return ; - case "webapp": - return ; + case "launch": + return ( +
+ {error && ( + +
+ + + {error} + +
+
+ )} + updateState({ email: v })} + timeline={state.deadline} + setTimeline={(v) => updateState({ deadline: v })} + message={state.message} + setMessage={(v) => updateState({ message: v })} + onSubmit={handleSubmit} + isValid={!!state.email && !!state.name} + /> +
+ ); default: return null; } }; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (stepIndex === activeSteps.length - 1) { - // Handle submission - const mailBody = ` - Name: ${state.name} - Email: ${state.email} - Rolle: ${state.role} - Projekt: ${state.projectType} - Konfiguration: ${currentUrl} - - Nachricht: - ${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 = !isRemotion - ? setInterval(function () { - const timeLeft = animationEnd - Date.now(); - - if (timeLeft <= 0) { - return clearInterval(interval); - } - - const particleCount = 50 * (timeLeft / duration); - (confetti as any)({ - ...defaults, - particleCount, - origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, - }); - (confetti as any)({ - ...defaults, - particleCount, - origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, - }); - }, 250) - : null; - - setIsSubmitted(true); - } else { - nextStep(); - } - }; - - const handleShare = () => { - setIsShareModalOpen(true); - }; - - if (isSubmitted) { - return ( - -
- -
-
-

- Anfrage gesendet! -

-

- Vielen Dank, {state.name.split(" ")[0]}. Ich melde mich zeitnah bei - Ihnen. -

-
- -
- ); - } - return ( -
-
-
-
-
-
- -
- {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{" "} - - - )} -
-
- -
-
- {activeSteps.map((step, i) => ( -
setHoveredStep(i)} - onMouseLeave={() => setHoveredStep(null)} - > - -
- ))} -
- - {!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."} -

-
-
-
-
- - - setIsShareModalOpen(false)} - url={currentUrl} - qrCodeData={qrCodeData} - /> +
+ 0 ? handlePrev : undefined} + isSubmitting={isSubmitting} + totalPrice={totals.totalPrice} + monthlyPrice={totals.monthlyPrice} + onRestart={() => { + setFlow("discovery"); + setStepIndex(0); + setState(initialState); + }} + > + {renderConfiguratorContent()} +
); } diff --git a/apps/web/src/components/ContactForm/Configurator/ConfiguratorLayout.tsx b/apps/web/src/components/ContactForm/Configurator/ConfiguratorLayout.tsx new file mode 100644 index 0000000..d2a7055 --- /dev/null +++ b/apps/web/src/components/ContactForm/Configurator/ConfiguratorLayout.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { AbstractCircuit } from "../../Effects/AbstractCircuit"; +import { RotateCcw, ChevronRight, ChevronLeft } from "lucide-react"; + +interface ConfiguratorLayoutProps { + children: React.ReactNode; + stepIndex: number; + totalSteps: number; + title: string; + onNext?: () => void; + onPrev?: () => void; + isSubmitting?: boolean; + totalPrice?: number; + monthlyPrice?: number; + onRestart?: () => void; +} + +export const ConfiguratorLayout = ({ + children, + stepIndex, + totalSteps, + title, + onNext, + onPrev, + isSubmitting, + totalPrice = 0, + monthlyPrice = 0, + onRestart, +}: ConfiguratorLayoutProps) => { + const handleRestart = () => { + if ( + window.confirm("Konfiguration neustarten? Ihr Fortschritt geht verloren.") + ) { + if (onRestart) { + onRestart(); + } else { + window.location.reload(); + } + } + }; + + return ( +
+ {/* Background: Geometric Dot Grid */} +
+ + {/* Subtle Circuit Overlay */} +
+ +
+ + {/* Header (Functional) */} +
+ {/* Top Bar: Controls */} +
+
+
+
+
+
+ + SYSTEM KONFIGURATOR + + + Schritt {String(stepIndex + 1).padStart(2, "0")} /{" "} + {String(totalSteps).padStart(2, "0")} + +
+
+ +
+ +
+
+ + {/* Progress Line */} +
+ +
+
+ + {/* Main Content Area */} +
+ + + {children} + + +
+ + {/* Footer / Controls */} +
+ {/* Left: Live Estimate (Integrated, No Overlay) */} +
+
+ + Kalkuliertes Budget + +
+ + €{totalPrice.toLocaleString("de-DE")} + + {monthlyPrice > 0 && ( + + + €{monthlyPrice}/mtl. + + )} +
+
+
+ + {/* Right: Navigation */} +
+ {onPrev && ( + + )} + {onNext && ( + + )} +
+
+
+ ); +}; diff --git a/apps/web/src/components/ContactForm/Configurator/Launchpad.tsx b/apps/web/src/components/ContactForm/Configurator/Launchpad.tsx new file mode 100644 index 0000000..7e6f5ee --- /dev/null +++ b/apps/web/src/components/ContactForm/Configurator/Launchpad.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Mail, Calendar, MessageSquare, Rocket } from "lucide-react"; +import { cn } from "../../../utils/cn"; +import { Reveal } from "../../Reveal"; + +interface LaunchpadProps { + email: string; + setEmail: (val: string) => void; + timeline: string; + setTimeline: (val: string) => void; + message: string; + setMessage: (val: string) => void; + onSubmit: () => void; + isValid: boolean; +} + +export const Launchpad = ({ + email, + setEmail, + timeline, + setTimeline, + message, + setMessage, + onSubmit, + isValid, +}: LaunchpadProps) => { + return ( +
+ +
+
+
+ System bereit +
+

+ Launch-Sequenz initialisieren +

+

+ Bitte bestätigen Sie die finalen Parameter, um den Prozess zu + starten. +

+
+ + + +
+ {/* Email Input */} +
+ + setEmail(e.target.value)} + placeholder="name@firma.de" + className="w-full bg-slate-50 border border-slate-200 rounded-xl p-5 text-slate-900 text-lg font-medium focus:border-green-500 focus:ring-4 focus:ring-green-500/10 focus:outline-none transition-all placeholder:text-slate-400" + /> +
+ + {/* Timeline Selection */} +
+ +
+ {[ + { id: "asap", label: "ASAP", sub: "Sofort" }, + { id: "1month", label: "< 1 Monat", sub: "Priorität" }, + { id: "3months", label: "1-3 Monate", sub: "Standard" }, + { id: "flexible", label: "Flexibel", sub: "Kein Stress" }, + ].map((t) => ( + + ))} +
+
+ + {/* Message Input */} +
+ +