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

- 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:
2026-02-16 19:34:08 +01:00
parent cb32b9d62f
commit 9cfe7ee9e5
58 changed files with 3231 additions and 1592 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
};

View 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">
&copy; ${new Date().getFullYear()} mintel.me — Technical Problem Solving
</div>
</div>
</body>
</html>
`;

View File

@@ -1,8 +1,8 @@
'use client';
"use client";
import * as React from 'react';
import { Check } from 'lucide-react';
import { motion } from 'framer-motion';
import * as React from "react";
import { Check } from "lucide-react";
import { motion } from "framer-motion";
interface CheckboxProps {
label: string;
@@ -16,24 +16,38 @@ export function Checkbox({ label, desc, checked, onChange }: CheckboxProps) {
<button
type="button"
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 ${
checked ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
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"
}`}
>
<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 && (
<motion.div
initial={{ scale: 0, rotate: -45 }}
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>
)}
</div>
<div className="flex-grow">
<h4 className={`text-2xl font-bold mb-1 transition-colors duration-500 ${checked ? 'text-white' : 'text-slate-900'}`}>{label}</h4>
{desc && <p className={`text-lg leading-relaxed transition-colors duration-500 ${checked ? 'text-slate-300' : 'text-slate-500'}`}>{desc}</p>}
<h4
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>
{checked && (
<motion.div

View File

@@ -1,18 +1,26 @@
'use client';
"use client";
import * as React from 'react';
import { LucideIcon } from 'lucide-react';
import * as React from "react";
import { LucideIcon } from "lucide-react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
interface InputProps extends React.InputHTMLAttributes<
HTMLInputElement | HTMLTextAreaElement
> {
label?: string;
icon?: LucideIcon;
isTextArea?: boolean;
rows?: number;
}
export function Input({ label, icon: Icon, isTextArea, className = '', ...props }: InputProps) {
const InputComponent = isTextArea ? 'textarea' : 'input';
export function Input({
label,
icon: Icon,
isTextArea,
className = "",
...props
}: InputProps) {
const InputComponent = isTextArea ? "textarea" : "input";
return (
<div className="space-y-4 w-full">
{label && (
@@ -22,13 +30,15 @@ export function Input({ label, icon: Icon, isTextArea, className = '', ...props
)}
<div className="relative group">
{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} />
</div>
)}
<InputComponent
{...(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>

View File

@@ -4,7 +4,6 @@ import * as React from "react";
import { FormState, Totals } from "../types";
import { PRICING } from "../constants";
import { AnimatedNumber } from "./AnimatedNumber";
/* eslint-disable no-unused-vars */
import { ConceptAutomation } from "../../Landing/ConceptIllustrations";
import { Download, Share2, RefreshCw } from "lucide-react";
@@ -49,9 +48,9 @@ export function PriceCalculation({
setPdfLoading(true);
try {
const { EstimationPDF } = await import("@mintel/pdf");
const { LocalEstimationPDF } = await import("../pdf/LocalEstimationPDF");
const doc = (
<EstimationPDF
<LocalEstimationPDF
state={state}
totalPrice={totalPrice}
monthlyPrice={monthlyPrice}
@@ -63,6 +62,7 @@ export function PriceCalculation({
footerLogo={
typeof LogoBlack === "string" ? LogoBlack : (LogoBlack as any).src
}
qrCodeData={_qrCodeData}
/>
);
@@ -91,8 +91,8 @@ export function PriceCalculation({
return (
<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="space-y-6">
<div className="bg-slate-50 border border-slate-100 rounded-2xl p-5 space-y-4">
<div className="space-y-4">
{state.projectType === "website" ? (
<>
<div className="space-y-4 overflow-y-auto pr-2 hide-scrollbar max-h-[120px]">

View 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>
);
};

View File

@@ -29,14 +29,14 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
return (
<div className="space-y-12">
<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 items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Share2 size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Share2 size={16} />
</div>
<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
? "Integrationen & Datenquellen"
: "Schnittstellen (API)"}
@@ -117,12 +117,12 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<ListPlus size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<ListPlus size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Weitere Systeme oder eigene APIs?
</h4>
</div>

View File

@@ -32,13 +32,13 @@ export function AssetsStep({
return (
<div className="space-y-12">
<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 items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Briefcase size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Briefcase size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Vorhandene Assets
</h4>
</div>
@@ -90,12 +90,12 @@ export function AssetsStep({
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<ListPlus size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<ListPlus size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Weitere vorhandene Unterlagen?
</h4>
</div>

View File

@@ -32,11 +32,11 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
};
return (
<div className="space-y-16">
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8"
className="space-y-6"
>
<div className="relative">
<Input
@@ -51,27 +51,24 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
</div>
</motion.div>
<div className="space-y-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-slate-900 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-slate-200">
<FileText size={28} />
<div className="space-y-4">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-slate-900 text-white rounded-xl flex items-center justify-center shadow-sm">
<FileText size={16} />
</div>
<div>
<div className="flex items-center gap-3">
<h4 className="text-3xl font-bold text-slate-900 tracking-tight">
<div className="flex items-center gap-2">
<h4 className="text-lg font-bold text-slate-900 tracking-tight">
Die Seitenstruktur
</h4>
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
<span className="px-2 py-0.5 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
Essenziell
</span>
</div>
<div className="flex items-center gap-2 text-slate-400 mt-1">
<HelpCircle size={14} className="shrink-0" />
<span className="text-base">
Wählen Sie die Bausteine Ihrer neuen Website.
</span>
</div>
<p className="text-sm text-slate-400 mt-0.5">
Wählen Sie die Bausteine Ihrer neuen Website.
</p>
</div>
</div>
<motion.button
@@ -88,7 +85,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
Ich weiß es nicht
</motion.button>
</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",
@@ -143,12 +140,12 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
</div>
<div className="space-y-12">
<div className="space-y-8">
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<ListPlus size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<ListPlus size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Weitere individuelle Seiten?
</h4>
</div>
@@ -168,23 +165,23 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
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">
<ListPlus size={120} />
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<ListPlus size={60} />
</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>
<h4 className="text-2xl font-bold text-white">
<h4 className="text-lg font-bold text-white">
Noch mehr Seiten?
</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
können.
</p>
</div>
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
@@ -194,9 +191,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
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>
<AnimatePresence mode="wait">
<motion.span
@@ -204,7 +201,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.5 }}
className="text-6xl font-bold w-16 text-center"
className="text-3xl font-bold w-10 text-center"
>
{state.otherPagesCount}
</motion.span>
@@ -216,9 +213,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
onClick={() =>
updateState({ otherPagesCount: state.otherPagesCount + 1 })
}
className="w-16 h-16 rounded-full bg-white/10 border border-white/10 flex items-center justify-center hover:bg-white/20 transition-colors focus:outline-none"
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>
</div>
</div>

View File

@@ -15,14 +15,14 @@ interface CompanyStepProps {
export function CompanyStep({ state, updateState }: CompanyStepProps) {
return (
<div className="space-y-16">
<div className="space-y-6">
<Reveal width="100%" delay={0.1}>
<div className="space-y-8" id="focus-target-company">
<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">
<Building2 size={24} />
<div className="space-y-4" id="focus-target-company">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Building2 size={16} />
</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">
Erforderlich
</span>
@@ -37,24 +37,24 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
</Reveal>
<Reveal width="100%" delay={0.2}>
<div className="space-y-8">
<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">
<Users size={24} />
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Users size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Mitarbeiteranzahl
</h4>
</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) => (
<motion.button
key={option.id}
whileHover={{ y: -5 }}
whileHover={{ y: -3 }}
whileTap={{ scale: 0.95 }}
type="button"
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
? "border-slate-900 bg-slate-900 text-white"
: "border-slate-100 bg-white hover:border-slate-300 text-slate-600"

View File

@@ -29,15 +29,15 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
};
return (
<div className="space-y-16">
<div className="space-y-6">
<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="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Settings2 size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Settings2 size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Inhalte selbst verwalten (CMS)
</h4>
</div>
@@ -77,10 +77,10 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
</Reveal>
<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="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-black">
<BarChart3 size={24} />
<BarChart3 size={16} />
</div>
<p className="text-xl font-bold text-slate-900">
Wie oft ändern sich Ihre Inhalte?
@@ -107,7 +107,7 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
whileTap={{ scale: 0.98 }}
type="button"
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
? "border-slate-900 bg-slate-900 text-white"
: "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"
>
<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 className="space-y-2">
<p className="text-amber-900 text-xl font-bold">
@@ -176,9 +176,9 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
</Reveal>
<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">
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Inhalte einpflegen
</h4>
<p className="text-lg text-slate-500 leading-relaxed">

View File

@@ -73,13 +73,13 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
};
return (
<div className="space-y-16">
<div className="space-y-6">
{/* Design Vibe */}
<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="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Design-Richtung
</h4>
<p className="text-slate-500">
@@ -145,7 +145,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
<div className="space-y-12">
<div className="flex justify-between items-center">
<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">
Definieren Sie Ihre Markenfarben oder lassen Sie sich
inspirieren.
@@ -179,7 +179,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
</div>
{/* 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">
<Pipette size={16} />
Individuelle Farben
@@ -251,16 +251,16 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
{/* References */}
<Reveal width="100%" delay={0.3}>
<div className="space-y-8">
<div className="space-y-6">
<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
</h4>
<p className="text-slate-500">
Gibt es Websites, die Ihnen besonders gut gefallen?
</p>
</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
items={state.references || []}
onAdd={(v) =>

View File

@@ -29,16 +29,16 @@ export function FeaturesStep({
};
return (
<div className="space-y-16">
<div className="space-y-8">
<div className="space-y-6">
<div className="space-y-6">
<div className="flex justify-between items-center">
<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">
<LayoutGrid size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<LayoutGrid size={16} />
</div>
<div>
<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
</h4>
<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 className="space-y-12">
<div className="space-y-8">
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<ListPlus size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<ListPlus size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Weitere inhaltliche Module?
</h4>
</div>

View File

@@ -33,13 +33,13 @@ export function FunctionsStep({
return (
<div className="space-y-12">
<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 items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Cpu size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Cpu size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
{isWebApp
? "Funktionale Anforderungen"
: "Erweiterte Funktionen"}
@@ -183,12 +183,12 @@ export function FunctionsStep({
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="space-y-8">
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<ListPlus size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<ListPlus size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Weitere spezifische Wünsche?
</h4>
</div>

View File

@@ -46,13 +46,13 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
return (
<div className="space-y-12">
<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 items-center gap-4">
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Globe size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Globe size={16} />
</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">
Optional
</span>
@@ -72,7 +72,7 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
</motion.button>
</div>
<div className="space-y-8">
<div className="space-y-6">
<p className="text-lg text-slate-500 leading-relaxed ml-2">
Welche Sprachen soll Ihre Website unterstützen?
</p>
@@ -153,10 +153,10 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
</Reveal>
<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="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">
Warum dieser Faktor?
</span>

View File

@@ -46,14 +46,14 @@ export function PresenceStep({
];
return (
<div className="space-y-16">
<div className="space-y-6">
<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="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Globe size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Globe size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Bestehende Website
</h4>
<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>
</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}>
<Input
label="Bestehende Domain"
@@ -91,12 +91,12 @@ export function PresenceStep({
</div>
<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="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
<Share2 size={24} />
<div className="w-8 h-8 bg-slate-50 rounded-xl flex items-center justify-center text-black">
<Share2 size={16} />
</div>
<h4 className="text-2xl font-bold text-slate-900">
<h4 className="text-lg font-bold text-slate-900">
Social Media Accounts
</h4>
</div>
@@ -161,7 +161,7 @@ export function PresenceStep({
placeholder={`https://${platform.id}.com/ihr-profil`}
value={state.socialMediaUrls[id] || ""}
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>
);
@@ -169,7 +169,7 @@ export function PresenceStep({
</AnimatePresence>
{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">
Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.
</p>

View File

@@ -30,7 +30,7 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
<div className="space-y-12">
<div className="space-y-6">
<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
type="button"
onClick={() => toggleDontKnow("timeline")}
@@ -91,7 +91,7 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
</div>
</div>
{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} />
<p className="text-base text-slate-600 leading-relaxed">
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein
@@ -102,9 +102,9 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
)}
{(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">
<AlertCircle size={24} />
<AlertCircle size={16} />
</div>
<div className="space-y-2">
<p className="text-amber-900 text-xl font-bold">

View File

@@ -16,19 +16,19 @@ interface TypeStepProps {
export function TypeStep({ state, updateState }: TypeStepProps) {
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",
label: "Website",
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",
label: "Web App",
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) => (
<Reveal key={type.id} width="100%" delay={index * 0.1}>
@@ -38,7 +38,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
whileTap={{ scale: 0.98 }}
type="button"
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
? "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"
@@ -49,9 +49,9 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
>
{type.illustration}
</div>
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-3 mb-3">
<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}
</h4>
@@ -62,7 +62,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
</span>
</div>
<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}
</p>
@@ -70,7 +70,7 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
{state.projectType === type.id && (
<motion.div
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" />
</motion.div>

View File

@@ -23,8 +23,8 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
{/* Target Audience */}
<div className="space-y-6">
<div className="flex items-center gap-4">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Users size={24} className="text-black" /> Zielgruppe
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
<Users size={16} className="text-black" /> Zielgruppe
</h4>
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
Fokus
@@ -47,7 +47,7 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
key={opt.id}
type="button"
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
? "border-slate-900 bg-slate-900 text-white"
: "border-slate-100 bg-white hover:border-slate-200"
@@ -66,7 +66,7 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
{/* User Roles */}
<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>
<div className="flex flex-wrap gap-4">
{[
@@ -94,32 +94,32 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
{/* Platform Type */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Monitor size={24} className="text-black" /> Plattform-Fokus
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
<Monitor size={16} className="text-black" /> Plattform-Fokus
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{
id: "desktop",
label: "Desktop First",
icon: <Monitor size={24} />,
icon: <Monitor size={16} />,
},
{
id: "mobile",
label: "Mobile First",
icon: <Smartphone size={24} />,
icon: <Smartphone size={16} />,
},
{
id: "pwa",
label: "PWA (Installierbar)",
icon: <Globe size={24} />,
icon: <Globe size={16} />,
},
].map((opt) => (
<button
key={opt.id}
type="button"
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
? "border-slate-900 bg-slate-900 text-white"
: "border-slate-100 bg-white hover:border-slate-200"
@@ -140,8 +140,8 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
{/* Data Sensitivity */}
<div className="space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Shield size={24} className="text-black" /> Datensicherheit
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
<Shield size={16} className="text-black" /> Datensicherheit
</h4>
<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}
type="button"
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
? "border-slate-900 bg-slate-900 text-white"
: "border-slate-100 bg-white hover:border-slate-200"
@@ -178,9 +178,9 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
</div>
{/* Authentication */}
<div className="p-10 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-6">
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
<Lock size={24} className="text-black" /> Authentifizierung
<div className="p-10 bg-slate-50 rounded-2xl border border-slate-100 space-y-6">
<h4 className="text-lg font-bold text-slate-900 flex items-center gap-3">
<Lock size={16} className="text-black" /> Authentifizierung
</h4>
<p className="text-lg text-slate-500">
Wie sollen sich Nutzer anmelden?

View File

@@ -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 {
projectType: ProjectType;

View 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>
);
};

View 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>
);
};

View File

@@ -28,7 +28,7 @@ export const CodeWindow: React.FC<CodeWindowProps> = ({
return (
<div
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]",
className,
)}

View 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>
);
};

View File

@@ -4,3 +4,6 @@ export { BinaryStream } from "./BinaryStream";
export { GradientMesh } from "./GradientMesh";
export { CodeSnippet } from "./CodeSnippet";
export { AbstractCircuit } from "./AbstractCircuit";
export { CMSVisualizer } from "./CMSVisualizer";
export { ArchitectureVisualizer } from "./ArchitectureVisualizer";
export { ResultVisualizer } from "./ResultVisualizer";

View File

@@ -2,6 +2,7 @@
import React from "react";
import { motion } from "framer-motion";
import { cn } from "../utils/cn";
/**
* TECHNICAL MARKER COMPONENT
@@ -19,24 +20,70 @@ export const Marker: React.FC<MarkerProps> = ({
children,
delay = 0,
className = "",
color = "rgba(255,235,59,0.95)",
color = "rgba(255,235,59,0.7)",
}) => {
return (
<span className={`relative inline-block px-1 ${className}`}>
<motion.span
initial={{ scaleX: 0, opacity: 0 }}
whileInView={{ scaleX: 1, opacity: 1 }}
viewport={{ once: true, margin: "-5%" }}
transition={{
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 }}
<span className={cn("relative inline px-1", className)}>
<svg
className="absolute inset-x-0 bottom-0 top-0 h-full w-full pointer-events-none z-[-1]"
preserveAspectRatio="none"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
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>
);
};

View File

@@ -22,36 +22,25 @@ export const Reveal: React.FC<RevealProps> = ({
scale = 0.98,
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 = {
hidden: {
opacity: 0,
y: direction === "up" ? 15 : direction === "down" ? -15 : 0,
x: direction === "left" ? 15 : direction === "right" ? -15 : 0,
y: direction === "up" ? 20 : direction === "down" ? -20 : 0,
x: direction === "left" ? 20 : direction === "right" ? -20 : 0,
scale: scale !== 1 ? scale : 1,
filter: blur ? "blur(4px)" : "none",
filter: blur ? "blur(8px)" : "none",
},
visible: {
opacity: 1,
y: 0,
x: 0,
scale: 1,
filter: "none",
filter: "blur(0px)",
},
};
return (
<div
ref={ref}
style={{
position: "relative",
width,
@@ -61,16 +50,21 @@ export const Reveal: React.FC<RevealProps> = ({
<motion.div
variants={variants}
initial="hidden"
animate={mainControls}
style={{ transformStyle: "preserve-3d" }}
whileInView="visible"
viewport={{ once: true, margin: "-10%" }}
style={{
width: width === "100%" ? "100%" : "inherit",
backfaceVisibility: "hidden",
}}
transition={{
duration: 0.4,
duration: 0.5,
delay: delay,
type: "spring",
stiffness: 260,
stiffness: 100,
damping: 20,
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}

View File

@@ -49,7 +49,7 @@ export const Section: React.FC<SectionProps> = ({
return (
<section
className={cn(
"relative py-6 md:py-40 group overflow-hidden",
"relative py-12 md:py-40 group overflow-hidden",
bgClass,
borderTopClass,
borderBottomClass,

View File

@@ -41,7 +41,7 @@ export const BlogCommandBar: React.FC<BlogCommandBarProps> = ({
type="text"
value={searchQuery}
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"
/>
{searchQuery && (
@@ -49,7 +49,7 @@ export const BlogCommandBar: React.FC<BlogCommandBarProps> = ({
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"
>
Clear
Leeren
</button>
)}
</div>