feat: redesign page heroes, implement organic markers, and streamline contact flow
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Failing after 4m3s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 5s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Failing after 4m3s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 5s
- Refined hero sections for About, Blog, Websites, and Case Studies for a bespoke industrial entry point. - Redesigned Marker component using layered SVG paths for an organic, hand-drawn highlighter effect. - Restored technical precision in ArchitectureVisualizer with refined line thickness. - Streamlined contact page by removing generic headers and prioritizing the configurator/gateway. - Updated technical references to reflect self-hosted Gitea infrastructure. - Cleaned up unused imports and addressed linting warnings across modified pages.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
} from "../../src/components/Landing";
|
} from "../../src/components/Landing";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
H1,
|
||||||
H3,
|
H3,
|
||||||
H4,
|
H4,
|
||||||
LeadText,
|
LeadText,
|
||||||
@@ -36,17 +36,16 @@ import { Marker } from "../../src/components/Marker";
|
|||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<AbstractCircuit />
|
{/* Background decoration removed per user request */}
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative pt-12 md:pt-32 pb-8 md:pb-24 overflow-hidden border-b border-slate-50">
|
<section className="relative pt-12 md:pt-32 pb-8 md:pb-24 overflow-hidden border-b border-slate-50">
|
||||||
<Container variant="narrow" className="relative z-10">
|
<Container variant="narrow" className="relative z-10">
|
||||||
<div className="flex flex-col items-center text-center space-y-6 md:space-y-12">
|
<div className="flex flex-col items-center text-center space-y-6 md:space-y-12">
|
||||||
<Reveal>
|
<Reveal width="fit-content">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Structural rings around avatar */}
|
{/* Structural rings around avatar */}
|
||||||
<div className="absolute inset-0 -m-6 md:-m-8 border border-slate-100 rounded-full animate-[spin_30s_linear_infinite] opacity-50" />
|
{/* Structural rings removed per user request */}
|
||||||
<div className="absolute inset-0 -m-3 md:-m-4 border border-slate-200 rounded-full animate-[spin_20s_linear_infinite_reverse] opacity-30" />
|
|
||||||
|
|
||||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||||
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||||
@@ -75,15 +74,17 @@ export default function AboutPage() {
|
|||||||
<div className="h-px w-6 md:w-8 bg-slate-900"></div>
|
<div className="h-px w-6 md:w-8 bg-slate-900"></div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<PageHeader
|
<Reveal delay={0.2}>
|
||||||
title={
|
<H1 className="text-4xl md:text-8xl leading-none tracking-tighter">
|
||||||
<>
|
Über <span className="text-slate-400">mich.</span>
|
||||||
Über <span className="text-slate-400">mich.</span>
|
</H1>
|
||||||
</>
|
</Reveal>
|
||||||
}
|
<Reveal delay={0.3}>
|
||||||
description="15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen halten."
|
<p className="text-slate-400 font-medium max-w-xl mx-auto text-sm md:text-xl">
|
||||||
className="pt-0 md:pt-0"
|
15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen
|
||||||
/>
|
halten.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -237,8 +238,8 @@ export default function AboutPage() {
|
|||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-start">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8 min-w-0">
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<LeadText className="text-lg md:text-xl text-slate-400">
|
<LeadText className="text-lg md:text-xl text-slate-400">
|
||||||
Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt,
|
Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt,
|
||||||
@@ -270,7 +271,7 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative terminal */}
|
{/* Decorative terminal */}
|
||||||
<Reveal delay={0.3}>
|
<Reveal delay={0.3} className="min-w-0">
|
||||||
<CodeSnippet variant="terminal" className="opacity-70" />
|
<CodeSnippet variant="terminal" className="opacity-70" />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
|
|||||||
import { blogPosts } from "../../../src/data/blogPosts";
|
import { blogPosts } from "../../../src/data/blogPosts";
|
||||||
import { PageHeader } from "../../../src/components/PageHeader";
|
import { PageHeader } from "../../../src/components/PageHeader";
|
||||||
import { Section } from "../../../src/components/Section";
|
import { Section } from "../../../src/components/Section";
|
||||||
|
import { Reveal } from "../../../src/components/Reveal";
|
||||||
import { BlogPostClient } from "../../../src/components/BlogPostClient";
|
import { BlogPostClient } from "../../../src/components/BlogPostClient";
|
||||||
import { PostComponents } from "../../../src/components/blog/posts";
|
import { PostComponents } from "../../../src/components/blog/posts";
|
||||||
import { Card } from "../../../src/components/Layout";
|
import { Card } from "../../../src/components/Layout";
|
||||||
@@ -50,54 +51,58 @@ export default async function BlogPostPage({
|
|||||||
<main id="post-content">
|
<main id="post-content">
|
||||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||||
<div className="max-w-5xl mx-auto px-0 sm:px-4 md:px-0">
|
<div className="max-w-5xl mx-auto px-0 sm:px-4 md:px-0">
|
||||||
<Card
|
<Reveal delay={0.4} width="100%">
|
||||||
variant="glass"
|
<Card
|
||||||
techBorder
|
variant="glass"
|
||||||
className="relative overflow-hidden rounded-none sm:rounded-3xl"
|
techBorder
|
||||||
>
|
className="relative overflow-hidden rounded-none sm:rounded-3xl"
|
||||||
{/* Decorative background grid inside the card */}
|
>
|
||||||
<div className="absolute inset-0 opacity-[0.03] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]" />
|
{/* Decorative background grid inside the card */}
|
||||||
|
<div className="absolute inset-0 opacity-[0.03] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]" />
|
||||||
|
|
||||||
<div className="relative z-10 px-5 py-10 md:px-16 md:py-20">
|
<div className="relative z-10 px-5 py-10 md:px-16 md:py-20">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-[9px] md:text-[10px] font-bold text-slate-400 mb-10 md:mb-12 uppercase tracking-[0.2em] border-b border-slate-100 pb-6">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-[9px] md:text-[10px] font-bold text-slate-400 mb-10 md:mb-12 uppercase tracking-[0.2em] border-b border-slate-100 pb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="w-2 h-2 rounded-full bg-slate-300" />
|
<span className="w-2 h-2 rounded-full bg-slate-300" />
|
||||||
<time dateTime={post.date}>{formattedDate}</time>
|
<time dateTime={post.date}>{formattedDate}</time>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4 sm:gap-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-slate-200 hidden sm:inline">|</span>
|
|
||||||
<span>{readingTime} min Lesezeit</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<div className="flex items-center gap-4 sm:gap-6">
|
||||||
{slug.substring(0, 4).toUpperCase()}-
|
<div className="flex items-center gap-2">
|
||||||
{Math.floor(Math.random() * 999)}
|
<span className="text-slate-200 hidden sm:inline">
|
||||||
</span>
|
|
|
||||||
</div>
|
</span>
|
||||||
</div>
|
<span>{readingTime} min Lesezeit</span>
|
||||||
|
</div>
|
||||||
{post.tags && post.tags.length > 0 && (
|
<span>
|
||||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
{slug.substring(0, 4).toUpperCase()}-
|
||||||
{post.tags.map((tag, index) => (
|
{Math.floor(Math.random() * 999)}
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{PostContent ? (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<PostContent />
|
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||||
) : (
|
{post.tags.map((tag, index) => (
|
||||||
<div className="p-8 bg-slate-50 border border-slate-200 rounded-lg italic text-slate-500">
|
<span
|
||||||
Inhalt wird bald veröffentlicht...
|
key={tag}
|
||||||
</div>
|
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||||
)}
|
>
|
||||||
</div>
|
#{tag}
|
||||||
</Card>
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{PostContent ? (
|
||||||
|
<PostContent />
|
||||||
|
) : (
|
||||||
|
<div className="p-8 bg-slate-50 border border-slate-200 rounded-lg italic text-slate-500">
|
||||||
|
Inhalt wird bald veröffentlicht...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useState, useEffect } from "react";
|
|||||||
import { MediumCard } from "../../src/components/MediumCard";
|
import { MediumCard } from "../../src/components/MediumCard";
|
||||||
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
|
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
|
||||||
import { blogPosts } from "../../src/data/blogPosts";
|
import { blogPosts } from "../../src/data/blogPosts";
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
|
||||||
import { SectionHeader } from "../../src/components/SectionHeader";
|
import { SectionHeader } from "../../src/components/SectionHeader";
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
@@ -124,7 +123,7 @@ export default function BlogPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-6 max-w-3xl mx-auto w-full">
|
<div className="grid grid-cols-1 gap-6 max-w-3xl mx-auto w-full">
|
||||||
{postsToShow.map((post, i) => (
|
{postsToShow.map((post, i) => (
|
||||||
<Reveal key={post.slug} delay={0.05 * (i % 6)} width="100%">
|
<Reveal key={post.slug} delay={0.05 * i} width="100%">
|
||||||
<MediumCard post={post} />
|
<MediumCard post={post} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
@@ -134,7 +133,7 @@ export default function BlogPage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<div className="flex justify-center pt-8">
|
<div className="flex justify-center pt-8">
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1} width="fit-content">
|
||||||
<button
|
<button
|
||||||
onClick={loadMore}
|
onClick={loadMore}
|
||||||
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
|
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
|
||||||
|
|||||||
@@ -205,11 +205,11 @@ export default function KLZCablesCaseStudy() {
|
|||||||
icon: <Cpu className="w-5 h-5 text-slate-400" />,
|
icon: <Cpu className="w-5 h-5 text-slate-400" />,
|
||||||
},
|
},
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<motion.div
|
<Reveal
|
||||||
key={i}
|
key={i}
|
||||||
initial={{ x: -20, opacity: 0 }}
|
direction="right"
|
||||||
whileInView={{ x: 0, opacity: 1 }}
|
delay={0.5 + i * 0.1}
|
||||||
transition={{ delay: 0.5 + i * 0.1, duration: 0.5 }}
|
width="100%"
|
||||||
className="flex gap-4 md:gap-6 border-b border-slate-200/50 pb-6 last:border-0 last:pb-0"
|
className="flex gap-4 md:gap-6 border-b border-slate-200/50 pb-6 last:border-0 last:pb-0"
|
||||||
>
|
>
|
||||||
<div className="shrink-0 mt-1">{item.icon}</div>
|
<div className="shrink-0 mt-1">{item.icon}</div>
|
||||||
@@ -221,7 +221,7 @@ export default function KLZCablesCaseStudy() {
|
|||||||
{item.desc}
|
{item.desc}
|
||||||
</BodyText>
|
</BodyText>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
import Image from "next/image";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
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 { Card } from "../../src/components/Layout";
|
||||||
import { Button } from "../../src/components/Button";
|
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 { ArrowRight } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
@@ -15,131 +15,132 @@ export default function CaseStudiesPage() {
|
|||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<AbstractCircuit />
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
{/* Featured Case Study Hero */}
|
||||||
title={
|
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||||
<>
|
<div className="space-y-12 md:space-y-24">
|
||||||
Case <span className="text-slate-400">Studies.</span>
|
<Reveal>
|
||||||
</>
|
<div className="space-y-6 max-w-4xl">
|
||||||
}
|
<H3 className="text-4xl md:text-8xl tracking-tighter leading-none">
|
||||||
description="Ergebnisse statt Versprechen. Was ich gebaut habe und was es bewirkt."
|
Case <span className="text-slate-400">Studies.</span>
|
||||||
backgroundSymbol="C"
|
</H3>
|
||||||
/>
|
<LeadText className="text-lg md:text-2xl text-slate-400 max-w-2xl">
|
||||||
|
Ergebnisse statt Versprechen. Dokumentierte Architektur-Lösungen
|
||||||
|
für komplexe Anforderungen.
|
||||||
|
</LeadText>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
{/* Featured Case Study */}
|
<Reveal>
|
||||||
<Section
|
<a href="/case-studies/klz-cables" className="block group">
|
||||||
number="01"
|
<Card
|
||||||
title="Showcase"
|
variant="glass"
|
||||||
borderTop
|
padding="none"
|
||||||
effects={<GradientMesh variant="metallic" className="opacity-70" />}
|
techBorder
|
||||||
>
|
className="overflow-hidden relative group min-h-[400px] md:min-h-[500px] flex flex-col md:flex-row"
|
||||||
<Reveal>
|
>
|
||||||
<a href="/case-studies/klz-cables" className="block group">
|
{/* Brand Gradient Background */}
|
||||||
<Card
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(14,165,233,0.08)_0%,transparent_50%),radial-gradient(circle_at_70%_70%,rgba(99,102,241,0.05)_0%,transparent_50%)]" />
|
||||||
variant="glass"
|
|
||||||
padding="none"
|
|
||||||
techBorder
|
|
||||||
className="overflow-hidden relative group min-h-[400px] md:min-h-[500px] flex flex-col md:flex-row"
|
|
||||||
>
|
|
||||||
{/* Brand Gradient Background */}
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(14,165,233,0.08)_0%,transparent_50%),radial-gradient(circle_at_70%_70%,rgba(99,102,241,0.05)_0%,transparent_50%)]" />
|
|
||||||
|
|
||||||
{/* Left Column: Content */}
|
{/* Left Column: Content */}
|
||||||
<div className="flex-1 p-4 md:p-12 relative z-10 flex flex-col justify-between">
|
<div className="flex-1 p-4 md:p-12 relative z-10 flex flex-col justify-between">
|
||||||
<div className="space-y-4 md:space-y-8">
|
<div className="space-y-4 md:space-y-8">
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
<img
|
<Image
|
||||||
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
||||||
alt="KLZ Logo"
|
alt="KLZ Logo"
|
||||||
className="h-6 md:h-8 invert opacity-80 group-hover:opacity-100 transition-opacity duration-500"
|
width={32}
|
||||||
/>
|
height={32}
|
||||||
<div className="h-px w-8 md:w-12 bg-slate-100" />
|
className="h-6 md:h-8 w-auto invert opacity-80 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
<Label className="text-slate-400 text-[9px] md:text-[10px]">
|
|
||||||
Case Study 2025
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 md:space-y-4">
|
|
||||||
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
|
||||||
KLZ <span className="text-slate-300">Cables</span>
|
|
||||||
</H3>
|
|
||||||
<LeadText className="text-slate-500 text-base md:text-xl max-w-xl leading-relaxed">
|
|
||||||
Engineering eines industriellen B2B-Systems mit
|
|
||||||
<span className="text-slate-900 font-medium">
|
|
||||||
{" "}
|
|
||||||
automatisierter Asset-Pipeline
|
|
||||||
</span>{" "}
|
|
||||||
und hochperformantem Headless-Stack.
|
|
||||||
</LeadText>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pt-1 md:pt-2">
|
|
||||||
{["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map(
|
|
||||||
(tag, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="px-2 py-0.5 md:px-2.5 md:py-1 border border-slate-100 bg-white/50 rounded-md text-[8px] md:text-[9px] font-mono text-slate-400 uppercase tracking-widest group-hover:border-slate-300 transition-colors duration-500"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-8 md:pt-12">
|
|
||||||
<div className="inline-flex items-center gap-2 md:gap-3 text-[10px] md:text-sm font-bold text-slate-400 group-hover:text-slate-900 transition-all duration-500">
|
|
||||||
<span>EXPLORE PROJECT</span>
|
|
||||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-2 transition-transform duration-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Visual/Technical Decor */}
|
|
||||||
<div className="w-full md:w-1/3 min-h-[150px] md:min-h-0 bg-slate-50 relative overflow-hidden border-t md:border-t-0 md:border-l border-slate-100">
|
|
||||||
<div className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[6px] md:text-[8px] p-4 flex flex-col gap-1 overflow-hidden">
|
|
||||||
{Array.from({ length: 40 }).map((_, i) => (
|
|
||||||
<div key={i} className="whitespace-nowrap">
|
|
||||||
{Array.from({ length: 15 })
|
|
||||||
.map((_, j) => (
|
|
||||||
<span
|
|
||||||
key={j}
|
|
||||||
className={
|
|
||||||
Math.random() > 0.5
|
|
||||||
? "text-slate-900"
|
|
||||||
: "text-slate-400"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Math.floor(Math.random() * 2)}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
.join(" ")}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Abstract "Cable" lines */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center p-8 md:p-12">
|
|
||||||
<div className="w-full h-full relative">
|
|
||||||
{[1, 2, 3].map((v) => (
|
|
||||||
<motion.div
|
|
||||||
key={v}
|
|
||||||
initial={{ scaleY: 0 }}
|
|
||||||
whileInView={{ scaleY: 1 }}
|
|
||||||
transition={{ duration: 2, delay: 0.5 + v * 0.2 }}
|
|
||||||
className="absolute inset-y-0 border-r border-slate-200 origin-top"
|
|
||||||
style={{ right: `${v * 25}%` }}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="h-px w-8 md:w-12 bg-slate-100" />
|
||||||
|
<Label className="text-slate-400 text-[9px] md:text-[10px]">
|
||||||
|
Case Study 2025
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 md:space-y-4">
|
||||||
|
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
||||||
|
KLZ <span className="text-slate-300">Cables</span>
|
||||||
|
</H3>
|
||||||
|
<LeadText className="text-slate-500 text-base md:text-xl max-w-xl leading-relaxed">
|
||||||
|
Engineering eines industriellen B2B-Systems mit
|
||||||
|
<span className="text-slate-900 font-medium">
|
||||||
|
{" "}
|
||||||
|
automatisierter Asset-Pipeline
|
||||||
|
</span>{" "}
|
||||||
|
und hochperformantem Headless-Stack.
|
||||||
|
</LeadText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 pt-1 md:pt-2">
|
||||||
|
{["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map(
|
||||||
|
(tag, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="px-2 py-0.5 md:px-2.5 md:py-1 border border-slate-100 bg-white/50 rounded-md text-[8px] md:text-[9px] font-mono text-slate-400 uppercase tracking-widest group-hover:border-slate-300 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-8 md:pt-12">
|
||||||
|
<div className="inline-flex items-center gap-2 md:gap-3 text-[10px] md:text-sm font-bold text-slate-400 group-hover:text-slate-900 transition-all duration-500">
|
||||||
|
<span>EXPLORE PROJECT</span>
|
||||||
|
<ArrowRight className="w-4 h-4 group-hover:translate-x-2 transition-transform duration-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Visual/Technical Decor */}
|
||||||
|
<div className="w-full md:w-1/3 min-h-[150px] md:min-h-0 bg-slate-50 relative overflow-hidden border-t md:border-t-0 md:border-l border-slate-100">
|
||||||
|
<div className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[6px] md:text-[8px] p-4 flex flex-col gap-1 overflow-hidden">
|
||||||
|
{Array.from({ length: 40 }).map((_, i) => (
|
||||||
|
<div key={i} className="whitespace-nowrap">
|
||||||
|
{Array.from({ length: 15 })
|
||||||
|
.map((_, j) => (
|
||||||
|
<span
|
||||||
|
key={j}
|
||||||
|
className={
|
||||||
|
Math.random() > 0.5
|
||||||
|
? "text-slate-900"
|
||||||
|
: "text-slate-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Math.floor(Math.random() * 2)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
.join(" ")}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute bottom-4 right-4 md:bottom-8 md:right-8 text-[8px] md:text-[10px] font-mono text-slate-300 rotate-90 origin-right uppercase tracking-[0.3em]">
|
{/* Abstract "Cable" lines */}
|
||||||
Industrial Grade
|
<div className="absolute inset-0 flex items-center justify-center p-8 md:p-12">
|
||||||
|
<div className="w-full h-full relative">
|
||||||
|
{[1, 2, 3].map((v) => (
|
||||||
|
<motion.div
|
||||||
|
key={v}
|
||||||
|
initial={{ scaleY: 0 }}
|
||||||
|
whileInView={{ scaleY: 1 }}
|
||||||
|
transition={{ duration: 2, delay: 0.5 + v * 0.2 }}
|
||||||
|
className="absolute inset-y-0 border-r border-slate-200 origin-top"
|
||||||
|
style={{ right: `${v * 25}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 right-4 md:bottom-8 md:right-8 text-[8px] md:text-[10px] font-mono text-slate-300 rotate-90 origin-right uppercase tracking-[0.3em]">
|
||||||
|
Industrial Grade
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
</a>
|
||||||
</a>
|
</Reveal>
|
||||||
</Reveal>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Coming Soon */}
|
{/* Coming Soon */}
|
||||||
|
|||||||
@@ -1,101 +1,19 @@
|
|||||||
import { PageHeader } from "../../src/components/PageHeader";
|
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
|
||||||
import { Section } from "../../src/components/Section";
|
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 { ContactForm } from "../../src/components/ContactForm";
|
||||||
import { GradientMesh, AbstractCircuit } from "../../src/components/Effects";
|
import { AbstractCircuit } from "../../src/components/Effects";
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white min-h-screen overflow-hidden relative">
|
<div className="flex flex-col bg-white min-h-screen overflow-hidden relative">
|
||||||
<AbstractCircuit />
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
Kontakt<span className="text-slate-200">.</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
description="Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich zeitnah bei Ihnen."
|
|
||||||
backgroundSymbol="@"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
borderTop
|
containerVariant="wide"
|
||||||
effects={
|
effects={<></>}
|
||||||
<>
|
className="pt-24 pb-12 md:pt-32 md:pb-20"
|
||||||
<GradientMesh variant="metallic" className="opacity-70" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-12 lg:gap-24">
|
{/* Full-width Form */}
|
||||||
{/* Form */}
|
<ContactForm />
|
||||||
<div className="lg:col-span-7">
|
|
||||||
<Reveal>
|
|
||||||
<Card
|
|
||||||
variant="glass"
|
|
||||||
padding="normal"
|
|
||||||
className="relative overflow-hidden p-5 md:p-12"
|
|
||||||
>
|
|
||||||
<ContactForm />
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<div className="lg:col-span-5 space-y-4 md:space-y-8">
|
|
||||||
<Reveal delay={0.2}>
|
|
||||||
<Card
|
|
||||||
variant="glass"
|
|
||||||
padding="normal"
|
|
||||||
techBorder
|
|
||||||
className="group"
|
|
||||||
>
|
|
||||||
<div className="space-y-3 md:space-y-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
||||||
<Label className="text-slate-900 text-xs md:text-sm">
|
|
||||||
Verfügbarkeit
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<LeadText className="text-lg md:text-xl text-slate-400">
|
|
||||||
Aktuell nehme ich Projekte für{" "}
|
|
||||||
<span className="text-slate-900 font-bold">Q2 2026</span>{" "}
|
|
||||||
an.
|
|
||||||
</LeadText>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={0.3}>
|
|
||||||
<Card variant="glass" padding="normal" className="group">
|
|
||||||
<div className="space-y-3 md:space-y-4">
|
|
||||||
<Label className="text-slate-900 text-xs md:text-sm">
|
|
||||||
Direkt per E-Mail
|
|
||||||
</Label>
|
|
||||||
<a
|
|
||||||
href="mailto:marc@mintel.me"
|
|
||||||
className="block text-lg md:text-2xl font-bold text-slate-900 hover:text-slate-400 transition-colors duration-500 border-b border-slate-100 hover:border-slate-400 pb-1"
|
|
||||||
>
|
|
||||||
marc@mintel.me
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={0.4}>
|
|
||||||
<div className="p-5 md:p-6 space-y-2 md:space-y-3 rounded-2xl border border-slate-50 bg-slate-50/30">
|
|
||||||
<Label className="text-slate-400 text-xs md:text-sm">
|
|
||||||
Antwortzeit
|
|
||||||
</Label>
|
|
||||||
<H3 className="text-lg md:text-2xl text-slate-300">
|
|
||||||
<span className="text-slate-900">< 24h</span> an Werktagen.
|
|
||||||
</H3>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,41 +1,60 @@
|
|||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { blogPosts } from '../../../src/data/blogPosts';
|
import { blogPosts } from "../../../src/data/blogPosts";
|
||||||
import { MediumCard } from '../../../src/components/MediumCard';
|
import { MediumCard } from "../../../src/components/MediumCard";
|
||||||
|
import { Reveal } from "../../../src/components/Reveal";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const allTags = Array.from(new Set(blogPosts.flatMap(post => post.tags || [])));
|
const allTags = Array.from(
|
||||||
return allTags.map(tag => ({
|
new Set(blogPosts.flatMap((post) => post.tags || [])),
|
||||||
|
);
|
||||||
|
return allTags.map((tag) => ({
|
||||||
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 { tag } = await params;
|
||||||
const posts = blogPosts.filter(post => post.tags?.includes(tag));
|
const posts = blogPosts.filter((post) => post.tags?.includes(tag));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
<Reveal>
|
||||||
Posts tagged <span className="highlighter-yellow px-2 rounded">{tag}</span>
|
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
||||||
</h1>
|
Posts tagged{" "}
|
||||||
<p className="text-slate-600">
|
<span className="highlighter-yellow px-2 rounded">{tag}</span>
|
||||||
{posts.length} post{posts.length === 1 ? '' : 's'}
|
</h1>
|
||||||
</p>
|
</Reveal>
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
{posts.length} post{posts.length === 1 ? "" : "s"}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{posts.map(post => (
|
{posts.map((post, i) => (
|
||||||
<MediumCard key={post.slug} post={post} />
|
<Reveal key={post.slug} delay={0.1 + i * 0.05} width="100%">
|
||||||
|
<MediumCard post={post} />
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-slate-200">
|
<Reveal delay={0.3}>
|
||||||
<Link href="/blog" className="text-slate-600 hover:text-slate-900 inline-flex items-center">
|
<div className="mt-8 pt-6 border-t border-slate-200">
|
||||||
← Back to blog
|
<Link
|
||||||
</Link>
|
href="/blog"
|
||||||
</div>
|
className="text-slate-600 hover:text-slate-900 inline-flex items-center"
|
||||||
|
>
|
||||||
|
← Back to blog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,141 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { Container } from '../../../src/components/Layout';
|
import { Container } from "../../../src/components/Layout";
|
||||||
import { Label } from '../../../src/components/Typography';
|
import { Label } from "../../../src/components/Typography";
|
||||||
import { Check, ArrowLeft, Zap, ExternalLink } from 'lucide-react';
|
import { Check, ArrowLeft, Zap, ExternalLink } from "lucide-react";
|
||||||
import { technologies } from './data';
|
import { technologies } from "./data";
|
||||||
|
import { Reveal } from "../../../src/components/Reveal";
|
||||||
|
|
||||||
export default function TechnologyContent({ slug }: { slug: string }) {
|
export default function TechnologyContent({ slug }: { slug: string }) {
|
||||||
const tech = technologies[slug];
|
const tech = technologies[slug];
|
||||||
|
|
||||||
if (!tech) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-4xl font-bold mb-4">Technology Not Found</h1>
|
|
||||||
<Link href="/" className="text-blue-600 hover:underline">Return Home</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Icon = tech.icon;
|
|
||||||
|
|
||||||
|
if (!tech) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white min-h-screen text-slate-900 pb-24">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="bg-slate-50 border-b border-slate-200">
|
<div className="text-center">
|
||||||
<Container className="py-24">
|
<h1 className="text-4xl font-bold mb-4">Technology Not Found</h1>
|
||||||
<Link href="/case-studies/klz-cables" className="inline-flex items-center text-sm font-bold text-slate-500 hover:text-slate-900 mb-8 transition-colors">
|
<Link href="/" className="text-blue-600 hover:underline">
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Case Study
|
Return Home
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row items-start gap-8">
|
|
||||||
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
|
|
||||||
<Icon className="w-12 h-12" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Label className="text-slate-400 mb-2">TECHNOLOGY DEEP DIVE</Label>
|
|
||||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">{tech.title}</h1>
|
|
||||||
<p className="text-xl text-slate-500 font-medium">{tech.subtitle}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Container className="py-16">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
|
||||||
<div className="lg:col-span-8 space-y-12">
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
|
|
||||||
<p className="text-xl leading-relaxed text-slate-700">{tech.description}</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
|
||||||
<Zap className="w-6 h-6 text-amber-500" /> Why I use it
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{tech.benefits.map((benefit, i) => (
|
|
||||||
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
||||||
<Check className="w-5 h-5 text-green-600 shrink-0" />
|
|
||||||
<span className="font-medium text-slate-700">{benefit}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-blue-50 border border-blue-100 rounded-2xl p-8">
|
|
||||||
<Label className="text-blue-600 mb-2">CUSTOMER IMPACT</Label>
|
|
||||||
<h3 className="text-2xl font-bold text-slate-900 mb-4">What does this mean for you?</h3>
|
|
||||||
<p className="text-lg text-slate-700 leading-relaxed">
|
|
||||||
{tech.customerValue}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:col-span-4 space-y-8">
|
|
||||||
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 sticky top-24">
|
|
||||||
<h3 className="font-bold text-slate-900 mb-4">Related Technologies</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{tech.related.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.slug}
|
|
||||||
href={`/technologies/${item.slug}`}
|
|
||||||
className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-slate-400 hover:shadow-sm transition-all group"
|
|
||||||
>
|
|
||||||
<span className="font-medium text-slate-700 group-hover:text-slate-900">{item.name}</span>
|
|
||||||
<ExternalLink className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = tech.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white min-h-screen text-slate-900 pb-24">
|
||||||
|
<div className="bg-slate-50 border-b border-slate-200">
|
||||||
|
<Container className="py-24">
|
||||||
|
<Reveal>
|
||||||
|
<Link
|
||||||
|
href="/case-studies/klz-cables"
|
||||||
|
className="inline-flex items-center text-sm font-bold text-slate-500 hover:text-slate-900 mb-8 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Case Study
|
||||||
|
</Link>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row items-start gap-8">
|
||||||
|
<Reveal>
|
||||||
|
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
|
||||||
|
<Icon className="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<Label className="text-slate-400 mb-2">
|
||||||
|
TECHNOLOGY DEEP DIVE
|
||||||
|
</Label>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.2}>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||||
|
{tech.title}
|
||||||
|
</h1>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.3}>
|
||||||
|
<p className="text-xl text-slate-500 font-medium">
|
||||||
|
{tech.subtitle}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Container className="py-16">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||||
|
<div className="lg:col-span-8 space-y-12">
|
||||||
|
<section>
|
||||||
|
<Reveal>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
|
||||||
|
<p className="text-xl leading-relaxed text-slate-700">
|
||||||
|
{tech.description}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Reveal>
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||||
|
<Zap className="w-6 h-6 text-amber-500" /> Why I use it
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{tech.benefits.map((benefit, i) => (
|
||||||
|
<Reveal key={i} delay={0.1 + i * 0.05}>
|
||||||
|
<div className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100 h-full">
|
||||||
|
<Check className="w-5 h-5 text-green-600 shrink-0" />
|
||||||
|
<span className="font-medium text-slate-700">
|
||||||
|
{benefit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Reveal delay={0.4}>
|
||||||
|
<section className="bg-blue-50 border border-blue-100 rounded-2xl p-8">
|
||||||
|
<Label className="text-blue-600 mb-2">CUSTOMER IMPACT</Label>
|
||||||
|
<h3 className="text-2xl font-bold text-slate-900 mb-4">
|
||||||
|
What does this mean for you?
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg text-slate-700 leading-relaxed">
|
||||||
|
{tech.customerValue}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-4 space-y-8">
|
||||||
|
<Reveal delay={0.5}>
|
||||||
|
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 sticky top-24">
|
||||||
|
<h3 className="font-bold text-slate-900 mb-4">
|
||||||
|
Related Technologies
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tech.related.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.slug}
|
||||||
|
href={`/technologies/${item.slug}`}
|
||||||
|
className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-slate-400 hover:shadow-sm transition-all group"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-slate-700 group-hover:text-slate-900">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
<ExternalLink className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
import {
|
import {
|
||||||
SystemArchitecture,
|
|
||||||
SpeedPerformance,
|
SpeedPerformance,
|
||||||
SolidFoundation,
|
SolidFoundation,
|
||||||
LayerSeparation,
|
LayerSeparation,
|
||||||
DirectService,
|
|
||||||
TaskDone,
|
TaskDone,
|
||||||
} from "../../src/components/Landing";
|
} from "../../src/components/Landing";
|
||||||
import {
|
import {
|
||||||
H3,
|
H3,
|
||||||
H4,
|
|
||||||
LeadText,
|
LeadText,
|
||||||
BodyText,
|
BodyText,
|
||||||
Label,
|
Label,
|
||||||
|
MonoLabel,
|
||||||
} from "../../src/components/Typography";
|
} from "../../src/components/Typography";
|
||||||
import { Card } from "../../src/components/Layout";
|
import { Card } from "../../src/components/Layout";
|
||||||
import { Button } from "../../src/components/Button";
|
import { Button } from "../../src/components/Button";
|
||||||
@@ -25,6 +22,9 @@ import {
|
|||||||
GradientMesh,
|
GradientMesh,
|
||||||
CodeSnippet,
|
CodeSnippet,
|
||||||
AbstractCircuit,
|
AbstractCircuit,
|
||||||
|
CMSVisualizer,
|
||||||
|
ArchitectureVisualizer,
|
||||||
|
ResultVisualizer,
|
||||||
} from "../../src/components/Effects";
|
} from "../../src/components/Effects";
|
||||||
import { Marker } from "../../src/components/Marker";
|
import { Marker } from "../../src/components/Marker";
|
||||||
|
|
||||||
@@ -33,86 +33,77 @@ export default function WebsitesPage() {
|
|||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<AbstractCircuit />
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||||
title={
|
<div className="space-y-12 md:space-y-24">
|
||||||
<>
|
<div className="space-y-6 md:space-y-10 max-w-5xl">
|
||||||
Websites, die <br />
|
<Reveal>
|
||||||
<span className="text-slate-400">
|
<div className="space-y-4">
|
||||||
<Marker color="rgba(255,235,59,0.5)">
|
<MonoLabel className="text-blue-500 tracking-[0.2em] text-[10px] md:text-xs">
|
||||||
einfach funktionieren.
|
SYSTEM ENGINEERING
|
||||||
</Marker>
|
</MonoLabel>
|
||||||
</span>
|
<H3 className="text-4xl md:text-8xl leading-[1.0] tracking-tighter">
|
||||||
</>
|
Websites, die <br />
|
||||||
}
|
<span className="text-slate-400">
|
||||||
description="Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur für maximale Performance."
|
<Marker color="rgba(255,235,59,0.5)">
|
||||||
backgroundSymbol="W"
|
einfach funktionieren.
|
||||||
className="px-5 md:px-0"
|
</Marker>
|
||||||
/>
|
</span>
|
||||||
|
</H3>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.2}>
|
||||||
|
<LeadText className="text-lg md:text-2xl max-w-2xl text-slate-500 md:text-slate-400 leading-relaxed">
|
||||||
|
Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur
|
||||||
|
für{" "}
|
||||||
|
<span className="text-slate-900 font-bold underline decoration-slate-200 underline-offset-8">
|
||||||
|
maximale Performance
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</LeadText>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 01: Architektur – WIE ich baue */}
|
<div className="space-y-12">
|
||||||
<Section
|
<Reveal delay={0.3} direction="up">
|
||||||
number="01"
|
<ArchitectureVisualizer />
|
||||||
title="Architektur"
|
</Reveal>
|
||||||
borderTop
|
|
||||||
illustration={<SystemArchitecture className="w-24 h-24" />}
|
|
||||||
>
|
|
||||||
<div className="space-y-8 md:space-y-12">
|
|
||||||
<Reveal>
|
|
||||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
|
||||||
Systeme, nicht Broschüren. <br />
|
|
||||||
<span className="text-slate-400">
|
|
||||||
Jede Website ist Ingenieursarbeit.
|
|
||||||
</span>
|
|
||||||
</H3>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={0.2}>
|
|
||||||
<LeadText className="text-lg md:text-2xl max-w-2xl text-slate-400">
|
|
||||||
Ich entwickle Websites von Grund auf – mit modernen Frameworks,
|
|
||||||
eigener Infrastruktur und einem Deployment-Prozess, der{" "}
|
|
||||||
<span className="text-slate-900">
|
|
||||||
<Marker delay={0.2} color="rgba(148,163,184,0.1)">
|
|
||||||
automatisiert und reproduzierbar
|
|
||||||
</Marker>
|
|
||||||
</span>{" "}
|
|
||||||
ist.
|
|
||||||
</LeadText>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
{/* Tech Stack Visual */}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<Reveal delay={0.4}>
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
|
|
||||||
{[
|
{[
|
||||||
{ label: "Next.js", sub: "Framework" },
|
{
|
||||||
{ label: "TypeScript", sub: "Sprache" },
|
label: "Next.js",
|
||||||
{ label: "Docker", sub: "Infrastruktur" },
|
sub: "Architecture",
|
||||||
{ label: "Directus", sub: "CMS" },
|
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) => (
|
].map((item, i) => (
|
||||||
<Card
|
<Reveal key={i} delay={0.4 + i * 0.1}>
|
||||||
key={i}
|
<div className="space-y-2 p-6 rounded-2xl border border-slate-50 bg-white shadow-sm hover:border-slate-200 transition-all group">
|
||||||
variant="glass"
|
<Label className="text-slate-900 group-hover:text-blue-600 transition-colors uppercase tracking-widest text-[10px]">
|
||||||
padding="small"
|
|
||||||
techBorder
|
|
||||||
className="group text-center"
|
|
||||||
>
|
|
||||||
<div className="space-y-1 md:space-y-2">
|
|
||||||
<Label className="text-slate-900 text-xs md:text-sm">
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</Label>
|
</Label>
|
||||||
<span className="block text-[8px] md:text-[9px] font-mono text-slate-300 uppercase tracking-widest">
|
<BodyText className="text-xs text-slate-400">
|
||||||
{item.sub}
|
{item.desc}
|
||||||
</span>
|
</BodyText>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</div>
|
||||||
|
|
||||||
{/* Decorative Code Snippet */}
|
|
||||||
<Reveal delay={0.6}>
|
|
||||||
<div className="max-w-md opacity-70">
|
|
||||||
<CodeSnippet variant="code" />
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -139,7 +130,7 @@ export default function WebsitesPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-center">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-center">
|
||||||
<div className="md:col-span-12 lg:col-span-7 space-y-6 md:space-y-8">
|
<div className="md:col-span-12 lg:col-span-7 space-y-6 md:space-y-8">
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-lg md:text-xl text-slate-400">
|
<LeadText className="text-lg md:text-xl text-slate-500 md:text-slate-400">
|
||||||
Jede Seite wird vorab gerendert und über ein CDN ausgeliefert.
|
Jede Seite wird vorab gerendert und über ein CDN ausgeliefert.
|
||||||
Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "}
|
Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "}
|
||||||
<span className="text-slate-900">Reproduzierbar.</span>
|
<span className="text-slate-900">Reproduzierbar.</span>
|
||||||
@@ -199,7 +190,7 @@ export default function WebsitesPage() {
|
|||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-500 md:text-slate-400">
|
||||||
Ihre Website besteht aus{" "}
|
Ihre Website besteht aus{" "}
|
||||||
<span className="text-slate-900">Ihrem Code</span>. Kein
|
<span className="text-slate-900">Ihrem Code</span>. Kein
|
||||||
WordPress, kein Wix, keine Blackbox. Alles versioniert, alles
|
WordPress, kein Wix, keine Blackbox. Alles versioniert, alles
|
||||||
@@ -249,58 +240,90 @@ export default function WebsitesPage() {
|
|||||||
illustration={<LayerSeparation className="w-24 h-24" />}
|
illustration={<LayerSeparation className="w-24 h-24" />}
|
||||||
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-8 md:space-y-12">
|
<div className="space-y-12 md:space-y-20">
|
||||||
<Reveal>
|
<div className="space-y-6 md:space-y-10 max-w-5xl">
|
||||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
<Reveal>
|
||||||
Inhalte pflegen <br />
|
<div className="space-y-4">
|
||||||
<span className="text-slate-400">ohne Angst.</span>
|
<MonoLabel className="text-blue-500 tracking-[0.2em] text-[10px] md:text-xs">
|
||||||
</H3>
|
ARCHITECTURAL SEPARATION
|
||||||
</Reveal>
|
</MonoLabel>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-start">
|
<H3 className="text-4xl md:text-7xl leading-[1.1] tracking-tighter">
|
||||||
<div className="md:col-span-12 lg:col-span-7">
|
Inhalte pflegen <br />
|
||||||
<Reveal delay={0.2}>
|
<span className="text-slate-400 italic font-serif">
|
||||||
<LeadText className="text-lg md:text-2xl text-slate-400">
|
ohne Angst.
|
||||||
Technik und Inhalt sind{" "}
|
|
||||||
<span className="text-slate-900">
|
|
||||||
<Marker color="rgba(255,235,59,0.5)">
|
|
||||||
strikt getrennt
|
|
||||||
</Marker>
|
|
||||||
</span>
|
</span>
|
||||||
. Sie bearbeiten Texte und Bilder in einem intuitiven System –
|
</H3>
|
||||||
das Design bleibt geschützt.
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.2}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<LeadText className="text-lg md:text-2xl text-slate-500 md:text-slate-400 leading-relaxed max-w-3xl">
|
||||||
|
Vergessen Sie zerschossene Layouts nach einem Textupdate.
|
||||||
|
Meine Websites trennen{" "}
|
||||||
|
<span className="text-slate-900 font-bold underline decoration-blue-500/30 underline-offset-8">
|
||||||
|
Daten von Design
|
||||||
|
</span>
|
||||||
|
.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
||||||
|
<div className="lg:col-span-8 relative">
|
||||||
|
<Reveal delay={0.4} direction="up">
|
||||||
|
<CMSVisualizer className="w-full mx-auto" />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-12 lg:col-span-5">
|
|
||||||
<Reveal delay={0.4}>
|
<div className="lg:col-span-4 space-y-8">
|
||||||
<Card
|
<Reveal delay={0.5}>
|
||||||
variant="glass"
|
<div className="space-y-4">
|
||||||
padding="normal"
|
<BodyText className="text-slate-500 leading-relaxed">
|
||||||
techBorder
|
Durch eine krisenfeste Headless-Architektur (Directus)
|
||||||
className="space-y-6"
|
bewegen Sie sich in einem geschützten Sandkasten – während
|
||||||
>
|
das Frontend-System die visuelle Integrität Ihrer Marke
|
||||||
<div className="space-y-3">
|
garantiert.
|
||||||
<div className="flex items-center gap-3">
|
</BodyText>
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
|
||||||
<Label className="text-slate-900">Sie dürfen</Label>
|
<div className="flex flex-wrap gap-3">
|
||||||
</div>
|
{["Layout-Schutz", "Live-Vorschau", "Role-RBAC"].map(
|
||||||
<BodyText className="font-medium text-sm md:text-base">
|
(tag, i) => (
|
||||||
Texte, Bilder und Inhalte frei bearbeiten.
|
<div
|
||||||
</BodyText>
|
key={i}
|
||||||
|
className="px-3 py-1 bg-white border border-slate-100 rounded-full text-[10px] font-mono text-slate-400"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 opacity-40">
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
</Reveal>
|
||||||
<div className="w-2 h-2 rounded-full bg-slate-300"></div>
|
|
||||||
<Label>Geschützt</Label>
|
<Reveal delay={0.6}>
|
||||||
</div>
|
<div className="p-6 bg-slate-900 rounded-2xl shadow-xl text-[10px] font-mono text-white/50 space-y-3">
|
||||||
<BodyText className="line-through text-xs md:text-base">
|
<div className="flex justify-between items-center text-white/90">
|
||||||
Design, Layout, Code-Struktur.
|
<span>PROTOCOL</span>
|
||||||
</BodyText>
|
<span className="text-green-500 font-bold">ENFORCED</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<p className="leading-tight">
|
||||||
|
Website architecture validates all CMS payloads against the
|
||||||
|
design schema before rendering.
|
||||||
|
</p>
|
||||||
|
<div className="pt-2 border-t border-white/10 flex items-center justify-between">
|
||||||
|
<span>INTEGRITY</span>
|
||||||
|
<span className="text-white">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Reveal delay={0.7}>
|
||||||
|
<div className="p-px w-full bg-gradient-to-r from-transparent via-slate-100 to-transparent" />
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -311,52 +334,40 @@ export default function WebsitesPage() {
|
|||||||
borderTop
|
borderTop
|
||||||
illustration={<TaskDone className="w-24 h-24" />}
|
illustration={<TaskDone className="w-24 h-24" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-8 md:space-y-16">
|
<div className="space-y-12 md:space-y-24">
|
||||||
<Reveal>
|
<div className="max-w-4xl space-y-6">
|
||||||
<H3 className="text-2xl md:text-5xl tracking-tighter">
|
<Reveal>
|
||||||
Was Sie konkret <br />
|
<H3 className="text-4xl md:text-7xl leading-[1.1] tracking-tighter">
|
||||||
<span className="text-slate-400">bekommen.</span>
|
Was Sie konkret <br />
|
||||||
</H3>
|
<span className="text-slate-400">bekommen.</span>
|
||||||
</Reveal>
|
</H3>
|
||||||
|
</Reveal>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
<Reveal delay={0.2}>
|
||||||
{[
|
<LeadText className="text-lg md:text-2xl text-slate-500 md:text-slate-400 max-w-2xl">
|
||||||
{
|
Keine halben Sachen. Ich liefere Ihnen ein schlüsselfertiges
|
||||||
title: "Ihr Code",
|
System mit voller Kontrolle und Transparenz.
|
||||||
desc: "Vollständiger Quellcode, versioniert auf GitHub. Kein Vendor Lock-in.",
|
</LeadText>
|
||||||
},
|
</Reveal>
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Reveal key={i} delay={i * 0.1}>
|
|
||||||
<div className="space-y-3 md:space-y-4 group">
|
|
||||||
<div className="w-8 h-px bg-slate-200 group-hover:w-full transition-all duration-1000" />
|
|
||||||
<H4 className="text-lg md:text-2xl font-bold">
|
|
||||||
{item.title}
|
|
||||||
</H4>
|
|
||||||
<LeadText className="text-sm md:text-lg text-slate-400">
|
|
||||||
{item.desc}
|
|
||||||
</LeadText>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.3} direction="up">
|
||||||
<div className="pt-10 md:pt-16 border-t border-slate-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 md:gap-8">
|
<ResultVisualizer />
|
||||||
<div className="space-y-2">
|
</Reveal>
|
||||||
<Label>Bereit?</Label>
|
|
||||||
<LeadText className="text-xl md:text-2xl">
|
<Reveal delay={0.5}>
|
||||||
|
<div className="pt-10 md:pt-16 border-t border-slate-100 flex flex-col md:flex-row justify-between items-start md:items-center gap-12">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<MonoLabel className="text-blue-500">
|
||||||
|
BEREIT FÜR DEN NÄCHSTEN SCHRITT?
|
||||||
|
</MonoLabel>
|
||||||
|
<div className="text-xl md:text-3xl font-bold tracking-tight text-slate-900">
|
||||||
Lassen Sie uns über Ihr Projekt sprechen.
|
Lassen Sie uns über Ihr Projekt sprechen.
|
||||||
</LeadText>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button href="/contact" className="w-full md:w-auto">
|
<Button
|
||||||
|
href="/contact"
|
||||||
|
className="w-full md:w-auto h-16 px-10 text-lg rounded-2xl shadow-2xl shadow-blue-500/10"
|
||||||
|
>
|
||||||
Projekt anfragen
|
Projekt anfragen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
|
"nodemailer": "^8.0.1",
|
||||||
"playwright": "^1.58.1",
|
"playwright": "^1.58.1",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"puppeteer": "^24.36.1",
|
"puppeteer": "^24.36.1",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"@next/eslint-plugin-next": "^16.1.6",
|
"@next/eslint-plugin-next": "^16.1.6",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
|
"@types/nodemailer": "^7.0.10",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
48
apps/web/src/actions/contact.ts
Normal file
48
apps/web/src/actions/contact.ts
Normal file
@@ -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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 (
|
||||||
|
<div className="relative w-full min-h-[85vh] bg-white text-slate-900 flex flex-col overflow-hidden border border-slate-200 rounded-xl shadow-lg">
|
||||||
|
{/* Background: Geometric Dot Grid */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0 opacity-[0.4] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `radial-gradient(#cbd5e1 1px, transparent 1px)`,
|
||||||
|
backgroundSize: `24px 24px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Subtle Circuit Overlay */}
|
||||||
|
<div className="absolute inset-0 z-0 opacity-[0.05] pointer-events-none mix-blend-multiply">
|
||||||
|
<AbstractCircuit />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header (Functional) */}
|
||||||
|
<header className="relative z-10 flex flex-col border-b border-slate-200 bg-white/80 backdrop-blur-sm">
|
||||||
|
{/* Top Bar: Controls */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 md:px-8">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative flex items-center justify-center w-4 h-4">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 shadow-sm" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-bold text-slate-900 leading-none tracking-tight">
|
||||||
|
SYSTEM KONFIGURATOR
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono text-slate-400 uppercase tracking-widest mt-0.5">
|
||||||
|
Schritt {String(stepIndex + 1).padStart(2, "0")} /{" "}
|
||||||
|
{String(totalSteps).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleRestart}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold text-slate-400 hover:text-red-600 hover:bg-slate-50 transition-colors uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />{" "}
|
||||||
|
<span className="hidden md:inline">Zurücksetzen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Line */}
|
||||||
|
<div className="relative w-full h-1 bg-slate-100">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 left-0 h-full bg-slate-900"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${((stepIndex + 1) / totalSteps) * 100}%` }}
|
||||||
|
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<main className="flex-1 relative z-10 flex flex-col items-center justify-center p-6 md:p-12 w-full overflow-y-auto">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={stepIndex}
|
||||||
|
initial={{ opacity: 0, y: 15, filter: "blur(4px)" }}
|
||||||
|
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||||
|
exit={{ opacity: 0, y: -15, filter: "blur(4px)" }}
|
||||||
|
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="w-full max-w-5xl"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer / Controls */}
|
||||||
|
<footer className="relative z-10 flex flex-col md:flex-row items-center justify-between px-6 py-5 md:px-8 border-t border-slate-200 bg-white/90 backdrop-blur-sm gap-4">
|
||||||
|
{/* Left: Live Estimate (Integrated, No Overlay) */}
|
||||||
|
<div className="w-full md:w-auto flex items-center gap-6">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[10px] font-bold font-mono text-slate-400 uppercase tracking-widest mb-0.5">
|
||||||
|
Kalkuliertes Budget
|
||||||
|
</span>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-xl font-bold text-slate-900">
|
||||||
|
€{totalPrice.toLocaleString("de-DE")}
|
||||||
|
</span>
|
||||||
|
{monthlyPrice > 0 && (
|
||||||
|
<span className="text-xs font-medium text-slate-500">
|
||||||
|
+ €{monthlyPrice}/mtl.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Navigation */}
|
||||||
|
<div className="flex items-center gap-3 w-full md:w-auto justify-end">
|
||||||
|
{onPrev && (
|
||||||
|
<button
|
||||||
|
onClick={onPrev}
|
||||||
|
className="px-5 py-3 rounded-xl text-xs font-bold font-mono tracking-wider text-slate-500 hover:text-slate-900 hover:bg-slate-50 transition-all flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} /> ZURÜCK
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onNext && (
|
||||||
|
<button
|
||||||
|
onClick={onNext}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-8 py-3 bg-slate-900 text-white rounded-xl text-xs font-bold font-mono tracking-wider hover:bg-slate-800 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transform active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
{isSubmitting ? "VERARBEITE..." : "WEITER"}
|
||||||
|
{!isSubmitting && <ChevronRight size={16} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
145
apps/web/src/components/ContactForm/Configurator/Launchpad.tsx
Normal file
145
apps/web/src/components/ContactForm/Configurator/Launchpad.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="w-full max-w-3xl mx-auto space-y-12 pb-12">
|
||||||
|
<Reveal width="100%" delay={0.1}>
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-slate-100 text-[10px] font-bold uppercase tracking-widest text-slate-500 mb-2">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
System bereit
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-bold text-slate-900 tracking-tight">
|
||||||
|
Launch-Sequenz initialisieren
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-500 text-lg max-w-xl mx-auto leading-relaxed">
|
||||||
|
Bitte bestätigen Sie die finalen Parameter, um den Prozess zu
|
||||||
|
starten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal width="100%" delay={0.2}>
|
||||||
|
<div className="space-y-8 bg-white p-8 md:p-10 rounded-3xl border border-slate-100 shadow-xl shadow-slate-200/50">
|
||||||
|
{/* Email Input */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-bold font-mono text-slate-900 uppercase tracking-wider flex items-center gap-2">
|
||||||
|
<Mail size={14} className="text-slate-400" /> Kommunikations-Kanal
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline Selection */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-bold font-mono text-slate-900 uppercase tracking-wider flex items-center gap-2">
|
||||||
|
<Calendar size={14} className="text-slate-400" /> Zeitfenster
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ 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) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => setTimeline(t.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center p-4 rounded-xl border text-sm transition-all duration-200",
|
||||||
|
timeline === t.id
|
||||||
|
? "bg-slate-900 text-white border-slate-900 shadow-lg scale-[1.02]"
|
||||||
|
: "bg-white border-slate-200 text-slate-500 hover:border-slate-300 hover:bg-slate-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{t.label}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[10px] uppercase tracking-wide mt-1",
|
||||||
|
timeline === t.id ? "text-slate-400" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t.sub}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Input */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-bold font-mono text-slate-900 uppercase tracking-wider flex items-center gap-2">
|
||||||
|
<MessageSquare size={14} className="text-slate-400" />{" "}
|
||||||
|
Transmission Payload (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Spezielle Anforderungen, Briefing-Links oder Fragen..."
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-5 text-slate-900 font-medium min-h-[140px] focus:border-slate-400 focus:outline-none transition-all placeholder:text-slate-400 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal width="100%" delay={0.3}>
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.02,
|
||||||
|
boxShadow: "0 20px 40px -10px rgba(0,0,0,0.2)",
|
||||||
|
}}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={!isValid}
|
||||||
|
className="relative group w-full md:w-auto min-w-[300px] px-8 py-5 bg-slate-900 text-white text-lg font-bold font-mono tracking-widest rounded-xl overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed shadow-xl transition-all"
|
||||||
|
>
|
||||||
|
<span className="relative z-10 flex items-center justify-center gap-3">
|
||||||
|
ANFRAGE SENDEN{" "}
|
||||||
|
<Rocket
|
||||||
|
size={20}
|
||||||
|
className="group-hover:translate-x-1 transition-transform"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-green-600 to-green-500 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300 z-0" />
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-400 font-medium flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||||
|
Sichere 256-Bit verschlüsselte Übertragung
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
168
apps/web/src/components/ContactForm/Configurator/ModuleGrid.tsx
Normal file
168
apps/web/src/components/ContactForm/Configurator/ModuleGrid.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Check, Plus } from "lucide-react";
|
||||||
|
import { cn } from "../../../utils/cn";
|
||||||
|
import { Reveal } from "../../Reveal";
|
||||||
|
|
||||||
|
interface ModuleOption {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
priceEstimate?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModuleGridProps {
|
||||||
|
title: string;
|
||||||
|
options: ModuleOption[];
|
||||||
|
selected: string[];
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
maxSelection?: number;
|
||||||
|
otherCount?: number;
|
||||||
|
onOtherCountChange?: (val: number) => void;
|
||||||
|
otherLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModuleGrid = ({
|
||||||
|
title,
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
onToggle,
|
||||||
|
otherCount = 0,
|
||||||
|
onOtherCountChange,
|
||||||
|
otherLabel = "Zusätzliche Elemente",
|
||||||
|
}: ModuleGridProps) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Reveal width="100%" delay={0.05} direction="none">
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<div className="h-px bg-slate-200 flex-1" />
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 uppercase tracking-widest text-center min-w-max px-4">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div className="h-px bg-slate-200 flex-1" />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{options.map((option, i) => {
|
||||||
|
const isSelected = selected.includes(option.id);
|
||||||
|
return (
|
||||||
|
<Reveal
|
||||||
|
key={option.id}
|
||||||
|
width="100%"
|
||||||
|
delay={i * 0.05}
|
||||||
|
direction="up"
|
||||||
|
scale={0.95}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => onToggle(option.id)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col items-start p-6 rounded-2xl border text-left w-full h-full transition-all duration-300",
|
||||||
|
isSelected
|
||||||
|
? "bg-green-50/40 border-green-500 ring-4 ring-green-500/10 shadow-xl scale-[1.02] z-10"
|
||||||
|
: "bg-white border-slate-200 hover:border-slate-300 hover:shadow-lg hover:-translate-y-1 hover:z-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between w-full mb-4">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300 shadow-sm",
|
||||||
|
isSelected
|
||||||
|
? "bg-green-500 text-white shadow-green-500/30 ring-4 ring-green-100"
|
||||||
|
: "bg-slate-50 text-slate-500 group-hover:bg-slate-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.icon ||
|
||||||
|
(isSelected ? <Check size={24} /> : <Plus size={24} />)}
|
||||||
|
</div>
|
||||||
|
{option.priceEstimate && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"px-2.5 py-1 rounded text-[10px] font-bold font-mono uppercase tracking-wider transition-colors",
|
||||||
|
isSelected
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-slate-100 text-slate-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
+{option.priceEstimate}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-bold mb-2 transition-colors",
|
||||||
|
isSelected ? "text-green-900" : "text-slate-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</h4>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-sm leading-relaxed transition-colors",
|
||||||
|
isSelected ? "text-green-800/70" : "text-slate-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selection Check Circle */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-4 right-4 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all",
|
||||||
|
isSelected
|
||||||
|
? "border-green-500 bg-green-500 text-white scale-100 opacity-100"
|
||||||
|
: "border-slate-200 bg-transparent scale-90 opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check size={14} strokeWidth={3} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{onOtherCountChange && (
|
||||||
|
<div className="col-span-1 md:col-span-2 lg:col-span-3 mt-4">
|
||||||
|
<Reveal width="100%" delay={options.length * 0.05} direction="up">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between p-6 bg-slate-50 border border-dashed border-slate-300 rounded-2xl gap-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-slate-900 font-bold text-lg">
|
||||||
|
{otherLabel}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-500 text-sm">
|
||||||
|
Gibt es weitere Anforderungen, die oben nicht aufgeführt
|
||||||
|
sind?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 bg-white p-2 rounded-xl border border-slate-200 shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
onOtherCountChange(Math.max(0, otherCount - 1))
|
||||||
|
}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg border border-slate-100 hover:bg-slate-50 transition-colors text-slate-400 hover:text-slate-900 font-bold text-xl"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<div className="min-w-[40px] text-center font-mono font-bold text-2xl text-slate-900">
|
||||||
|
{otherCount}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onOtherCountChange(otherCount + 1)}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg border border-slate-100 hover:bg-slate-50 transition-colors text-slate-400 hover:text-slate-900 font-bold text-xl"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "../../../utils/cn";
|
||||||
|
import { ProjectType } from "../types";
|
||||||
|
import { Reveal } from "../../Reveal";
|
||||||
|
import { MessageSquareText, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
interface NarrativeInputProps {
|
||||||
|
name: string;
|
||||||
|
setName: (val: string) => void;
|
||||||
|
company: string;
|
||||||
|
setCompany: (val: string) => void;
|
||||||
|
projectType: ProjectType;
|
||||||
|
setProjectType: (val: ProjectType) => void;
|
||||||
|
onToggleFreeText?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
autoFocus,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="inline-grid items-center relative group mx-1">
|
||||||
|
{/* Invisible span to dictate width */}
|
||||||
|
<span className="col-start-1 row-start-1 opacity-0 pointer-events-none whitespace-pre border-b-2 border-transparent px-2 py-1 text-2xl md:text-5xl font-bold tracking-tight min-w-[140px]">
|
||||||
|
{value || placeholder}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Actual Input */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
className={cn(
|
||||||
|
"col-start-1 row-start-1 w-full h-full bg-transparent focus:outline-none px-2 py-1",
|
||||||
|
"text-2xl md:text-5xl font-bold tracking-tight transition-all duration-300",
|
||||||
|
"border-b-2",
|
||||||
|
value
|
||||||
|
? "text-slate-900 border-slate-900"
|
||||||
|
: "text-slate-300 border-slate-100 placeholder:text-slate-200 focus:border-green-500 focus:placeholder:text-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NarrativeInput = ({
|
||||||
|
name,
|
||||||
|
setName,
|
||||||
|
company,
|
||||||
|
setCompany,
|
||||||
|
projectType,
|
||||||
|
setProjectType,
|
||||||
|
onToggleFreeText,
|
||||||
|
}: NarrativeInputProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] text-left w-full px-4 max-w-5xl mx-auto">
|
||||||
|
<div className="w-full space-y-20">
|
||||||
|
{/* Header Section */}
|
||||||
|
<Reveal width="100%" delay={0.1}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||||
|
INITIALISIERUNG // SCHRITT_01
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap items-baseline gap-y-6 text-slate-900">
|
||||||
|
<span className="text-2xl md:text-5xl font-medium text-slate-400">
|
||||||
|
Hi, ich bin
|
||||||
|
</span>
|
||||||
|
<AutoInput
|
||||||
|
value={name}
|
||||||
|
onChange={setName}
|
||||||
|
placeholder="Ihr Name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<span className="text-2xl md:text-5xl font-medium text-slate-400">
|
||||||
|
von
|
||||||
|
</span>
|
||||||
|
<AutoInput
|
||||||
|
value={company}
|
||||||
|
onChange={setCompany}
|
||||||
|
placeholder="Firma / Projekt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Project Focus Section */}
|
||||||
|
<Reveal width="100%" delay={0.3} direction="up">
|
||||||
|
<div className="space-y-8 p-10 bg-slate-50/50 rounded-3xl border border-slate-100">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-[10px] font-mono text-slate-400 uppercase tracking-widest font-bold">
|
||||||
|
MISSION_OBJECTIVE
|
||||||
|
</span>
|
||||||
|
<div className="h-px bg-slate-200 flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{(["website", "web-app", "ecommerce"] as ProjectType[]).map(
|
||||||
|
(type) => {
|
||||||
|
const isActive = projectType === type;
|
||||||
|
const labels = {
|
||||||
|
website: "Corpo Website",
|
||||||
|
"web-app": "Web Application",
|
||||||
|
ecommerce: "E-Commerce",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setProjectType(type)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col items-start p-6 rounded-2xl border transition-all duration-300",
|
||||||
|
isActive
|
||||||
|
? "bg-white border-slate-900 shadow-xl shadow-slate-200 -translate-y-1"
|
||||||
|
: "bg-transparent border-slate-200 text-slate-500 hover:border-slate-300 hover:bg-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-bold transition-colors",
|
||||||
|
isActive ? "text-slate-900" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{labels[type]}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono mt-2 opacity-50 uppercase tracking-wider">
|
||||||
|
{isActive ? "[ AKTIVIERT ]" : "Auswählen"}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="active-indicator"
|
||||||
|
className="absolute top-4 right-4 w-2 h-2 rounded-full bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.5)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Footer / Alternative Action */}
|
||||||
|
<Reveal width="100%" delay={0.5} direction="up">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between gap-8 pt-8 border-t border-slate-100">
|
||||||
|
<p className="text-sm text-slate-400 font-medium max-w-sm">
|
||||||
|
Nutzen Sie unseren Konfigurator für eine präzise Aufwandsschätzung
|
||||||
|
oder senden Sie uns direkt eine Nachricht.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={onToggleFreeText}
|
||||||
|
className="group flex items-center gap-3 px-6 py-3 rounded-full bg-white border border-slate-200 text-slate-600 text-sm font-bold hover:border-slate-900 hover:text-slate-900 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<MessageSquareText
|
||||||
|
size={18}
|
||||||
|
className="text-slate-400 group-hover:text-slate-900"
|
||||||
|
/>
|
||||||
|
<span>Direktnachricht senden</span>
|
||||||
|
<ArrowRight
|
||||||
|
size={16}
|
||||||
|
className="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Plus, X, Link2, Globe } from "lucide-react";
|
||||||
|
import { cn } from "../../../utils/cn";
|
||||||
|
|
||||||
|
interface ReferenceInputProps {
|
||||||
|
references: string[];
|
||||||
|
setReferences: (refs: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReferenceInput = ({
|
||||||
|
references,
|
||||||
|
setReferences,
|
||||||
|
}: ReferenceInputProps) => {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
const addReference = () => {
|
||||||
|
if (inputValue.trim() && !references.includes(inputValue.trim())) {
|
||||||
|
setReferences([...references, inputValue.trim()]);
|
||||||
|
setInputValue("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeReference = (ref: string) => {
|
||||||
|
setReferences(references.filter((r) => r !== ref));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-2xl mx-auto">
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<div className="h-px bg-slate-200 flex-1" />
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 uppercase tracking-widest text-center min-w-max px-4">
|
||||||
|
REFERENZEN & INSPIRATION
|
||||||
|
</h3>
|
||||||
|
<div className="h-px bg-slate-200 flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1 group">
|
||||||
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-slate-900 transition-colors">
|
||||||
|
<Globe size={18} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && addReference()}
|
||||||
|
placeholder="Website URL oder Name (z.B. apple.com)"
|
||||||
|
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-xl focus:outline-none focus:ring-4 focus:ring-slate-900/5 focus:border-slate-900 transition-all text-slate-900 placeholder:text-slate-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={addReference}
|
||||||
|
disabled={!inputValue.trim()}
|
||||||
|
className="px-6 py-4 bg-slate-900 text-white rounded-xl font-bold hover:bg-slate-800 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Plus size={20} />{" "}
|
||||||
|
<span className="hidden md:inline">Hinzufügen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{references.map((ref) => (
|
||||||
|
<motion.div
|
||||||
|
key={ref}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 10 }}
|
||||||
|
className="flex items-center justify-between p-4 bg-slate-50 border border-slate-100 rounded-xl group hover:border-slate-200 transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-white border border-slate-200 flex items-center justify-center text-slate-400 group-hover:text-slate-900 transition-colors shadow-sm">
|
||||||
|
<Link2 size={14} />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-slate-700">{ref}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeReference(ref)}
|
||||||
|
className="p-2 text-slate-300 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{references.length === 0 && (
|
||||||
|
<div className="py-12 text-center border-2 border-dashed border-slate-100 rounded-2xl">
|
||||||
|
<p className="text-slate-400 font-medium">
|
||||||
|
Noch keine Referenzen hinzugefügt.
|
||||||
|
</p>
|
||||||
|
<p className="text-slate-300 text-sm mt-1">
|
||||||
|
Geben Sie eine URL ein, die Ihnen gefällt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
183
apps/web/src/components/ContactForm/ContactGateway.tsx
Normal file
183
apps/web/src/components/ContactForm/ContactGateway.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
import { Reveal } from "../Reveal";
|
||||||
|
import { MessageSquareText, Settings2, ArrowRight } from "lucide-react";
|
||||||
|
import { ProjectType } from "./types";
|
||||||
|
|
||||||
|
interface ContactGatewayProps {
|
||||||
|
name: string;
|
||||||
|
setName: (val: string) => void;
|
||||||
|
company: string;
|
||||||
|
setCompany: (val: string) => void;
|
||||||
|
projectType: ProjectType;
|
||||||
|
setProjectType: (val: ProjectType) => void;
|
||||||
|
onChooseConfigurator: () => void;
|
||||||
|
onChooseDirectMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
autoFocus,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="inline-grid items-center relative group mx-1">
|
||||||
|
<span className="col-start-1 row-start-1 opacity-0 pointer-events-none whitespace-pre border-b-2 border-transparent px-2 py-1 text-2xl md:text-5xl font-bold tracking-tight min-w-[140px]">
|
||||||
|
{value || placeholder}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
className={cn(
|
||||||
|
"col-start-1 row-start-1 w-full h-full bg-transparent focus:outline-none px-2 py-1",
|
||||||
|
"text-2xl md:text-5xl font-bold tracking-tight transition-all duration-300",
|
||||||
|
"border-b-2",
|
||||||
|
value
|
||||||
|
? "text-slate-900 border-slate-900"
|
||||||
|
: "text-slate-300 border-slate-100 placeholder:text-slate-200 focus:border-green-500 focus:placeholder:text-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContactGateway = ({
|
||||||
|
name,
|
||||||
|
setName,
|
||||||
|
company,
|
||||||
|
setCompany,
|
||||||
|
onChooseConfigurator,
|
||||||
|
onChooseDirectMessage,
|
||||||
|
}: ContactGatewayProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[70vh] text-left w-full px-4 max-w-5xl mx-auto space-y-24">
|
||||||
|
{/* Identity Section */}
|
||||||
|
<Reveal width="100%" delay={0.1}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||||
|
IDENTIFIKATION // SCHRITT_00
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap items-baseline gap-y-6 text-slate-900">
|
||||||
|
<span className="text-2xl md:text-5xl font-medium text-slate-400">
|
||||||
|
Hi, ich bin
|
||||||
|
</span>
|
||||||
|
<AutoInput value={name} onChange={setName} placeholder="Ihr Name" />
|
||||||
|
<span className="text-2xl md:text-5xl font-medium text-slate-400">
|
||||||
|
von
|
||||||
|
</span>
|
||||||
|
<AutoInput
|
||||||
|
value={company}
|
||||||
|
onChange={setCompany}
|
||||||
|
placeholder="Firma / Projekt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Path Selection */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||||
|
{/* Configurator Path */}
|
||||||
|
<Reveal width="100%" delay={0.3} direction="up">
|
||||||
|
<button
|
||||||
|
onClick={onChooseConfigurator}
|
||||||
|
disabled={!name}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex flex-col items-start p-8 rounded-3xl border text-left transition-all duration-500 overflow-hidden",
|
||||||
|
name
|
||||||
|
? "bg-slate-900 border-slate-800 text-white shadow-2xl hover:-translate-y-2"
|
||||||
|
: "bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||||
|
<Settings2 size={120} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Settings2 size={24} className="mb-6 text-green-400" />
|
||||||
|
<h3 className="text-2xl font-bold mb-2 tracking-tight">
|
||||||
|
System-Konfigurator
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-400 font-medium mb-8 max-w-[280px]">
|
||||||
|
Konfigurieren Sie Ihr Projekt modular für eine präzise
|
||||||
|
Aufwandsschätzung.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold">
|
||||||
|
<span>Sitzung starten</span>
|
||||||
|
<ArrowRight
|
||||||
|
size={14}
|
||||||
|
className="group-hover:translate-x-1 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!name && (
|
||||||
|
<div className="absolute inset-0 bg-slate-50/60 backdrop-blur-[6px] z-20" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Direct Mail Path */}
|
||||||
|
<Reveal width="100%" delay={0.4} direction="up">
|
||||||
|
<button
|
||||||
|
onClick={onChooseDirectMessage}
|
||||||
|
disabled={!name}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex flex-col items-start p-8 rounded-3xl border text-left transition-all duration-500 overflow-hidden",
|
||||||
|
name
|
||||||
|
? "bg-white border-slate-200 text-slate-900 hover:-translate-y-2 hover:border-slate-400 hover:shadow-xl"
|
||||||
|
: "bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageSquareText
|
||||||
|
size={24}
|
||||||
|
className={cn(
|
||||||
|
"mb-6 transition-colors",
|
||||||
|
name ? "text-slate-900" : "text-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<h3 className="text-2xl font-bold mb-2 tracking-tight">
|
||||||
|
Direktnachricht
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium mb-8 max-w-[280px]",
|
||||||
|
name ? "text-slate-500" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Kurze Frage oder spezifisches Anliegen? Senden Sie mir direkt
|
||||||
|
Informationen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold transition-colors",
|
||||||
|
name
|
||||||
|
? "text-slate-400 group-hover:text-slate-900"
|
||||||
|
: "text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Formular öffnen</span>
|
||||||
|
<ArrowRight
|
||||||
|
size={14}
|
||||||
|
className="group-hover:translate-x-1 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!name && (
|
||||||
|
<div className="absolute inset-0 bg-slate-50/60 backdrop-blur-[6px] z-20" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
125
apps/web/src/components/ContactForm/DirectMessageFlow.tsx
Normal file
125
apps/web/src/components/ContactForm/DirectMessageFlow.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
import { Reveal } from "../Reveal";
|
||||||
|
import { Mail, MessageSquare, ArrowLeft, Send } from "lucide-react";
|
||||||
|
|
||||||
|
interface DirectMessageFlowProps {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
setEmail: (val: string) => void;
|
||||||
|
company: string;
|
||||||
|
message: string;
|
||||||
|
setMessage: (val: string) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DirectMessageFlow = ({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
company,
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
onBack,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
}: DirectMessageFlowProps) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-3xl mx-auto px-4 py-12">
|
||||||
|
<Reveal width="100%" delay={0.1}>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors mb-12"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={14} /> Zurück zur Auswahl
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="space-y-12">
|
||||||
|
<Reveal width="100%" delay={0.2}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||||
|
DIREKTNACHRICHT // MODUS_AKTIVIERT
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||||
|
Wie kann ich helfen, {name.split(" ")[0]}?
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-500 font-medium">
|
||||||
|
Sende mir eine Nachricht zu {company || "deinem Projekt"} und ich
|
||||||
|
melde mich in Kürze.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Email Input */}
|
||||||
|
<Reveal width="100%" delay={0.3} direction="up">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
<Mail size={12} /> Rückantwort an
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="ihre@email.de"
|
||||||
|
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Message Input */}
|
||||||
|
<Reveal width="100%" delay={0.4} direction="up">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
<MessageSquare size={12} /> Ihre Nachricht
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Beschreiben Sie kurz Ihr Anliegen..."
|
||||||
|
rows={6}
|
||||||
|
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Reveal width="100%" delay={0.5} direction="up">
|
||||||
|
<button
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={isSubmitting || !email || !message}
|
||||||
|
className={cn(
|
||||||
|
"group relative w-full py-5 rounded-2xl font-bold text-lg transition-all duration-300 flex items-center justify-center gap-3 overflow-hidden",
|
||||||
|
isSubmitting || !email || !message
|
||||||
|
? "bg-slate-100 text-slate-400 cursor-not-allowed"
|
||||||
|
: "bg-slate-900 text-white shadow-xl hover:shadow-2xl hover:-translate-y-1 active:scale-[0.98]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-green-400/0 via-green-400/10 to-green-400/0 opacity-0 group-hover:opacity-100 -translate-x-full group-hover:translate-x-full transition-all duration-1000" />
|
||||||
|
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
<span>Übertragung läuft...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Nachricht absenden</span>
|
||||||
|
<Send
|
||||||
|
size={20}
|
||||||
|
className="group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
96
apps/web/src/components/ContactForm/EmailTemplates.tsx
Normal file
96
apps/web/src/components/ContactForm/EmailTemplates.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export const getInquiryEmailHtml = (data: any) => `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Courier New', Courier, monospace; background-color: #0f172a; color: #f8fafc; margin: 0; padding: 20px; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; background-color: #1e293b; border: 1px solid #334155; padding: 40px; border-radius: 8px; }
|
||||||
|
.header { border-bottom: 2px solid #22c55e; padding-bottom: 20px; margin-bottom: 30px; }
|
||||||
|
.title { font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #f8fafc; }
|
||||||
|
.label { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
|
||||||
|
.value { font-size: 16px; margin-bottom: 20px; color: #22c55e; }
|
||||||
|
.section { margin-bottom: 30px; }
|
||||||
|
.footer { font-size: 10px; color: #64748b; margin-top: 40px; border-top: 1px solid #334155; padding-top: 20px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">NEUE_ANFRAGE_INPUT</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="label">ABSENDER</div>
|
||||||
|
<div class="value">${data.name} (${data.email})</div>
|
||||||
|
|
||||||
|
<div class="label">UNTERNEHMEN</div>
|
||||||
|
<div class="value">${data.companyName || "N/A"}</div>
|
||||||
|
|
||||||
|
<div class="label">PROJEKT_TYP</div>
|
||||||
|
<div class="value">${data.projectType}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
data.isFreeText
|
||||||
|
? `
|
||||||
|
<div class="section">
|
||||||
|
<div class="label">NACHRICHT (FREITEXT)</div>
|
||||||
|
<div class="value" style="white-space: pre-wrap; color: #f8fafc;">${data.message}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
<div class="section">
|
||||||
|
<div class="label">KONFIGURATION</div>
|
||||||
|
<div class="value" style="font-size: 12px; color: #94a3b8; background: #0f172a; padding: 15px; border-radius: 4px;">
|
||||||
|
${JSON.stringify(data.config, null, 2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
SISTEM_STATUS: VALIDATED<br>
|
||||||
|
TIMESTAMP: ${new Date().toISOString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getConfirmationEmailHtml = (data: any) => `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Courier New', Courier, monospace; background-color: #f8fafc; color: #0f172a; margin: 0; padding: 20px; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e2e8f0; padding: 40px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
||||||
|
.header { text-align: center; margin-bottom: 40px; }
|
||||||
|
.status-badge { display: inline-block; padding: 4px 12px; background-color: #22c55e; color: #0f172a; font-size: 10px; font-weight: bold; border-radius: 9999px; margin-bottom: 16px; }
|
||||||
|
.title { font-size: 28px; font-weight: bold; letter-spacing: -0.02em; margin-bottom: 8px; }
|
||||||
|
.subtitle { color: #64748b; font-size: 16px; line-height: 1.5; }
|
||||||
|
.content { line-height: 1.6; color: #334155; margin-bottom: 40px; }
|
||||||
|
.footer { text-align: center; font-size: 12px; color: #94a3b8; border-top: 1px solid #f1f5f9; padding-top: 30px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="status-badge">SEQUENZ_INITIIERT</div>
|
||||||
|
<div class="title">Hallo ${data.name.split(" ")[0]},</div>
|
||||||
|
<div class="subtitle">vielen Dank für deine Anfrage.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p>Ich habe deine Nachricht erhalten und schaue mir die Details zu <strong>${data.companyName || "deinem Projekt"}</strong> umgehend an.</p>
|
||||||
|
<p>Normalerweise melde ich mich innerhalb von 24 Stunden bei dir zurück, um die nächsten Schritte zu besprechen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© ${new Date().getFullYear()} mintel.me — Technical Problem Solving
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from "lucide-react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
interface CheckboxProps {
|
interface CheckboxProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -16,24 +16,38 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
className={`w-full p-5 rounded-[2rem] border-2 text-left transition-all duration-300 flex items-start gap-4 focus:outline-none overflow-hidden relative ${
|
className={`w-full p-4 rounded-xl border-2 text-left transition-all duration-300 flex items-start gap-3 focus:outline-none overflow-hidden relative ${
|
||||||
checked ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
checked
|
||||||
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
|
: "border-slate-100 bg-white hover:border-slate-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<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'}`}>
|
<div
|
||||||
|
className={`mt-0.5 w-6 h-6 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" : "border-slate-200"}`}
|
||||||
|
>
|
||||||
{checked && (
|
{checked && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0, rotate: -45 }}
|
initial={{ scale: 0, rotate: -45 }}
|
||||||
animate={{ scale: 1, rotate: 0 }}
|
animate={{ scale: 1, rotate: 0 }}
|
||||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
>
|
>
|
||||||
<Check size={18} strokeWidth={4} />
|
<Check size={14} strokeWidth={4} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<h4 className={`text-2xl font-bold mb-1 transition-colors duration-500 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4>
|
<h4
|
||||||
{desc && <p className={`text-lg leading-relaxed transition-colors duration-500 ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>}
|
className={`text-base font-bold mb-0.5 transition-colors duration-500 ${checked ? "text-white" : "text-slate-900"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</h4>
|
||||||
|
{desc && (
|
||||||
|
<p
|
||||||
|
className={`text-sm leading-relaxed transition-colors duration-500 ${checked ? "text-slate-300" : "text-slate-500"}`}
|
||||||
|
>
|
||||||
|
{desc}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{checked && (
|
{checked && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
|
interface InputProps extends React.InputHTMLAttributes<
|
||||||
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
|
> {
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isTextArea?: boolean;
|
isTextArea?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Input({ label, icon: Icon, isTextArea, className = '', ...props }: InputProps) {
|
export function Input({
|
||||||
const InputComponent = isTextArea ? 'textarea' : 'input';
|
label,
|
||||||
|
icon: Icon,
|
||||||
|
isTextArea,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: InputProps) {
|
||||||
|
const InputComponent = isTextArea ? "textarea" : "input";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
{label && (
|
{label && (
|
||||||
@@ -22,13 +30,15 @@ export function Input({ label, icon: Icon, isTextArea, className = '', ...props
|
|||||||
)}
|
)}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<div className={`absolute left-6 ${isTextArea ? 'top-10' : 'top-1/2'} -translate-y-1/2 text-black transition-colors`}>
|
<div
|
||||||
|
className={`absolute left-6 ${isTextArea ? "top-10" : "top-1/2"} -translate-y-1/2 text-black transition-colors`}
|
||||||
|
>
|
||||||
<Icon size={24} />
|
<Icon size={24} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<InputComponent
|
<InputComponent
|
||||||
{...(props as any)}
|
{...(props as any)}
|
||||||
className={`w-full p-8 ${Icon ? 'pl-16' : 'px-10'} bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl focus:shadow-2xl ${isTextArea ? 'resize-none' : ''} ${className}`}
|
className={`w-full p-8 ${Icon ? "pl-16" : "px-10"} bg-white border border-slate-100 rounded-[2.5rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-xl focus:shadow-2xl ${isTextArea ? "resize-none" : ""} ${className}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as React from "react";
|
|||||||
import { FormState, Totals } from "../types";
|
import { FormState, Totals } from "../types";
|
||||||
import { PRICING } from "../constants";
|
import { PRICING } from "../constants";
|
||||||
import { AnimatedNumber } from "./AnimatedNumber";
|
import { AnimatedNumber } from "./AnimatedNumber";
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
|
|
||||||
import { ConceptAutomation } from "../../Landing/ConceptIllustrations";
|
import { ConceptAutomation } from "../../Landing/ConceptIllustrations";
|
||||||
import { Download, Share2, RefreshCw } from "lucide-react";
|
import { Download, Share2, RefreshCw } from "lucide-react";
|
||||||
@@ -49,9 +48,9 @@ export function PriceCalculation({
|
|||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { EstimationPDF } = await import("@mintel/pdf");
|
const { LocalEstimationPDF } = await import("../pdf/LocalEstimationPDF");
|
||||||
const doc = (
|
const doc = (
|
||||||
<EstimationPDF
|
<LocalEstimationPDF
|
||||||
state={state}
|
state={state}
|
||||||
totalPrice={totalPrice}
|
totalPrice={totalPrice}
|
||||||
monthlyPrice={monthlyPrice}
|
monthlyPrice={monthlyPrice}
|
||||||
@@ -63,6 +62,7 @@ export function PriceCalculation({
|
|||||||
footerLogo={
|
footerLogo={
|
||||||
typeof LogoBlack === "string" ? LogoBlack : (LogoBlack as any).src
|
typeof LogoBlack === "string" ? LogoBlack : (LogoBlack as any).src
|
||||||
}
|
}
|
||||||
|
qrCodeData={_qrCodeData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ export function PriceCalculation({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
|
<div className="lg:col-span-4 lg:sticky lg:top-32 self-start z-30">
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-[3rem] p-6 space-y-6">
|
<div className="bg-slate-50 border border-slate-100 rounded-2xl p-5 space-y-4">
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{state.projectType === "website" ? (
|
{state.projectType === "website" ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-4 overflow-y-auto pr-2 hide-scrollbar max-h-[120px]">
|
<div className="space-y-4 overflow-y-auto pr-2 hide-scrollbar max-h-[120px]">
|
||||||
|
|||||||
129
apps/web/src/components/ContactForm/pdf/LocalEstimationPDF.tsx
Normal file
129
apps/web/src/components/ContactForm/pdf/LocalEstimationPDF.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Page as PDFPage,
|
||||||
|
Document as PDFDocument,
|
||||||
|
Image as PDFImage,
|
||||||
|
StyleSheet,
|
||||||
|
View as PDFView,
|
||||||
|
Text as PDFText,
|
||||||
|
} from "@react-pdf/renderer";
|
||||||
|
import {
|
||||||
|
FrontPageModule,
|
||||||
|
SitemapModule,
|
||||||
|
EstimationModule,
|
||||||
|
TransparenzModule,
|
||||||
|
ClosingModule,
|
||||||
|
SimpleLayout,
|
||||||
|
pdfStyles,
|
||||||
|
calculatePositions,
|
||||||
|
} from "@mintel/pdf";
|
||||||
|
|
||||||
|
// Local styles for QR Code overlay
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
qrContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 40,
|
||||||
|
right: 40,
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
qrImage: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
qrLabel: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: "#94a3b8", // slate-400
|
||||||
|
marginTop: 4,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PDFProps {
|
||||||
|
state: any;
|
||||||
|
totalPrice: number;
|
||||||
|
monthlyPrice?: number;
|
||||||
|
totalPagesCount?: number;
|
||||||
|
pricing: any;
|
||||||
|
headerIcon?: string;
|
||||||
|
footerLogo?: string;
|
||||||
|
qrCodeData?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocalEstimationPDF = ({
|
||||||
|
state,
|
||||||
|
totalPrice,
|
||||||
|
pricing,
|
||||||
|
headerIcon,
|
||||||
|
footerLogo,
|
||||||
|
qrCodeData,
|
||||||
|
}: PDFProps) => {
|
||||||
|
const date = new Date().toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
const positions = calculatePositions(state, pricing);
|
||||||
|
|
||||||
|
const companyData = {
|
||||||
|
name: "Marc Mintel",
|
||||||
|
address1: "Georg-Meistermann-Straße 7",
|
||||||
|
address2: "54586 Schüller",
|
||||||
|
ustId: "DE367588065",
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
state,
|
||||||
|
date,
|
||||||
|
icon: headerIcon,
|
||||||
|
footerLogo,
|
||||||
|
companyData,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pageCounter = 1;
|
||||||
|
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PDFDocument title={`Angebot - ${state.companyName || "Projekt"}`}>
|
||||||
|
<PDFPage size="A4" style={pdfStyles.titlePage}>
|
||||||
|
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
|
||||||
|
{qrCodeData && (
|
||||||
|
<PDFView style={styles.qrContainer}>
|
||||||
|
<PDFImage src={qrCodeData} style={styles.qrImage} />
|
||||||
|
<PDFText style={styles.qrLabel}>Scan me</PDFText>
|
||||||
|
</PDFView>
|
||||||
|
)}
|
||||||
|
</PDFPage>
|
||||||
|
|
||||||
|
{/* BriefingModule Page REMOVED as per user request ("die zweite seite ist leer, weg damit") */}
|
||||||
|
|
||||||
|
{state.sitemap && state.sitemap.length > 0 && (
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<SitemapModule state={state} />
|
||||||
|
</SimpleLayout>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<EstimationModule
|
||||||
|
state={state}
|
||||||
|
positions={positions}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
</SimpleLayout>
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<TransparenzModule pricing={pricing} />
|
||||||
|
</SimpleLayout>
|
||||||
|
|
||||||
|
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||||
|
<ClosingModule />
|
||||||
|
</SimpleLayout>
|
||||||
|
</PDFDocument>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,14 +29,14 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Share2 size={24} />
|
<Share2 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
{isWebApp
|
{isWebApp
|
||||||
? "Integrationen & Datenquellen"
|
? "Integrationen & Datenquellen"
|
||||||
: "Schnittstellen (API)"}
|
: "Schnittstellen (API)"}
|
||||||
@@ -117,12 +117,12 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
|||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<ListPlus size={24} />
|
<ListPlus size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Weitere Systeme oder eigene APIs?
|
Weitere Systeme oder eigene APIs?
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ export function AssetsStep({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Briefcase size={24} />
|
<Briefcase size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Vorhandene Assets
|
Vorhandene Assets
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,12 +90,12 @@ export function AssetsStep({
|
|||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<ListPlus size={24} />
|
<ListPlus size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Weitere vorhandene Unterlagen?
|
Weitere vorhandene Unterlagen?
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="space-y-8"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
@@ -51,27 +51,24 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-14 h-14 bg-slate-900 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-slate-200">
|
<div className="w-8 h-8 bg-slate-900 text-white rounded-xl flex items-center justify-center shadow-sm">
|
||||||
<FileText size={28} />
|
<FileText size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="text-3xl font-bold text-slate-900 tracking-tight">
|
<h4 className="text-lg font-bold text-slate-900 tracking-tight">
|
||||||
Die Seitenstruktur
|
Die Seitenstruktur
|
||||||
</h4>
|
</h4>
|
||||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
Essenziell
|
Essenziell
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-slate-400 mt-1">
|
<p className="text-sm text-slate-400 mt-0.5">
|
||||||
<HelpCircle size={14} className="shrink-0" />
|
Wählen Sie die Bausteine Ihrer neuen Website.
|
||||||
<span className="text-base">
|
</p>
|
||||||
Wählen Sie die Bausteine Ihrer neuen Website.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -88,7 +85,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
Ich weiß es nicht
|
Ich weiß es nicht
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
id: "Home",
|
id: "Home",
|
||||||
@@ -143,12 +140,12 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<ListPlus size={24} />
|
<ListPlus size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Weitere individuelle Seiten?
|
Weitere individuelle Seiten?
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,23 +165,23 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.4 }}
|
transition={{ delay: 0.4 }}
|
||||||
className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-6 shadow-2xl shadow-slate-200 relative overflow-hidden group"
|
className="p-6 bg-slate-900 text-white rounded-2xl space-y-4 shadow-lg relative overflow-hidden group"
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||||
<ListPlus size={120} />
|
<ListPlus size={60} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-8 relative z-10">
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4 relative z-10">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-2xl font-bold text-white">
|
<h4 className="text-lg font-bold text-white">
|
||||||
Noch mehr Seiten?
|
Noch mehr Seiten?
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-lg text-slate-400 mt-1">
|
<p className="text-sm text-slate-400 mt-0.5">
|
||||||
Falls Sie die Namen noch nicht wissen, aber die Menge schätzen
|
Falls Sie die Namen noch nicht wissen, aber die Menge schätzen
|
||||||
können.
|
können.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-4">
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
@@ -194,9 +191,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
otherPagesCount: Math.max(0, state.otherPagesCount - 1),
|
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"
|
className="w-10 h-10 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={28} />
|
<Minus size={18} />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -204,7 +201,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
initial={{ opacity: 0, scale: 0.5 }}
|
initial={{ opacity: 0, scale: 0.5 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 1.5 }}
|
exit={{ opacity: 0, scale: 1.5 }}
|
||||||
className="text-6xl font-bold w-16 text-center"
|
className="text-3xl font-bold w-10 text-center"
|
||||||
>
|
>
|
||||||
{state.otherPagesCount}
|
{state.otherPagesCount}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -216,9 +213,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateState({ otherPagesCount: state.otherPagesCount + 1 })
|
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"
|
className="w-10 h-10 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={28} />
|
<Plus size={18} />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ interface CompanyStepProps {
|
|||||||
|
|
||||||
export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8" id="focus-target-company">
|
<div className="space-y-4" id="focus-target-company">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Building2 size={24} />
|
<Building2 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">Unternehmen</h4>
|
<h4 className="text-lg font-bold text-slate-900">Unternehmen</h4>
|
||||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
Erforderlich
|
Erforderlich
|
||||||
</span>
|
</span>
|
||||||
@@ -37,24 +37,24 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Users size={24} />
|
<Users size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Mitarbeiteranzahl
|
Mitarbeiteranzahl
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
{EMPLOYEE_OPTIONS.map((option) => (
|
{EMPLOYEE_OPTIONS.map((option) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={option.id}
|
key={option.id}
|
||||||
whileHover={{ y: -5 }}
|
whileHover={{ y: -3 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ employeeCount: option.id })}
|
onClick={() => updateState({ employeeCount: option.id })}
|
||||||
className={`p-6 rounded-[2rem] border-2 transition-all duration-300 font-bold text-lg ${
|
className={`p-4 rounded-xl border-2 transition-all duration-300 font-bold text-sm ${
|
||||||
state.employeeCount === option.id
|
state.employeeCount === option.id
|
||||||
? "border-slate-900 bg-slate-900 text-white"
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
: "border-slate-100 bg-white hover:border-slate-300 text-slate-600"
|
: "border-slate-100 bg-white hover:border-slate-300 text-slate-600"
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between p-10 bg-white border border-slate-100 rounded-[3rem] gap-8">
|
<div className="flex flex-col md:flex-row items-center justify-between p-6 bg-white border border-slate-100 rounded-2xl gap-4">
|
||||||
<div className="max-w-2xl space-y-4">
|
<div className="max-w-2xl space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Settings2 size={24} />
|
<Settings2 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Inhalte selbst verwalten (CMS)
|
Inhalte selbst verwalten (CMS)
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,10 +77,10 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-10">
|
<div className="p-10 bg-slate-50 rounded-2xl border border-slate-100 space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-black">
|
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-black">
|
||||||
<BarChart3 size={24} />
|
<BarChart3 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xl font-bold text-slate-900">
|
<p className="text-xl font-bold text-slate-900">
|
||||||
Wie oft ändern sich Ihre Inhalte?
|
Wie oft ändern sich Ihre Inhalte?
|
||||||
@@ -107,7 +107,7 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ expectedAdjustments: opt.id })}
|
onClick={() => updateState({ expectedAdjustments: opt.id })}
|
||||||
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none ${
|
className={`p-6 rounded-xl border-2 text-left transition-all duration-300 focus:outline-none ${
|
||||||
state.expectedAdjustments === opt.id
|
state.expectedAdjustments === opt.id
|
||||||
? "border-slate-900 bg-slate-900 text-white"
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
: "border-slate-200 bg-white hover:border-slate-400"
|
: "border-slate-200 bg-white hover:border-slate-400"
|
||||||
@@ -136,7 +136,7 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
|||||||
className="p-8 bg-amber-50 rounded-[2.5rem] border border-amber-100 flex gap-6 items-start"
|
className="p-8 bg-amber-50 rounded-[2.5rem] border border-amber-100 flex gap-6 items-start"
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shrink-0">
|
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shrink-0">
|
||||||
<AlertCircle size={24} />
|
<AlertCircle size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-amber-900 text-xl font-bold">
|
<p className="text-amber-900 text-xl font-bold">
|
||||||
@@ -176,9 +176,9 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.3}>
|
<Reveal width="100%" delay={0.3}>
|
||||||
<div className="flex flex-col gap-8 p-10 bg-white border border-slate-100 rounded-[3rem]">
|
<div className="flex flex-col gap-4 p-6 bg-white border border-slate-100 rounded-2xl">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Inhalte einpflegen
|
Inhalte einpflegen
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-lg text-slate-500 leading-relaxed">
|
<p className="text-lg text-slate-500 leading-relaxed">
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
{/* Design Vibe */}
|
{/* Design Vibe */}
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Design-Richtung
|
Design-Richtung
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-slate-500">
|
<p className="text-slate-500">
|
||||||
@@ -145,7 +145,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">Farbschema</h4>
|
<h4 className="text-lg font-bold text-slate-900">Farbschema</h4>
|
||||||
<p className="text-slate-500">
|
<p className="text-slate-500">
|
||||||
Definieren Sie Ihre Markenfarben oder lassen Sie sich
|
Definieren Sie Ihre Markenfarben oder lassen Sie sich
|
||||||
inspirieren.
|
inspirieren.
|
||||||
@@ -179,7 +179,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Picker */}
|
{/* Custom Picker */}
|
||||||
<div className="space-y-8 p-10 bg-slate-50 rounded-[3rem] border border-slate-100">
|
<div className="space-y-6 p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||||
<div className="flex items-center gap-3 text-slate-400 font-bold uppercase tracking-widest text-xs">
|
<div className="flex items-center gap-3 text-slate-400 font-bold uppercase tracking-widest text-xs">
|
||||||
<Pipette size={16} />
|
<Pipette size={16} />
|
||||||
Individuelle Farben
|
Individuelle Farben
|
||||||
@@ -251,16 +251,16 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
|||||||
|
|
||||||
{/* References */}
|
{/* References */}
|
||||||
<Reveal width="100%" delay={0.3}>
|
<Reveal width="100%" delay={0.3}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Referenz-Websites
|
Referenz-Websites
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-slate-500">
|
<p className="text-slate-500">
|
||||||
Gibt es Websites, die Ihnen besonders gut gefallen?
|
Gibt es Websites, die Ihnen besonders gut gefallen?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-10 bg-white border border-slate-100 rounded-[3rem]">
|
<div className="p-10 bg-white border border-slate-100 rounded-2xl">
|
||||||
<RepeatableList
|
<RepeatableList
|
||||||
items={state.references || []}
|
items={state.references || []}
|
||||||
onAdd={(v) =>
|
onAdd={(v) =>
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ export function FeaturesStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<LayoutGrid size={24} />
|
<LayoutGrid size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
System-Module
|
System-Module
|
||||||
</h4>
|
</h4>
|
||||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
@@ -90,12 +90,12 @@ export function FeaturesStep({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<ListPlus size={24} />
|
<ListPlus size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Weitere inhaltliche Module?
|
Weitere inhaltliche Module?
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export function FunctionsStep({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Cpu size={24} />
|
<Cpu size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
{isWebApp
|
{isWebApp
|
||||||
? "Funktionale Anforderungen"
|
? "Funktionale Anforderungen"
|
||||||
: "Erweiterte Funktionen"}
|
: "Erweiterte Funktionen"}
|
||||||
@@ -183,12 +183,12 @@ export function FunctionsStep({
|
|||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<ListPlus size={24} />
|
<ListPlus size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Weitere spezifische Wünsche?
|
Weitere spezifische Wünsche?
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Globe size={24} />
|
<Globe size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">Sprachen</h4>
|
<h4 className="text-lg font-bold text-slate-900">Sprachen</h4>
|
||||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
Optional
|
Optional
|
||||||
</span>
|
</span>
|
||||||
@@ -72,7 +72,7 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
|||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<p className="text-lg text-slate-500 leading-relaxed ml-2">
|
<p className="text-lg text-slate-500 leading-relaxed ml-2">
|
||||||
Welche Sprachen soll Ihre Website unterstützen?
|
Welche Sprachen soll Ihre Website unterstützen?
|
||||||
</p>
|
</p>
|
||||||
@@ -153,10 +153,10 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.3}>
|
<Reveal width="100%" delay={0.3}>
|
||||||
<div className="p-10 bg-slate-900 text-white rounded-[3rem] space-y-8 relative overflow-hidden">
|
<div className="p-10 bg-slate-900 text-white rounded-2xl space-y-6 relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
|
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
|
||||||
<div className="flex items-center gap-4 text-slate-400 relative z-10">
|
<div className="flex items-center gap-4 text-slate-400 relative z-10">
|
||||||
<Info size={24} />
|
<Info size={16} />
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">
|
<span className="text-sm font-bold uppercase tracking-widest">
|
||||||
Warum dieser Faktor?
|
Warum dieser Faktor?
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export function PresenceStep({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-6">
|
||||||
<Reveal width="100%" delay={0.1}>
|
<Reveal width="100%" delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Globe size={24} />
|
<Globe size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Bestehende Website
|
Bestehende Website
|
||||||
</h4>
|
</h4>
|
||||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
@@ -71,7 +71,7 @@ export function PresenceStep({
|
|||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<Input
|
<Input
|
||||||
label="Bestehende Domain"
|
label="Bestehende Domain"
|
||||||
@@ -91,12 +91,12 @@ export function PresenceStep({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.3}>
|
<Reveal width="100%" delay={0.3}>
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
|
||||||
<Share2 size={24} />
|
<Share2 size={16} />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-2xl font-bold text-slate-900">
|
<h4 className="text-lg font-bold text-slate-900">
|
||||||
Social Media Accounts
|
Social Media Accounts
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +161,7 @@ export function PresenceStep({
|
|||||||
placeholder={`https://${platform.id}.com/ihr-profil`}
|
placeholder={`https://${platform.id}.com/ihr-profil`}
|
||||||
value={state.socialMediaUrls[id] || ""}
|
value={state.socialMediaUrls[id] || ""}
|
||||||
onChange={(e) => updateUrl(id, e.target.value)}
|
onChange={(e) => updateUrl(id, e.target.value)}
|
||||||
className="w-full p-6 pl-40 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg focus:shadow-xl"
|
className="w-full p-6 pl-40 bg-white border border-slate-100 rounded-xl focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg focus:shadow-xl"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@@ -169,7 +169,7 @@ export function PresenceStep({
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{state.socialMedia.length === 0 && (
|
{state.socialMedia.length === 0 && (
|
||||||
<div className="p-12 border-2 border-dashed border-slate-100 rounded-[3rem] text-center">
|
<div className="p-12 border-2 border-dashed border-slate-100 rounded-2xl text-center">
|
||||||
<p className="text-slate-400 font-medium">
|
<p className="text-slate-400 font-medium">
|
||||||
Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.
|
Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">Zeitplan</h4>
|
<h4 className="text-lg font-bold text-slate-900">Zeitplan</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleDontKnow("timeline")}
|
onClick={() => toggleDontKnow("timeline")}
|
||||||
@@ -91,7 +91,7 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{state.deadline === "asap" && (
|
{state.deadline === "asap" && (
|
||||||
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">
|
<div className="p-8 bg-slate-50 rounded-xl border border-slate-100 flex gap-6 items-start">
|
||||||
<AlertCircle className="text-slate-900 shrink-0 mt-1" size={28} />
|
<AlertCircle className="text-slate-900 shrink-0 mt-1" size={28} />
|
||||||
<p className="text-base text-slate-600 leading-relaxed">
|
<p className="text-base text-slate-600 leading-relaxed">
|
||||||
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein
|
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein
|
||||||
@@ -102,9 +102,9 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(isMissingAssets || isMissingPages) && (
|
{(isMissingAssets || isMissingPages) && (
|
||||||
<div className="p-8 bg-amber-50 rounded-[2rem] border border-amber-100 flex gap-6 items-start">
|
<div className="p-8 bg-amber-50 rounded-xl border border-amber-100 flex gap-6 items-start">
|
||||||
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shrink-0">
|
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shrink-0">
|
||||||
<AlertCircle size={24} />
|
<AlertCircle size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-amber-900 text-xl font-bold">
|
<p className="text-amber-900 text-xl font-bold">
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ interface TypeStepProps {
|
|||||||
|
|
||||||
export function TypeStep({ state, updateState }: TypeStepProps) {
|
export function TypeStep({ state, updateState }: TypeStepProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
id: "website",
|
id: "website",
|
||||||
label: "Website",
|
label: "Website",
|
||||||
desc: "Klassische Webpräsenz, Portfolio oder Blog.",
|
desc: "Klassische Webpräsenz, Portfolio oder Blog.",
|
||||||
illustration: <ConceptWebsite className="w-20 h-20 mb-6" />,
|
illustration: <ConceptWebsite className="w-12 h-12 mb-3" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "web-app",
|
id: "web-app",
|
||||||
label: "Web App",
|
label: "Web App",
|
||||||
desc: "Internes Tool, Dashboard oder Prozess-Logik.",
|
desc: "Internes Tool, Dashboard oder Prozess-Logik.",
|
||||||
illustration: <ConceptSystem className="w-20 h-20 mb-6" />,
|
illustration: <ConceptSystem className="w-12 h-12 mb-3" />,
|
||||||
},
|
},
|
||||||
].map((type, index) => (
|
].map((type, index) => (
|
||||||
<Reveal key={type.id} width="100%" delay={index * 0.1}>
|
<Reveal key={type.id} width="100%" delay={index * 0.1}>
|
||||||
@@ -38,7 +38,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ projectType: type.id as ProjectType })}
|
onClick={() => updateState({ projectType: type.id as ProjectType })}
|
||||||
className={`w-full p-12 rounded-[4rem] border-2 text-left transition-all duration-700 focus:outline-none overflow-hidden relative group ${
|
className={`w-full p-8 rounded-2xl border-2 text-left transition-all duration-700 focus:outline-none overflow-hidden relative group ${
|
||||||
state.projectType === type.id
|
state.projectType === type.id
|
||||||
? "border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400"
|
? "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"
|
: "border-slate-100 bg-white hover:border-slate-300 hover:shadow-xl"
|
||||||
@@ -49,9 +49,9 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
|
|||||||
>
|
>
|
||||||
{type.illustration}
|
{type.illustration}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<h4
|
<h4
|
||||||
className={`text-5xl font-bold tracking-tight ${state.projectType === type.id ? "text-white" : "text-slate-900"}`}
|
className={`text-2xl font-bold tracking-tight ${state.projectType === type.id ? "text-white" : "text-slate-900"}`}
|
||||||
>
|
>
|
||||||
{type.label}
|
{type.label}
|
||||||
</h4>
|
</h4>
|
||||||
@@ -62,7 +62,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
className={`text-2xl leading-relaxed ${state.projectType === type.id ? "text-slate-200" : "text-slate-500"}`}
|
className={`text-base leading-relaxed ${state.projectType === type.id ? "text-slate-200" : "text-slate-500"}`}
|
||||||
>
|
>
|
||||||
{type.desc}
|
{type.desc}
|
||||||
</p>
|
</p>
|
||||||
@@ -70,7 +70,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
|
|||||||
{state.projectType === type.id && (
|
{state.projectType === type.id && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="activeType"
|
layoutId="activeType"
|
||||||
className="absolute top-8 right-8 w-6 h-6 bg-white rounded-full shadow-lg flex items-center justify-center"
|
className="absolute top-4 right-4 w-5 h-5 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div className="w-2 h-2 bg-slate-900 rounded-full" />
|
<div className="w-2 h-2 bg-slate-900 rounded-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
{/* Target Audience */}
|
{/* Target Audience */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
|
||||||
<Users size={24} className="text-black" /> Zielgruppe
|
<Users size={16} className="text-black" /> Zielgruppe
|
||||||
</h4>
|
</h4>
|
||||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||||
Fokus
|
Fokus
|
||||||
@@ -47,7 +47,7 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
key={opt.id}
|
key={opt.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ targetAudience: opt.id })}
|
onClick={() => updateState({ targetAudience: opt.id })}
|
||||||
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
|
className={`p-8 rounded-xl border-2 text-left transition-all ${
|
||||||
state.targetAudience === opt.id
|
state.targetAudience === opt.id
|
||||||
? "border-slate-900 bg-slate-900 text-white"
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
: "border-slate-100 bg-white hover:border-slate-200"
|
: "border-slate-100 bg-white hover:border-slate-200"
|
||||||
@@ -66,7 +66,7 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
|
|
||||||
{/* User Roles */}
|
{/* User Roles */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h4 className="text-2xl font-bold text-slate-900">Benutzerrollen</h4>
|
<h4 className="text-lg font-bold text-slate-900">Benutzerrollen</h4>
|
||||||
<p className="text-lg text-slate-500">Wer wird das System nutzen?</p>
|
<p className="text-lg text-slate-500">Wer wird das System nutzen?</p>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{[
|
{[
|
||||||
@@ -94,32 +94,32 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
|
|
||||||
{/* Platform Type */}
|
{/* Platform Type */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
|
||||||
<Monitor size={24} className="text-black" /> Plattform-Fokus
|
<Monitor size={16} className="text-black" /> Plattform-Fokus
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
id: "desktop",
|
id: "desktop",
|
||||||
label: "Desktop First",
|
label: "Desktop First",
|
||||||
icon: <Monitor size={24} />,
|
icon: <Monitor size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mobile",
|
id: "mobile",
|
||||||
label: "Mobile First",
|
label: "Mobile First",
|
||||||
icon: <Smartphone size={24} />,
|
icon: <Smartphone size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pwa",
|
id: "pwa",
|
||||||
label: "PWA (Installierbar)",
|
label: "PWA (Installierbar)",
|
||||||
icon: <Globe size={24} />,
|
icon: <Globe size={16} />,
|
||||||
},
|
},
|
||||||
].map((opt) => (
|
].map((opt) => (
|
||||||
<button
|
<button
|
||||||
key={opt.id}
|
key={opt.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ platformType: opt.id })}
|
onClick={() => updateState({ platformType: opt.id })}
|
||||||
className={`p-8 rounded-[2rem] border-2 flex flex-col items-center gap-4 transition-all ${
|
className={`p-8 rounded-xl border-2 flex flex-col items-center gap-4 transition-all ${
|
||||||
state.platformType === opt.id
|
state.platformType === opt.id
|
||||||
? "border-slate-900 bg-slate-900 text-white"
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
: "border-slate-100 bg-white hover:border-slate-200"
|
: "border-slate-100 bg-white hover:border-slate-200"
|
||||||
@@ -140,8 +140,8 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
|
|
||||||
{/* Data Sensitivity */}
|
{/* Data Sensitivity */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
|
||||||
<Shield size={24} className="text-black" /> Datensicherheit
|
<Shield size={16} className="text-black" /> Datensicherheit
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{[
|
{[
|
||||||
@@ -160,7 +160,7 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
key={opt.id}
|
key={opt.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateState({ dataSensitivity: opt.id })}
|
onClick={() => updateState({ dataSensitivity: opt.id })}
|
||||||
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
|
className={`p-8 rounded-xl border-2 text-left transition-all ${
|
||||||
state.dataSensitivity === opt.id
|
state.dataSensitivity === opt.id
|
||||||
? "border-slate-900 bg-slate-900 text-white"
|
? "border-slate-900 bg-slate-900 text-white"
|
||||||
: "border-slate-100 bg-white hover:border-slate-200"
|
: "border-slate-100 bg-white hover:border-slate-200"
|
||||||
@@ -178,9 +178,9 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Authentication */}
|
{/* Authentication */}
|
||||||
<div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-6">
|
<div className="p-10 bg-slate-50 rounded-2xl border border-slate-100 space-y-6">
|
||||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
|
||||||
<Lock size={24} className="text-black" /> Authentifizierung
|
<Lock size={16} className="text-black" /> Authentifizierung
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-lg text-slate-500">
|
<p className="text-lg text-slate-500">
|
||||||
Wie sollen sich Nutzer anmelden?
|
Wie sollen sich Nutzer anmelden?
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export type ProjectType = 'website' | 'web-app';
|
export type ProjectType = "website" | "web-app" | "ecommerce";
|
||||||
|
|
||||||
export interface FormState {
|
export interface FormState {
|
||||||
projectType: ProjectType;
|
projectType: ProjectType;
|
||||||
|
|||||||
156
apps/web/src/components/Effects/ArchitectureVisualizer.tsx
Normal file
156
apps/web/src/components/Effects/ArchitectureVisualizer.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { GitBranch, Box, Server, Globe } from "lucide-react";
|
||||||
|
import { MonoLabel, Label } from "../Typography";
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
|
||||||
|
const Node: React.FC<{
|
||||||
|
icon: React.ElementType;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
active?: boolean;
|
||||||
|
color?: string;
|
||||||
|
}> = ({ icon: Icon, title, status, active, color = "blue" }) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
className="flex flex-col items-center gap-2 md:gap-3 relative z-10 w-full md:w-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-12 h-12 md:w-16 md:h-16 rounded-xl md:rounded-2xl border flex items-center justify-center transition-all duration-700 shadow-sm",
|
||||||
|
active
|
||||||
|
? `bg-${color}-50 border-${color}-200 text-${color}-600 shadow-${color}-100/50 scale-110`
|
||||||
|
: "bg-white border-slate-100 text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5 md:w-8 md:h-8" />
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="active-glow"
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 rounded-2xl blur-xl -z-10",
|
||||||
|
`bg-${color}-400/20`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-0.5">
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
"text-[8px] md:text-[9px] font-bold uppercase tracking-widest",
|
||||||
|
active ? "text-slate-900" : "text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Label>
|
||||||
|
<MonoLabel
|
||||||
|
className={cn(
|
||||||
|
"text-[6px] md:text-[7px]",
|
||||||
|
active ? "text-green-500" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</MonoLabel>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Connector: React.FC<{ active?: boolean }> = ({ active }) => (
|
||||||
|
<div className="flex-1 w-px md:w-auto h-8 md:h-[1px] bg-slate-100 relative min-h-[20px] md:min-w-[40px] shrink-0">
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scaleX: 0, scaleY: 0 }}
|
||||||
|
animate={{ scaleX: 1, scaleY: 1 }}
|
||||||
|
className="absolute inset-0 bg-blue-300 origin-top md:origin-left"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-1 h-1 rounded-full",
|
||||||
|
active ? "bg-blue-300 animate-pulse" : "bg-slate-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArchitectureVisualizer: React.FC<{ className?: string }> = ({
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const [step, setStep] = React.useState(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setStep((s) => (s + 1) % 4);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative p-6 md:p-12 rounded-3xl border border-slate-100 bg-slate-50/30 backdrop-blur-sm overflow-hidden flex flex-col md:flex-row items-center justify-between gap-2 md:gap-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.03] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "radial-gradient(#000 1px, transparent 1px)",
|
||||||
|
backgroundSize: "30px 30px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Node
|
||||||
|
icon={GitBranch}
|
||||||
|
title="Repository"
|
||||||
|
status="VCS_STABLE"
|
||||||
|
active={step === 0}
|
||||||
|
color="slate"
|
||||||
|
/>
|
||||||
|
<Connector active={step >= 1} />
|
||||||
|
<Node
|
||||||
|
icon={Box}
|
||||||
|
title="Container"
|
||||||
|
status="BUILD_SUCCESS"
|
||||||
|
active={step === 1}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Connector active={step >= 2} />
|
||||||
|
<Node
|
||||||
|
icon={Server}
|
||||||
|
title="Deployment"
|
||||||
|
status="HEALTH_OK"
|
||||||
|
active={step === 2}
|
||||||
|
color="indigo"
|
||||||
|
/>
|
||||||
|
<Connector active={step >= 3} />
|
||||||
|
<Node
|
||||||
|
icon={Globe}
|
||||||
|
title="CDN Edge"
|
||||||
|
status="LIVE_SYNCED"
|
||||||
|
active={step === 3}
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Decorative Binary Pulse */}
|
||||||
|
<div className="absolute top-4 left-4 font-mono text-[6px] md:text-[7px] text-slate-200 uppercase tracking-widest leading-none hidden sm:block">
|
||||||
|
BUILD_PROTOCOL_v4.2 // SYSTEM_IS_DETERMINISTIC
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:absolute bottom-4 right-1/2 md:translate-x-1/2 flex items-center gap-2 mt-6 md:mt-0">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
<span className="text-[7px] font-mono text-slate-400 font-bold uppercase tracking-widest">
|
||||||
|
{step === 0 && "Polling for changes..."}
|
||||||
|
{step === 1 && "Bundling production image..."}
|
||||||
|
{step === 2 && "Syncing cluster state..."}
|
||||||
|
{step === 3 && "Invalidating edge cache..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
215
apps/web/src/components/Effects/CMSVisualizer.tsx
Normal file
215
apps/web/src/components/Effects/CMSVisualizer.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Edit3,
|
||||||
|
CheckCircle2,
|
||||||
|
Globe,
|
||||||
|
RotateCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { MonoLabel, Label, BodyText } from "../Typography";
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
|
||||||
|
export const CMSVisualizer: React.FC<{ className?: string }> = ({
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = React.useState<"cms" | "live">("cms");
|
||||||
|
const [lastAction, setLastAction] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const triggerAction = (action: string) => {
|
||||||
|
setLastAction(action);
|
||||||
|
setTimeout(() => setLastAction(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full aspect-square rounded-3xl overflow-hidden border border-slate-100 bg-white shadow-2xl group/cms flex flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* ── BROWSER CHROME ── */}
|
||||||
|
<div className="h-16 bg-slate-50 border-b border-slate-100 flex flex-col shrink-0 z-30">
|
||||||
|
<div className="h-full flex items-center justify-between px-4 gap-4">
|
||||||
|
<div className="flex gap-1.5 shrink-0">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-slate-200" />
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-slate-200" />
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-slate-200" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 max-w-md h-8 bg-white border border-slate-200 rounded-lg flex items-center px-3 gap-2">
|
||||||
|
<Globe className="w-3 h-3 text-slate-400" />
|
||||||
|
<span className="text-[9px] font-mono text-slate-400 truncate">
|
||||||
|
{activeTab === "cms"
|
||||||
|
? "mintel.localhost/admin/posts/edit"
|
||||||
|
: "mintel.me/projects/performance"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex bg-slate-200/50 rounded-lg p-0.5 text-[8px] font-mono font-bold shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("cms")}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-md transition-all",
|
||||||
|
activeTab === "cms"
|
||||||
|
? "bg-white text-slate-900 shadow-sm"
|
||||||
|
: "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
CMS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("live")}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-md transition-all",
|
||||||
|
activeTab === "live"
|
||||||
|
? "bg-white text-slate-900 shadow-sm"
|
||||||
|
: "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
LIVE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── MAIN CONTENT AREA ── */}
|
||||||
|
<div className="flex-1 relative overflow-hidden bg-slate-50/20">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeTab === "cms" ? (
|
||||||
|
<motion.div
|
||||||
|
key="cms-view"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="absolute inset-0 flex flex-col md:flex-row"
|
||||||
|
>
|
||||||
|
{/* LEFT: Editor */}
|
||||||
|
<div className="flex-1 p-6 md:p-8 flex flex-col gap-5 bg-white border-r border-slate-100">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[9px] text-slate-400">
|
||||||
|
BLOG TITEL
|
||||||
|
</Label>
|
||||||
|
<div className="h-10 rounded-lg bg-slate-50 border border-slate-200 p-3 relative">
|
||||||
|
<motion.div
|
||||||
|
animate={{ opacity: [1, 0.4, 1] }}
|
||||||
|
transition={{ repeat: Infinity, duration: 2 }}
|
||||||
|
className="w-32 h-3.5 bg-slate-200/50 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[9px] text-slate-400">
|
||||||
|
TITELBILD
|
||||||
|
</Label>
|
||||||
|
<div
|
||||||
|
className="aspect-video rounded-lg bg-slate-50 border border-slate-200 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition-colors"
|
||||||
|
onClick={() => triggerAction("Image Updated")}
|
||||||
|
>
|
||||||
|
<span className="text-xl text-slate-300">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => triggerAction("Published")}
|
||||||
|
className="px-5 py-2 bg-slate-900 text-white text-[10px] font-bold rounded-xl flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-3 h-3" /> Veröffentlichen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT: Preview */}
|
||||||
|
<div className="hidden md:flex flex-1 p-8 items-center justify-center bg-slate-50/50 relative">
|
||||||
|
<div className="w-full max-w-[240px] aspect-[3/4] bg-white rounded-2xl border border-slate-200 shadow-sm p-4 space-y-3 relative opacity-60">
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<Lock className="w-2.5 h-2.5 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-3/4 bg-blue-100 rounded" />
|
||||||
|
<div className="aspect-video w-full bg-slate-100 rounded" />
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="h-1 w-full bg-slate-50 rounded" />
|
||||||
|
<div className="h-1 w-full bg-slate-50 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MonoLabel className="absolute bottom-4 text-[7px] text-slate-300">
|
||||||
|
SANDBOX MODE: DESIGN FROZEN
|
||||||
|
</MonoLabel>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="live-view"
|
||||||
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.02 }}
|
||||||
|
className="absolute inset-0 bg-white flex flex-col p-6 md:p-12"
|
||||||
|
>
|
||||||
|
<header className="flex justify-between items-center border-b border-slate-100 pb-4 mb-8">
|
||||||
|
<div className="w-8 h-8 bg-slate-900 rounded flex items-center justify-center text-white text-xs font-bold">
|
||||||
|
M
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 text-[8px] font-mono text-slate-400">
|
||||||
|
<span>BLOG</span>
|
||||||
|
<span>WORK</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<MonoLabel className="text-blue-500">
|
||||||
|
ARTICLE PREVIEW
|
||||||
|
</MonoLabel>
|
||||||
|
<h3 className="text-2xl md:text-4xl font-bold text-slate-900">
|
||||||
|
Performance by{" "}
|
||||||
|
<span className="text-slate-400 italic">Code.</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="aspect-video md:aspect-[21/9] w-full bg-slate-100 rounded-3xl relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="px-4 py-2 bg-white/90 rounded-full border border-slate-100 text-[10px] font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<Shield className="w-3 h-3 text-blue-500" /> DESIGN
|
||||||
|
ENFORCED
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BodyText className="text-slate-400 text-xs max-w-md">
|
||||||
|
Layout-Stabilität garantiert durch strikte Trennung von
|
||||||
|
Content und Architektur.
|
||||||
|
</BodyText>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto h-6 bg-slate-900 -mx-12 -mb-12 flex items-center px-6 justify-between">
|
||||||
|
<span className="text-[6px] font-mono text-white/40 uppercase tracking-widest">
|
||||||
|
Global CDN: Optimal
|
||||||
|
</span>
|
||||||
|
<span className="text-[6px] font-mono text-white/20">
|
||||||
|
© 2026 MINTEL.ME
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{lastAction && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
className="absolute bottom-6 left-1/2 -translate-x-1/2 z-50 bg-slate-900 text-white px-5 py-2.5 rounded-xl shadow-2xl flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<RotateCw className="w-3 h-3 animate-spin text-blue-400" />
|
||||||
|
<span className="text-[9px] font-bold tracking-wider uppercase">
|
||||||
|
{lastAction}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@ export const CodeWindow: React.FC<CodeWindowProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative rounded-xl border border-slate-100 bg-slate-50/50 backdrop-blur-sm overflow-hidden w-full flex-shrink-0 flex flex-col",
|
"relative rounded-xl border border-slate-100 bg-slate-50/50 backdrop-blur-sm overflow-hidden w-full flex flex-col",
|
||||||
fixedHeight && "h-[400px]",
|
fixedHeight && "h-[400px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
137
apps/web/src/components/Effects/ResultVisualizer.tsx
Normal file
137
apps/web/src/components/Effects/ResultVisualizer.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Code2,
|
||||||
|
Cpu,
|
||||||
|
LayoutDashboard,
|
||||||
|
CheckCircle2,
|
||||||
|
Package,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { MonoLabel, Label, BodyText } from "../Typography";
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
|
||||||
|
const DeliveryCard: React.FC<{
|
||||||
|
icon: React.ElementType;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
delay: number;
|
||||||
|
}> = ({ icon: Icon, title, desc, delay }) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, rotateX: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
|
||||||
|
transition={{ delay, duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
className="group relative h-full"
|
||||||
|
>
|
||||||
|
<div className="h-full bg-white border border-slate-100 rounded-2xl p-6 md:p-8 shadow-sm flex flex-col gap-6 transition-all duration-700 hover:border-slate-300 hover:shadow-xl hover:shadow-slate-100/50">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-slate-50 border border-slate-100 flex items-center justify-center text-slate-400 group-hover:text-slate-900 group-hover:bg-white group-hover:border-slate-200 transition-all duration-500">
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="w-5 h-5 rounded-full border border-green-100 bg-green-50 flex items-center justify-center">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xl font-bold tracking-tight text-slate-900">
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
<BodyText className="text-sm text-slate-400 leading-relaxed group-hover:text-slate-500 transition-colors">
|
||||||
|
{desc}
|
||||||
|
</BodyText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto pt-6 border-t border-slate-50 flex items-center justify-between">
|
||||||
|
<MonoLabel className="text-[8px] text-slate-300 uppercase tracking-widest">
|
||||||
|
Ownership: 100%
|
||||||
|
</MonoLabel>
|
||||||
|
<span className="text-[10px] font-mono font-bold text-slate-900 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
READY
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtle hover accent */}
|
||||||
|
<div className="absolute -inset-2 bg-gradient-to-tr from-slate-100/50 to-transparent rounded-3xl -z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-700 blur-sm" />
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ResultVisualizer: React.FC<{ className?: string }> = ({
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative p-6 md:p-12 lg:p-16 rounded-[2.5rem] border border-slate-100 bg-white overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-slate-50/30 opacity-60" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.02] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "radial-gradient(#000 1px, transparent 1px)",
|
||||||
|
backgroundSize: "40px 40px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 space-y-12">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="px-4 py-2 bg-slate-900 rounded-xl flex items-center gap-3 shadow-xl">
|
||||||
|
<Package className="w-4 h-4 text-white" />
|
||||||
|
<MonoLabel className="text-white text-[10px]">
|
||||||
|
DELIVERY_PACKAGE_v2
|
||||||
|
</MonoLabel>
|
||||||
|
</div>
|
||||||
|
<div className="h-px flex-1 bg-slate-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||||
|
<DeliveryCard
|
||||||
|
icon={Code2}
|
||||||
|
title="Quellcode"
|
||||||
|
desc="Ihr Eigentum. Versioniert auf GitHub, dokumentiert und jederzeit bereit für Weiterentwicklungen."
|
||||||
|
delay={0.1}
|
||||||
|
/>
|
||||||
|
<DeliveryCard
|
||||||
|
icon={Cpu}
|
||||||
|
title="Infrastruktur"
|
||||||
|
desc="Docker-Container & CI/CD Pipelines. Ein System, das überall läuft und sich selbst aktualisiert."
|
||||||
|
delay={0.2}
|
||||||
|
/>
|
||||||
|
<DeliveryCard
|
||||||
|
icon={LayoutDashboard}
|
||||||
|
title="Content System"
|
||||||
|
desc="Ein maßgeschneidertes CMS für Ihre Inhalte. Intuitive Pflege ohne das Design zu gefährden."
|
||||||
|
delay={0.3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Line */}
|
||||||
|
<div className="pt-8 flex flex-col md:flex-row items-center justify-between gap-6 border-t border-slate-100">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-8 h-8 rounded-full border-2 border-white bg-slate-100"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Label className="text-slate-400 text-xs">
|
||||||
|
Ihre Teams haben die volle Kontrolle.
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-ping" />
|
||||||
|
<MonoLabel className="text-slate-900 font-bold tracking-tighter">
|
||||||
|
PROJECT STATUS: PRODUCTION_READY
|
||||||
|
</MonoLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,3 +4,6 @@ export { BinaryStream } from "./BinaryStream";
|
|||||||
export { GradientMesh } from "./GradientMesh";
|
export { GradientMesh } from "./GradientMesh";
|
||||||
export { CodeSnippet } from "./CodeSnippet";
|
export { CodeSnippet } from "./CodeSnippet";
|
||||||
export { AbstractCircuit } from "./AbstractCircuit";
|
export { AbstractCircuit } from "./AbstractCircuit";
|
||||||
|
export { CMSVisualizer } from "./CMSVisualizer";
|
||||||
|
export { ArchitectureVisualizer } from "./ArchitectureVisualizer";
|
||||||
|
export { ResultVisualizer } from "./ResultVisualizer";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "../utils/cn";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TECHNICAL MARKER COMPONENT
|
* TECHNICAL MARKER COMPONENT
|
||||||
@@ -19,24 +20,70 @@ export const Marker: React.FC<MarkerProps> = ({
|
|||||||
children,
|
children,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
className = "",
|
className = "",
|
||||||
color = "rgba(255,235,59,0.95)",
|
color = "rgba(255,235,59,0.7)",
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<span className={`relative inline-block px-1 ${className}`}>
|
<span className={cn("relative inline px-1", className)}>
|
||||||
<motion.span
|
<svg
|
||||||
initial={{ scaleX: 0, opacity: 0 }}
|
className="absolute inset-x-0 bottom-0 top-0 h-full w-full pointer-events-none z-[-1]"
|
||||||
whileInView={{ scaleX: 1, opacity: 1 }}
|
preserveAspectRatio="none"
|
||||||
viewport={{ once: true, margin: "-5%" }}
|
viewBox="0 0 100 100"
|
||||||
transition={{
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
duration: 1.2,
|
|
||||||
delay: delay + 0.1,
|
|
||||||
ease: [0.23, 1, 0.32, 1],
|
|
||||||
}}
|
|
||||||
className="absolute inset-0 z-[-1] -skew-x-6 rotate-[-1deg] translate-y-1 transform-gpu origin-left"
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
>
|
||||||
<span className="relative z-10 text-slate-900">{children}</span>
|
{/* Organic Stroke 1: Main body */}
|
||||||
|
<motion.path
|
||||||
|
d="M 0,85 C 10,87 25,82 40,84 C 55,86 75,81 90,83 C 95,84 100,85 100,85"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
whileInView={{ pathLength: 1, opacity: 1 }}
|
||||||
|
viewport={{ once: true, margin: "-5%" }}
|
||||||
|
transition={{
|
||||||
|
duration: 1.5,
|
||||||
|
delay: delay + 0.1,
|
||||||
|
ease: [0.23, 1, 0.32, 1],
|
||||||
|
}}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="60"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* Organic Stroke 2: Variation for overlap */}
|
||||||
|
<motion.path
|
||||||
|
d="M 5,82 C 20,80 40,85 60,82 C 80,79 95,84 100,83"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
whileInView={{ pathLength: 1, opacity: 0.6 }}
|
||||||
|
viewport={{ once: true, margin: "-5%" }}
|
||||||
|
transition={{
|
||||||
|
duration: 1.8,
|
||||||
|
delay: delay + 0.3,
|
||||||
|
ease: [0.23, 1, 0.32, 1],
|
||||||
|
}}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="35"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* Organic Stroke 3: Rough edge details */}
|
||||||
|
<motion.path
|
||||||
|
d="M 0,88 C 15,90 35,85 55,87 C 75,89 90,84 100,86"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
whileInView={{ pathLength: 1, opacity: 0.4 }}
|
||||||
|
viewport={{ once: true, margin: "-5%" }}
|
||||||
|
transition={{
|
||||||
|
duration: 1.2,
|
||||||
|
delay: delay + 0.2,
|
||||||
|
ease: [0.23, 1, 0.32, 1],
|
||||||
|
}}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="15"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="relative z-10 text-inherit">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,36 +22,25 @@ export const Reveal: React.FC<RevealProps> = ({
|
|||||||
scale = 0.98,
|
scale = 0.98,
|
||||||
blur = true,
|
blur = true,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef(null);
|
|
||||||
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
|
||||||
const mainControls = useAnimation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInView) {
|
|
||||||
mainControls.start("visible");
|
|
||||||
}
|
|
||||||
}, [isInView, mainControls]);
|
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
y: direction === "up" ? 15 : direction === "down" ? -15 : 0,
|
y: direction === "up" ? 20 : direction === "down" ? -20 : 0,
|
||||||
x: direction === "left" ? 15 : direction === "right" ? -15 : 0,
|
x: direction === "left" ? 20 : direction === "right" ? -20 : 0,
|
||||||
scale: scale !== 1 ? scale : 1,
|
scale: scale !== 1 ? scale : 1,
|
||||||
filter: blur ? "blur(4px)" : "none",
|
filter: blur ? "blur(8px)" : "none",
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
x: 0,
|
x: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
filter: "none",
|
filter: "blur(0px)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width,
|
width,
|
||||||
@@ -61,16 +50,21 @@ export const Reveal: React.FC<RevealProps> = ({
|
|||||||
<motion.div
|
<motion.div
|
||||||
variants={variants}
|
variants={variants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={mainControls}
|
whileInView="visible"
|
||||||
style={{ transformStyle: "preserve-3d" }}
|
viewport={{ once: true, margin: "-10%" }}
|
||||||
|
style={{
|
||||||
|
width: width === "100%" ? "100%" : "inherit",
|
||||||
|
backfaceVisibility: "hidden",
|
||||||
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.4,
|
duration: 0.5,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 260,
|
stiffness: 100,
|
||||||
damping: 20,
|
damping: 20,
|
||||||
mass: 1,
|
mass: 1,
|
||||||
opacity: { duration: 0.3, ease: [0.16, 1, 0.3, 1] },
|
opacity: { duration: 0.4, ease: [0.16, 1, 0.3, 1] },
|
||||||
|
filter: { duration: 0.4 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const Section: React.FC<SectionProps> = ({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative py-6 md:py-40 group overflow-hidden",
|
"relative py-12 md:py-40 group overflow-hidden",
|
||||||
bgClass,
|
bgClass,
|
||||||
borderTopClass,
|
borderTopClass,
|
||||||
borderBottomClass,
|
borderBottomClass,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const BlogCommandBar: React.FC<BlogCommandBarProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
placeholder="Search posts..."
|
placeholder="Beiträge suchen..."
|
||||||
className="w-full bg-transparent px-3 md:px-4 py-2 md:py-3 text-base md:text-lg text-slate-900 placeholder:text-slate-300 outline-none font-bold"
|
className="w-full bg-transparent px-3 md:px-4 py-2 md:py-3 text-base md:text-lg text-slate-900 placeholder:text-slate-300 outline-none font-bold"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
@@ -49,7 +49,7 @@ export const BlogCommandBar: React.FC<BlogCommandBarProps> = ({
|
|||||||
onClick={() => onSearchChange("")}
|
onClick={() => onSearchChange("")}
|
||||||
className="mr-2 px-2.5 py-1 md:px-3 md:py-1.5 bg-slate-100 hover:bg-slate-200 rounded-lg text-[9px] md:text-[10px] font-bold uppercase tracking-wider text-slate-500 hover:text-slate-900 transition-colors"
|
className="mr-2 px-2.5 py-1 md:px-3 md:py-1.5 bg-slate-100 hover:bg-slate-200 rounded-lg text-[9px] md:text-[10px] font-bold uppercase tracking-wider text-slate-500 hover:text-slate-900 transition-colors"
|
||||||
>
|
>
|
||||||
Clear
|
Leeren
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
apps/web/src/lib/env.ts
Normal file
20
apps/web/src/lib/env.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { validateMintelEnv } from "@mintel/next-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable schema extension.
|
||||||
|
*/
|
||||||
|
const envExtension = {
|
||||||
|
// Mail Configuration
|
||||||
|
MAIL_HOST: z.string().optional(),
|
||||||
|
MAIL_PORT: z.coerce.number().optional().default(587),
|
||||||
|
MAIL_USER: z.string().optional(),
|
||||||
|
MAIL_PASS: z.string().optional(),
|
||||||
|
MAIL_FROM: z.string().optional().default("marc@mintel.me"),
|
||||||
|
MAIL_RECIPIENTS: z.string().optional().default("marc@mintel.me"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validated environment object.
|
||||||
|
*/
|
||||||
|
export const env = validateMintelEnv(envExtension);
|
||||||
85
apps/web/src/lib/mail/mailer.ts
Normal file
85
apps/web/src/lib/mail/mailer.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import { env } from "../env";
|
||||||
|
|
||||||
|
let transporterInstance: nodemailer.Transporter | null = null;
|
||||||
|
|
||||||
|
function getTransporter() {
|
||||||
|
if (transporterInstance) return transporterInstance;
|
||||||
|
|
||||||
|
if (!env.MAIL_HOST) {
|
||||||
|
// In development, we might not have mail configured, so we log instead of throwing
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.warn(
|
||||||
|
"MAIL_HOST is not configured. Emails will be logged to console.",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
"MAIL_HOST is not configured. Please check your environment variables.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transporterInstance = nodemailer.createTransport({
|
||||||
|
host: env.MAIL_HOST,
|
||||||
|
port: env.MAIL_PORT,
|
||||||
|
secure: env.MAIL_PORT === 465,
|
||||||
|
auth: {
|
||||||
|
user: env.MAIL_USER,
|
||||||
|
pass: env.MAIL_PASS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return transporterInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendEmailOptions {
|
||||||
|
to?: string | string[];
|
||||||
|
replyTo?: string;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmail({
|
||||||
|
to,
|
||||||
|
replyTo,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
}: SendEmailOptions) {
|
||||||
|
const recipients = to || env.MAIL_RECIPIENTS;
|
||||||
|
const transporter = getTransporter();
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: env.MAIL_FROM,
|
||||||
|
to: recipients,
|
||||||
|
replyTo,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!transporter) {
|
||||||
|
console.log("--- EMAIL SIMULATION ---");
|
||||||
|
console.log("To:", recipients);
|
||||||
|
console.log("Subject:", subject);
|
||||||
|
console.log("HTML Output suppressed");
|
||||||
|
console.log("--- END SIMULATION ---");
|
||||||
|
return { success: true, messageId: "simulated-id" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await transporter.sendMail(mailOptions);
|
||||||
|
console.log("Email sent successfully", {
|
||||||
|
messageId: info.messageId,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
});
|
||||||
|
return { success: true, messageId: info.messageId };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error("Error sending email", {
|
||||||
|
error: errorMsg,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
});
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,39 +95,43 @@ export const PAGE_SAMPLES = [
|
|||||||
export const FEATURE_OPTIONS = [
|
export const FEATURE_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: "blog_news",
|
id: "blog_news",
|
||||||
label: "Blog / News",
|
label: "News & Artikel",
|
||||||
desc: "Ein Bereich für aktuelle Beiträge und Neuigkeiten.",
|
desc: "Ein Bereich für aktuelle Beiträge.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "products",
|
id: "products",
|
||||||
label: "Produktbereich",
|
label: "Produkte / Leistungen",
|
||||||
desc: "Katalog Ihrer Leistungen oder Produkte.",
|
desc: "Katalog Ihres Angebots.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "jobs",
|
id: "jobs",
|
||||||
label: "Karriere / Jobs",
|
label: "Jobs & Karriere",
|
||||||
desc: "Stellenanzeigen und Bewerbungsoptionen.",
|
desc: "Stellenanzeigen schalten.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "refs",
|
id: "refs",
|
||||||
label: "Referenzen / Cases",
|
label: "Projekte / Cases",
|
||||||
desc: "Präsentation Ihrer Projekte.",
|
desc: "Ihre Arbeiten präsentieren.",
|
||||||
},
|
},
|
||||||
{ id: "events", label: "Events / Termine", desc: "Veranstaltungskalender." },
|
{ id: "events", label: "Termine & Events", desc: "Veranstaltungskalender." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const FUNCTION_OPTIONS = [
|
export const FUNCTION_OPTIONS = [
|
||||||
{ id: "search", label: "Suche", desc: "Volltextsuche über alle Inhalte." },
|
{ id: "search", label: "Suchfunktion", desc: "Inhalte schnell finden." },
|
||||||
{
|
{
|
||||||
id: "filter",
|
id: "filter",
|
||||||
label: "Filter-Systeme",
|
label: "Filterung",
|
||||||
desc: "Kategorisierung und Sortierung.",
|
desc: "Inhalte sortieren & filtern.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pdf",
|
||||||
|
label: "Automatische PDFs",
|
||||||
|
desc: "Rechnungen oder Datenblätter.",
|
||||||
},
|
},
|
||||||
{ id: "pdf", label: "PDF-Export", desc: "Automatisierte PDF-Erstellung." },
|
|
||||||
{
|
{
|
||||||
id: "forms",
|
id: "forms",
|
||||||
label: "Individuelle Formular-Logik",
|
label: "Smarte Formulare",
|
||||||
desc: "Smarte Validierung & mehrstufige Prozesse.",
|
desc: "Anfragen mit Logik prüfen.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -99,13 +99,27 @@ export function getDefaultAnalytics(): AnalyticsService {
|
|||||||
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || "",
|
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || "",
|
||||||
hostUrl: env.UMAMI_API_ENDPOINT,
|
hostUrl: env.UMAMI_API_ENDPOINT,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (provider === "plausible") {
|
||||||
defaultAnalytics = createPlausibleAnalytics({
|
defaultAnalytics = createPlausibleAnalytics({
|
||||||
domain: env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN,
|
domain: env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN,
|
||||||
scriptUrl:
|
scriptUrl: env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL,
|
||||||
env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL ||
|
|
||||||
"https://plausible.yourdomain.com/js/script.js",
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// No analytics provider configured
|
||||||
|
return {
|
||||||
|
getAdapter: () => ({
|
||||||
|
track: async () => {},
|
||||||
|
page: async () => {},
|
||||||
|
getScriptTag: () => null,
|
||||||
|
}),
|
||||||
|
track: async () => {},
|
||||||
|
page: async () => {},
|
||||||
|
identify: async () => {},
|
||||||
|
trackEvent: async () => {},
|
||||||
|
trackOutboundLink: async () => {},
|
||||||
|
trackSearch: async () => {},
|
||||||
|
trackPageLoad: async () => {},
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultAnalytics;
|
return defaultAnalytics;
|
||||||
|
|||||||
@@ -3,36 +3,40 @@
|
|||||||
* Decoupled implementation
|
* Decoupled implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig } from './interfaces';
|
import type {
|
||||||
|
AnalyticsAdapter,
|
||||||
|
AnalyticsEvent,
|
||||||
|
AnalyticsConfig,
|
||||||
|
} from "./interfaces";
|
||||||
|
|
||||||
export class PlausibleAdapter implements AnalyticsAdapter {
|
export class PlausibleAdapter implements AnalyticsAdapter {
|
||||||
private domain: string;
|
private domain: string;
|
||||||
private scriptUrl: string;
|
private scriptUrl: string;
|
||||||
|
|
||||||
constructor(config: AnalyticsConfig) {
|
constructor(config: AnalyticsConfig) {
|
||||||
this.domain = config.domain || 'mintel.me';
|
this.domain = config.domain || "";
|
||||||
this.scriptUrl = config.scriptUrl || 'https://plausible.yourdomain.com/js/script.js';
|
this.scriptUrl = config.scriptUrl || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async track(event: AnalyticsEvent): Promise<void> {
|
async track(event: AnalyticsEvent): Promise<void> {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
if (w.plausible) {
|
if (w.plausible) {
|
||||||
w.plausible(event.name, {
|
w.plausible(event.name, {
|
||||||
props: event.props
|
props: event.props,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async page(path: string, props?: Record<string, any>): Promise<void> {
|
async page(path: string, props?: Record<string, any>): Promise<void> {
|
||||||
await this.track({
|
await this.track({
|
||||||
name: 'Pageview',
|
name: "Pageview",
|
||||||
props: { path, ...props }
|
props: { path, ...props },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptTag(): string {
|
getScriptTag(): string {
|
||||||
return `<script defer data-domain="${this.domain}" src="${this.scriptUrl}"></script>`;
|
return `<script defer data-domain="${this.domain}" src="${this.scriptUrl}"></script>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,14 +46,31 @@ export function getImgproxyUrl(
|
|||||||
// Handle local paths or relative URLs
|
// Handle local paths or relative URLs
|
||||||
let absoluteSrc = src;
|
let absoluteSrc = src;
|
||||||
if (src.startsWith("/")) {
|
if (src.startsWith("/")) {
|
||||||
const baseUrl =
|
const baseUrlForSrc =
|
||||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||||
(typeof window !== "undefined" ? window.location.origin : "");
|
(typeof window !== "undefined" ? window.location.origin : "");
|
||||||
if (baseUrl) {
|
if (baseUrlForSrc) {
|
||||||
absoluteSrc = `${baseUrl}${src}`;
|
absoluteSrc = `${baseUrlForSrc}${src}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Development mapping: Map local domains to internal Docker hostnames
|
||||||
|
// so imgproxy can fetch images without SSL issues or external routing
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
if (absoluteSrc.includes("mintel.localhost")) {
|
||||||
|
absoluteSrc = absoluteSrc.replace(
|
||||||
|
/^https?:\/\/mintel\.localhost/,
|
||||||
|
"http://app:3000",
|
||||||
|
);
|
||||||
|
} else if (absoluteSrc.includes("cms.mintel.localhost")) {
|
||||||
|
absoluteSrc = absoluteSrc.replace(
|
||||||
|
/^https?:\/\/cms\.mintel\.localhost/,
|
||||||
|
"http://directus:8055",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`[imgproxy] ${src} -> ${absoluteSrc}`);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width = 0,
|
width = 0,
|
||||||
height = 0,
|
height = 0,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const ContactFormShowcase: React.FC = () => {
|
|||||||
state.name = text.substring(0, Math.round(progress));
|
state.name = text.substring(0, Math.round(progress));
|
||||||
}
|
}
|
||||||
if (frame > T.EMAIL_TYPE_START) {
|
if (frame > T.EMAIL_TYPE_START) {
|
||||||
const text = "marc@mintel.me";
|
const text = "request@mintel.me";
|
||||||
const progress = interpolate(
|
const progress = interpolate(
|
||||||
frame,
|
frame,
|
||||||
[T.EMAIL_TYPE_START, T.EMAIL_TYPE_END],
|
[T.EMAIL_TYPE_START, T.EMAIL_TYPE_END],
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ services:
|
|||||||
- infra
|
- infra
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-web.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-web.entrypoints=web"
|
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=http://${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
gatekeeper:
|
gatekeeper:
|
||||||
profiles: ["gatekeeper"]
|
profiles: ["gatekeeper"]
|
||||||
@@ -31,9 +29,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-gatekeeper.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-gatekeeper.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=http://gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
directus:
|
directus:
|
||||||
image: registry.infra.mintel.me/mintel/directus:latest
|
image: registry.infra.mintel.me/mintel/directus:latest
|
||||||
@@ -58,11 +57,10 @@ services:
|
|||||||
- ./directus/extensions:/directus/extensions
|
- ./directus/extensions:/directus/extensions
|
||||||
- ./directus/migrations:/directus/migrations
|
- ./directus/migrations:/directus/migrations
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.entrypoints=web"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=http://${DIRECTUS_HOST:-cms.mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 8055}}"
|
||||||
|
|
||||||
directus-db:
|
directus-db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
@@ -89,16 +87,17 @@ services:
|
|||||||
- "cms.mintel.localhost:host-gateway"
|
- "cms.mintel.localhost:host-gateway"
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
|
IMGPROXY_URL_MAPPING: "http://mintel.localhost/:http://app:3000/,http://cms.mintel.localhost/:http://directus:8055/"
|
||||||
IMGPROXY_USE_ETAG: "true"
|
IMGPROXY_USE_ETAG: "true"
|
||||||
IMGPROXY_MAX_SRC_RESOLUTION: 20
|
IMGPROXY_MAX_SRC_RESOLUTION: 20
|
||||||
IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
||||||
IMGPROXY_URL_MAPPING: "http://mintel.localhost/:http://app:3000/,http://cms.mintel.localhost/:http://directus:8055/"
|
IMGPROXY_IGNORE_SSL_ERRORS: "true"
|
||||||
|
IMGPROXY_DEBUG: "true"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-imgproxy.rule=Host("${IMGPROXY_HOST:-img.mintel.localhost}")'
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-imgproxy.entrypoints=web"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-imgproxy.loadbalancer.server.port=8080"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-imgproxy.loadbalancer.server.port=8080"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=http://${IMGPROXY_HOST:-img.mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 8080}}"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-mintel-me}-ratelimit,${PROJECT_NAME:-mintel-me}-forward}"
|
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-mintel-me}-ratelimit,${PROJECT_NAME:-mintel-me}-forward}"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
|
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
|
||||||
@@ -73,9 +75,10 @@ services:
|
|||||||
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-mintel}
|
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-mintel}
|
||||||
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
|
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-gatekeeper.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-gatekeeper.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
directus:
|
directus:
|
||||||
image: registry.infra.mintel.me/mintel/directus:latest
|
image: registry.infra.mintel.me/mintel/directus:latest
|
||||||
@@ -113,6 +116,8 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.middlewares=${PROJECT_NAME:-mintel-me}-forward"
|
- "traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.middlewares=${PROJECT_NAME:-mintel-me}-forward"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=${DIRECTUS_HOST:-cms.mintel.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 8055}}"
|
||||||
|
|
||||||
directus-db:
|
directus-db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "docker network create infra 2>/dev/null || true && echo \"\n🚀 Development Environment Starting...\n\n📱 App: http://mintel.localhost\n🗄️ CMS: http://cms.mintel.localhost/admin\n🖼️ Imgproxy: http://img.mintel.localhost\n🚦 Traefik: http://localhost:8080\n\" && lsof -ti:3000 | xargs kill -9 2>/dev/null || true && rm -f apps/web/.next/dev/lock 2>/dev/null || true && docker rm -f mintel-me-gatekeeper 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down --remove-orphans && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up -d app directus directus-db gatekeeper imgproxy && pnpm -r dev",
|
"dev": "docker network create infra 2>/dev/null || true && echo \"\n🚀 Development Environment Starting...\n\n📱 App: http://mintel.localhost\n🗄️ CMS: http://cms.mintel.localhost/admin\n🖼️ Imgproxy: http://img.mintel.localhost\n🚦 Caddy Proxy: http://localhost:80\n\" && lsof -ti:3000 | xargs kill -9 2>/dev/null || true && rm -f apps/web/.next/dev/lock 2>/dev/null || true && docker rm -f mintel-me-gatekeeper 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down --remove-orphans && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up -d app directus directus-db gatekeeper imgproxy && pnpm -r dev",
|
||||||
"dev:local": "pnpm -r dev",
|
"dev:local": "pnpm -r dev",
|
||||||
"build": "pnpm -r build",
|
"build": "pnpm -r build",
|
||||||
"start": "pnpm -r start",
|
"start": "pnpm -r start",
|
||||||
|
|||||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -178,6 +178,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: ^16.1.6
|
specifier: ^16.1.6
|
||||||
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
nodemailer:
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1
|
||||||
playwright:
|
playwright:
|
||||||
specifier: ^1.58.1
|
specifier: ^1.58.1
|
||||||
version: 1.58.2
|
version: 1.58.2
|
||||||
@@ -257,6 +260,9 @@ importers:
|
|||||||
"@types/node":
|
"@types/node":
|
||||||
specifier: ^25.0.6
|
specifier: ^25.0.6
|
||||||
version: 25.2.0
|
version: 25.2.0
|
||||||
|
"@types/nodemailer":
|
||||||
|
specifier: ^7.0.10
|
||||||
|
version: 7.0.10
|
||||||
"@types/prismjs":
|
"@types/prismjs":
|
||||||
specifier: ^1.26.5
|
specifier: ^1.26.5
|
||||||
version: 1.26.5
|
version: 1.26.5
|
||||||
@@ -4150,6 +4156,12 @@ packages:
|
|||||||
integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==,
|
integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"@types/nodemailer@7.0.10":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==,
|
||||||
|
}
|
||||||
|
|
||||||
"@types/pg-pool@2.0.7":
|
"@types/pg-pool@2.0.7":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -9214,6 +9226,13 @@ packages:
|
|||||||
integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==,
|
integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodemailer@8.0.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==,
|
||||||
|
}
|
||||||
|
engines: { node: ">=6.0.0" }
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -15179,6 +15198,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
|
"@types/nodemailer@7.0.10":
|
||||||
|
dependencies:
|
||||||
|
"@types/node": 25.2.0
|
||||||
|
|
||||||
"@types/pg-pool@2.0.7":
|
"@types/pg-pool@2.0.7":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/pg": 8.15.6
|
"@types/pg": 8.15.6
|
||||||
@@ -18586,6 +18609,8 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
|
nodemailer@8.0.1: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user