feat(ui): enhance component share UX and add new interactive simulations
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🏗️ Build (push) Failing after 26s
Build & Deploy / 🧪 QA (push) Failing after 1m14s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-22 16:58:42 +01:00
parent d9ddce412a
commit 38f2cc8b85
22 changed files with 918 additions and 163 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useId } from 'react';
import { ComponentShareButton } from './ComponentShareButton';
interface BoldNumberProps {
/** The number to display, e.g. "53%" or "2.5M€" or "-20%" */
@@ -28,6 +29,7 @@ export const BoldNumber: React.FC<BoldNumberProps> = ({
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const [displayValue, setDisplayValue] = useState('');
const shareId = `boldnum-${useId().replace(/:/g, '')}`;
// Extract numeric part for animation
const numericMatch = value.match(/^([+-]?)(\d+(?:[.,]\d+)?)(.*)/);
@@ -84,22 +86,16 @@ export const BoldNumber: React.FC<BoldNumberProps> = ({
return () => clearInterval(timer);
}, [isVisible, value, prefix, suffix, targetNum, hasDecimals, numStr]);
const handleShare = async () => {
const shareText = `${value}${label}${source ? ` (${source})` : ''}`;
try {
if (navigator.share) {
await navigator.share({ text: shareText });
} else {
await navigator.clipboard.writeText(shareText);
}
} catch { /* user cancelled */ }
};
return (
<div
ref={ref}
className={`not-prose relative overflow-hidden rounded-2xl my-16 border border-slate-100 bg-slate-50/50 p-10 md:p-14 text-center ${className}`}
id={shareId}
className={`not-prose relative overflow-hidden rounded-2xl my-16 border border-slate-100 bg-slate-50/50 p-10 md:p-14 text-center group ${className}`}
>
<div className="absolute top-4 right-4 z-50 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<ComponentShareButton targetId={shareId} title={`Statistik: ${label}`} />
</div>
<div className="relative z-10">
<span className="block text-6xl md:text-8xl font-black tracking-tighter tabular-nums leading-none text-slate-900 pb-2">
{displayValue || value}
@@ -119,19 +115,6 @@ export const BoldNumber: React.FC<BoldNumberProps> = ({
</span>
)}
</div>
{/* Share button - subtle now */}
<button
onClick={handleShare}
className="absolute top-4 right-4 z-20 p-2 rounded-lg text-slate-300 hover:text-blue-600 hover:bg-blue-50 transition-all cursor-pointer"
title="Teilen"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
</button>
</div>
);
};

View File

@@ -98,10 +98,10 @@ export const Button: React.FC<ButtonProps> = ({
// Binary stream overlay colors by variant
const binaryColor =
variant === "primary"
? "rgba(255,255,255,0.06)"
? "rgba(255,255,255,0.12)"
: variant === "outline"
? "rgba(59,130,246,0.08)"
: "rgba(148,163,184,0.06)";
? "rgba(59,130,246,0.15)"
: "rgba(148,163,184,0.15)";
const inner = (
<motion.span
@@ -115,7 +115,7 @@ export const Button: React.FC<ButtonProps> = ({
style={{ opacity: hovered ? 1 : 0, transition: "opacity 0.3s ease" }}
>
<motion.span
className="whitespace-nowrap font-mono text-[8px] tracking-[0.3em] select-none"
className="whitespace-nowrap font-mono text-xs tracking-[0.4em] select-none"
style={{ color: binaryColor }}
animate={hovered ? { x: [0, -200] } : { x: 0 }}
transition={

View File

@@ -5,16 +5,19 @@ import { Share2 } from "lucide-react";
import { ShareModal } from "./ShareModal";
import { useAnalytics } from "./analytics/useAnalytics";
import * as htmlToImage from "html-to-image";
import QRCode from "qrcode";
interface ComponentShareButtonProps {
targetId: string;
title?: string;
shareText?: string;
className?: string;
}
export const ComponentShareButton: React.FC<ComponentShareButtonProps> = ({
targetId,
title = "Component",
title = "Mintel Architektur Insights",
shareText = "Schauen Sie sich diese spannende interaktive Simulation an:",
className = ""
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -27,31 +30,131 @@ export const ComponentShareButton: React.FC<ComponentShareButtonProps> = ({
? `${window.location.origin}${window.location.pathname}#${targetId}`
: "";
const applyWatermark = async (base64Img: string, qrCodeSrc: string): Promise<string> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
// html-to-image uses pixelRatio: 2, so img.width/img.height are 2x
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) return resolve(base64Img);
// Draw main image
ctx.drawImage(img, 0, 0);
// Watermark styling for 2x resolution
const padding = 32;
const wHeight = 110;
const wWidth = 360;
const x = canvas.width - wWidth - padding;
const y = canvas.height - wHeight - padding;
// Draw pill shape background
ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
ctx.shadowColor = "rgba(0, 0, 0, 0.05)";
ctx.shadowBlur = 10;
ctx.shadowOffsetY = 4;
if (ctx.roundRect) {
ctx.beginPath();
ctx.roundRect(x, y, wWidth, wHeight, 24);
ctx.fill();
} else {
ctx.fillRect(x, y, wWidth, wHeight);
}
// Draw text
ctx.shadowColor = "transparent";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
// "mintel."
ctx.font = "900 36px sans-serif";
const textX = x + wWidth - wHeight - 12; // Right of text, left of QR code
const mintelText = "mintel";
const dotText = ".";
const dotWidth = ctx.measureText(dotText).width;
const mintelWidth = ctx.measureText(mintelText).width;
ctx.fillStyle = "#0ea5e9";
ctx.fillText(dotText, textX, y + 40);
ctx.fillStyle = "#0f172a";
ctx.fillText(mintelText, textX - dotWidth, y + 40);
// "Artikel lesen"
ctx.font = "600 20px sans-serif";
ctx.fillStyle = "#64748b";
ctx.fillText("Zum Artikel", textX, y + 76);
// Draw solid line separator
ctx.beginPath();
ctx.moveTo(textX + 6, y + 20);
ctx.lineTo(textX + 6, y + wHeight - 20);
ctx.strokeStyle = "#e2e8f0";
ctx.lineWidth = 2;
ctx.stroke();
// Draw QR Code
const qrImg = new Image();
qrImg.onload = () => {
const qrSize = wHeight - 24; // padding 12*2 inside pill
const qrX = x + wWidth - qrSize - 12;
const qrY = y + 12;
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
resolve(canvas.toDataURL("image/png"));
};
qrImg.onerror = () => resolve(base64Img); // Error fallback
qrImg.src = qrCodeSrc;
};
img.onerror = () => resolve(base64Img); // Error fallback
img.src = base64Img;
});
};
const handleOpenModal = async () => {
setIsCapturing(true);
try {
const element = document.getElementById(targetId);
if (element) {
const dataUrl = await htmlToImage.toPng(element, {
// Force explicit dimensions to prevent html-to-image from clipping
const w = element.offsetWidth;
const h = element.offsetHeight;
const rawDataUrl = await htmlToImage.toPng(element, {
quality: 1,
type: 'image/png',
pixelRatio: 2,
backgroundColor: 'transparent',
backgroundColor: 'white',
skipFonts: true,
// Filter out any buttons that are part of the UI but shouldn't be in the image
width: w,
height: h,
style: {
transform: 'none',
margin: '0',
width: `${w}px`,
height: `${h}px`
},
filter: (node) => {
if (node.tagName === 'BUTTON' || (node as HTMLElement).dataset?.shareButton === 'true') {
const el = node as HTMLElement;
if (el.tagName === 'BUTTON' || el.dataset?.shareButton === 'true' || el.dataset?.shareWrapper === 'true') {
return false;
}
return true;
},
style: {
// Ensure we don't accidentally cut off by resetting transform/margins inside the clone
transform: 'none',
}
});
setGeneratedImage(dataUrl);
// Generate QR Code
const qrDataUrl = await QRCode.toDataURL(currentUrl, {
width: 120, // slightly larger for sharper scaling
margin: 0,
color: { dark: '#0f172a', light: '#ffffff' }
});
// Add Watermark
const watermarkedImage = await applyWatermark(rawDataUrl, qrDataUrl);
setGeneratedImage(watermarkedImage);
}
} catch (err) {
console.error("Failed to capture component:", err);
@@ -60,40 +163,41 @@ export const ComponentShareButton: React.FC<ComponentShareButtonProps> = ({
setIsModalOpen(true);
trackEvent("component_share_opened", {
component_id: targetId,
component_title: title,
component_title: title
});
}
};
return (
<>
<div className={`relative ${className}`}>
<button
onClick={handleOpenModal}
disabled={isCapturing}
data-share-button="true"
className={`inline-flex z-20 items-center justify-center gap-2 px-3 py-1.5 bg-white border border-slate-200 rounded-sm text-slate-500 hover:text-slate-900 hover:bg-slate-50 hover:border-slate-400 transition-all text-[10px] font-mono uppercase tracking-widest ${className}`}
aria-label="Component als Grafik teilen"
className={`flex items-center gap-2 px-3 py-1.5 rounded-full font-medium text-xs transition-all shadow-sm
${isCapturing
? "bg-slate-100 text-slate-400 cursor-wait border border-slate-200"
: "bg-white text-slate-600 hover:text-slate-900 border border-slate-200 hover:border-slate-300 hover:shadow-md"
}`}
title="Diesen Abschnitt als Bild teilen"
>
<Share2 strokeWidth={2.5} className={`w-3 h-3 ${isCapturing ? 'animate-spin' : ''}`} />
<span>{isCapturing ? "Erstelle Bild..." : "Teilen"}</span>
{isCapturing ? (
<div className="w-3.5 h-3.5 border-2 border-slate-300 border-t-slate-500 rounded-full animate-spin" />
) : (
<Share2 size={14} />
)}
<span>Teilen</span>
</button>
{/* ShareModal expects a direct image string in 'qrCodeData' or 'diagramImage' (except diagramImage specifically assumes SVGs).
Because ShareModal has logic that redraws diagramImage on a canvas assuming it's an SVG string,
we must bypass the SVG renderer. However, if we look at ShareModal, we need a way to pass a raw PNG.
Passing it as qrCodeData is a hack, or we can just send it via diagramImage and hope the canvas ignores it if it's already a Data URL.
Wait: ShareModal expects `diagramImage` (svg string) AND re-renders it.
Let's just pass our Data URL into a NEW prop or hijack the qrCodeData if necessary, but actually ShareModal only allows `diagramImage` as SVG logic right now.
Let's see if ShareModal needs an update to accept pure images, we'll check it. for now, let's pass it via diagramImage and see if we can adapt ShareModal. */}
{/* We will adapt ShareModal to handle both SVG strings & base64 PNG inputs via `diagramImage` */}
<ShareModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
url={currentUrl}
title={title}
description={shareText}
diagramImage={generatedImage}
qrCodeData={generatedImage} // Let's try sending it to both to be safe depending on ShareModal's internals
/>
</>
</div>
);
};

View File

@@ -25,11 +25,11 @@ export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, c
if (image) {
return (
<Reveal direction="up" delay={0.1}>
<div id={shareId} className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
<div className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
<div className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
@@ -37,7 +37,9 @@ export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, c
src={image}
alt={`Meme: ${template} - ${captionList.join(' ')}`}
className="w-full h-auto object-cover block"
loading="lazy"
loading="eager"
decoding="sync"
crossOrigin="anonymous"
/>
</div>
</div>
@@ -47,11 +49,11 @@ export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, c
return (
<Reveal direction="up" delay={0.1}>
<div id={shareId} className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
<div className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
<div className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
</div>
@@ -62,9 +64,7 @@ export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, c
{template === 'clown' && <ClownMeme captions={captionList} />}
{template === 'expanding' && <ExpandingBrainMeme captions={captionList} />}
{template === 'distracted' && <DistractedMeme captions={captionList} />}
{!['drake', 'ds', 'gru', 'fine', 'clown', 'expanding', 'distracted'].includes(template) && (
<GenericMeme captions={captionList} template={template} />
)}
<GenericMeme captions={captionList} template={template} />
</div>
</div>
</Reveal>

View File

@@ -12,6 +12,7 @@ interface ShareModalProps {
url: string;
qrCodeData?: string;
title?: string;
description?: string;
diagramImage?: string;
}
@@ -21,6 +22,7 @@ export function ShareModal({
url,
qrCodeData,
title,
description,
diagramImage,
}: ShareModalProps) {
const [copied, setCopied] = useState(false);
@@ -47,6 +49,9 @@ export function ShareModal({
if (ogImage || ogTitle || ogDesc) {
setOgData({ image: ogImage, title: ogTitle, description: ogDesc });
}
} else {
// Force clear OG data if we HAVE a diagramImage, to prevent fallback flashes
setOgData(null);
}
}, [isOpen, diagramImage, qrCodeData, title]);
@@ -63,13 +68,6 @@ export function ShareModal({
const svgUrl = URL.createObjectURL(svgBlob);
setImagePreview(svgUrl);
}
// Optional: If we want to strictly apply the watermark via Canvas, we would do it here.
// But for the sake of getting the preview to work reliably first, just setting the imagePreview
// directly to the data URL or SVG blob URL is the safest approach. The watermark logic was
// likely failing because `IconBlack` wasn't resolving correctly or `img.onload` wasn't firing
// properly in all environments.
}
}, [diagramImage, isOpen]);
@@ -83,7 +81,8 @@ export function ShareModal({
if (navigator.share) {
try {
const shareData: ShareData = {
title: title || "Mintel Diagramm",
title: title || "Mintel Architektur Insights",
text: description || "Schauen Sie sich diesen Beitrag an.",
url: url,
};
@@ -125,12 +124,13 @@ export function ShareModal({
};
const handleShareX = () => {
const text = encodeURIComponent(title || "Mintel Diagramm");
const text = encodeURIComponent(`${description ? description + " " : ""}${title || "Mintel Architektur Insights"}`);
const shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(url)}`;
window.open(shareUrl, "_blank", "width=550,height=420");
};
const handleShareLinkedIn = () => {
// LinkedIn share doesn't accept text directly via URL for personal profiles easily, but we pass url
const shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`;
window.open(shareUrl, "_blank", "width=550,height=420");
};
@@ -181,7 +181,7 @@ export function ShareModal({
<img
src={imagePreview}
alt={title || "Diagram"}
className="w-full max-h-[50vh] object-contain transition-transform duration-700 rounded-xl"
className="w-full max-h-[30vh] object-contain transition-transform duration-700 rounded-xl"
/>
</div>
@@ -288,11 +288,11 @@ export function ShareModal({
</div>
</div>
<div className="flex flex-col sm:flex-row items-center gap-4 pt-2">
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex items-center gap-2">
<button
onClick={handleShareX}
className="w-12 h-12 bg-black text-white rounded-xl flex items-center justify-center hover:bg-slate-900 transition-all border border-slate-800 active:scale-95"
className="w-12 h-12 bg-black text-white rounded-xl flex items-center justify-center hover:bg-slate-900 transition-all border border-slate-800 active:scale-95 shadow-sm"
title="Auf X teilen"
>
<svg
@@ -306,7 +306,7 @@ export function ShareModal({
<button
onClick={handleShareLinkedIn}
className="w-12 h-12 bg-[#0A66C2] text-white rounded-xl flex items-center justify-center hover:bg-[#004182] transition-all active:scale-95"
className="w-12 h-12 bg-[#0A66C2] text-white rounded-xl flex items-center justify-center hover:bg-[#004182] transition-all active:scale-95 shadow-sm"
title="Auf LinkedIn teilen"
>
<svg
@@ -321,10 +321,10 @@ export function ShareModal({
{typeof navigator !== "undefined" && !!navigator.share && (
<button
onClick={handleNativeShare}
className="w-12 h-12 bg-white border border-slate-200 text-slate-900 rounded-xl flex items-center justify-center hover:bg-slate-50 transition-all active:scale-95"
className="w-12 h-12 bg-white border border-slate-200 text-slate-900 rounded-xl flex items-center justify-center hover:bg-slate-50 transition-all active:scale-95 shadow-sm"
title="System-Dialog öffnen"
>
<Share2 size={20} className="text-slate-400" />
<Share2 size={20} className="text-slate-500" />
</button>
)}
</div>
@@ -332,10 +332,10 @@ export function ShareModal({
{imagePreview && (
<button
onClick={handleDownloadImage}
className="flex-1 w-full p-3 bg-slate-100 text-slate-900 rounded-xl font-bold flex items-center justify-center gap-3 hover:bg-slate-200 transition-all border border-slate-200 active:scale-[0.98]"
className="w-12 h-12 bg-slate-100 text-slate-600 rounded-xl flex items-center justify-center hover:bg-slate-200 transition-all active:scale-95 border border-slate-200 shrink-0"
title="Als PNG herunterladen"
>
<Download size={18} />
<span className="text-xs">Als Bild speichern (PNG)</span>
</button>
)}
</div>

View File

@@ -16,7 +16,6 @@ export function YouTubeEmbed({ videoId, title, className = "" }: YouTubeEmbedPro
className="absolute inset-0 w-full h-full"
src={`https://www.youtube-nocookie.com/embed/${videoId}?rel=0&modestbranding=1`}
title={title || "YouTube video player"}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
loading="lazy"
/>

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useState } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
import { Check, X, Shield, Zap, Expand, Database } from 'lucide-react';
export function ArchitectureBuilder({ className = '' }: { className?: string }) {
const [stack, setStack] = useState<'saas' | 'custom'>('saas');
return (
<Reveal direction="up" delay={0.1}>
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
<div className={`absolute -inset-1 blur opacity-30 rounded-3xl transition-all duration-700 ${stack === 'saas' ? 'bg-gradient-to-r from-orange-100 to-red-100' : 'bg-gradient-to-r from-emerald-100 to-blue-100'}`} />
<div id="sim-arch-builder" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
<ComponentShareButton
targetId="sim-arch-builder"
title="Architektur Vergleich: Miete vs Eigentum"
shareText="Sehen Sie sich diesen Vergleich zwischen Software-Miete und eigenem System an:"
/>
</div>
<div className="p-6 border-b border-slate-200 bg-slate-50">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">🏗</span>
<h3 className="font-bold text-slate-900 m-0">Architektur-Vergleich: Mieten vs. Bauen</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Entdecken Sie die strukturellen Unterschiede zwischen einem "Out-of-the-Box" Baukasten und einer maßgeschneiderten Architektur für Ihr Unternehmen.
</p>
</div>
<div className="flex flex-col md:flex-row">
{/* Control Panel */}
<div className="w-full md:w-1/3 p-6 bg-slate-50 md:border-r border-b md:border-b-0 border-slate-100 space-y-4">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-4">Ihr System-Ansatz</label>
<button
onClick={() => setStack('saas')}
className={`w-full text-left p-4 rounded-xl border-2 transition-all ${stack === 'saas' ? 'border-orange-500 bg-white shadow-sm' : 'border-transparent hover:bg-slate-100'}`}
>
<div className="font-bold text-slate-900 text-sm">🏡 Software Mieten (SaaS)</div>
<div className="text-xs text-slate-500 mt-1">Baukästen, Plugins, fixe Lizenzen</div>
</button>
<button
onClick={() => setStack('custom')}
className={`w-full text-left p-4 rounded-xl border-2 transition-all ${stack === 'custom' ? 'border-emerald-500 bg-white shadow-sm' : 'border-transparent hover:bg-slate-100'}`}
>
<div className="font-bold text-slate-900 text-sm">🏰 System Bauen (Custom)</div>
<div className="text-xs text-slate-500 mt-1">Eigenes IP, Headless, Skalierbar</div>
</button>
</div>
{/* Visualization Panel */}
<div className="w-full md:w-2/3 p-6 md:p-8 bg-white min-h-[400px] flex flex-col justify-center">
{stack === 'saas' ? (
<div className="animate-in fade-in slide-in-from-right-4 duration-500">
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="p-4 rounded-xl border border-orange-200 bg-orange-50/50 relative overflow-hidden">
<div className="text-3xl mb-2">📦</div>
<div className="font-bold text-sm text-orange-900">Standard-Features</div>
<div className="text-xs text-orange-700/70 mt-1 hidden sm:block">Sie zahlen auch für 80%, die Sie nicht nutzen.</div>
</div>
<div className="p-4 rounded-xl border border-red-200 bg-red-50/50 relative overflow-hidden">
<div className="absolute top-2 right-2 flex gap-1">
<div className="w-2 h-2 rounded-full bg-red-400 animate-pulse" />
</div>
<div className="text-3xl mb-2">🔌</div>
<div className="font-bold text-sm text-red-900">Plugin-Chaos</div>
<div className="text-xs text-red-700/70 mt-1 hidden sm:block">Jedes Update ist ein Sicherheitsrisiko.</div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-red-100 text-red-600 flex items-center justify-center shrink-0 border border-red-200"><X size={12} strokeWidth={3} /></div>
<span>Abhängigkeit von Preiserhöhungen</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-red-100 text-red-600 flex items-center justify-center shrink-0 border border-red-200"><X size={12} strokeWidth={3} /></div>
<span>Kein Aufbau von eigenem Firmenwert (IP)</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center shrink-0 border border-orange-200"><Zap size={12} /></div>
<span>Träge Ladezeiten durch geteilte Server</span>
</div>
</div>
</div>
) : (
<div className="animate-in fade-in slide-in-from-right-4 duration-500">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div className="p-4 rounded-xl border border-emerald-200 bg-emerald-50/50 relative">
<div className="text-emerald-500 mb-2"><Zap size={24} /></div>
<div className="font-bold text-sm text-emerald-900">Edge Content</div>
<div className="text-[10px] text-emerald-700/70 mt-1">Instante globale Ladezeiten.</div>
</div>
<div className="p-4 rounded-xl border border-blue-200 bg-blue-50/50 relative">
<div className="text-blue-500 mb-2"><Database size={24} /></div>
<div className="font-bold text-sm text-blue-900">Headless API</div>
<div className="text-[10px] text-blue-700/70 mt-1">Daten systemunabhängig.</div>
</div>
<div className="p-4 rounded-xl border border-slate-200 bg-slate-50 relative">
<div className="text-slate-500 mb-2"><Expand size={24} /></div>
<div className="font-bold text-sm text-slate-900">Custom UI</div>
<div className="text-[10px] text-slate-500 mt-1">100% Passform für Sie.</div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center shrink-0 border border-emerald-200"><Check size={12} strokeWidth={3} /></div>
<span>0 monatliche Fixkosten für Lizenzen</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center shrink-0 border border-emerald-200"><Check size={12} strokeWidth={3} /></div>
<span>Sie besitzen den Code = Echter Firmenwertaufbau</span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-6 h-6 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center shrink-0 border border-blue-200"><Shield size={12} /></div>
<span>Maximale Security (keine Standard-Plugins als Einfallstor)</span>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</Reveal>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import React, { useState } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
import { TrendingUp, Coins, PiggyBank, Briefcase } from 'lucide-react';
export function DigitalAssetVisualizer({ className = '' }: { className?: string }) {
const [view, setView] = useState<'liability' | 'asset'>('asset');
return (
<Reveal direction="up" delay={0.1}>
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
<div className={`absolute -inset-1 blur opacity-30 rounded-3xl transition-all duration-700 ${view === 'liability' ? 'bg-gradient-to-r from-red-100 to-orange-100' : 'bg-gradient-to-r from-indigo-100 to-emerald-100'}`} />
<div id="sim-digital-asset" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
<ComponentShareButton
targetId="sim-digital-asset"
title="Digitales Asset vs. Verbindlichkeit"
shareText="Verstehen Sie den Unterschied zwischen Software-Miete und echtem digitalen Eigentum:"
/>
</div>
<div className="p-6 border-b border-slate-200 bg-slate-50">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">💎</span>
<h3 className="font-bold text-slate-900 m-0">Software: Ausgabe oder Investment?</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Visualisieren Sie den Unterschied zwischen Software, die monatlich Geld verbrennt (SaaS-Miete), und Software, die als Firmenwert bilanziert werden kann (Eigenbau).
</p>
</div>
<div className="flex flex-col md:flex-row">
{/* Control Panel */}
<div className="w-full md:w-1/3 p-6 bg-slate-50 md:border-r border-b md:border-b-0 border-slate-100 space-y-4">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-4">Wirtschaftliche Sicht</label>
<button
onClick={() => setView('liability')}
className={`w-full flex items-center gap-3 text-left p-4 rounded-xl border-2 transition-all ${view === 'liability' ? 'border-red-400 bg-white shadow-sm' : 'border-transparent hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-lg ${view === 'liability' ? 'bg-red-100 text-red-600' : 'bg-slate-200 text-slate-500'}`}>
<Coins size={18} />
</div>
<div>
<div className="font-bold text-slate-900 text-sm">Verbindlichkeit (SaaS)</div>
<div className="text-[10px] text-slate-500 mt-0.5">Laufende Kosten, 0% Eigentum</div>
</div>
</button>
<button
onClick={() => setView('asset')}
className={`w-full flex items-center gap-3 text-left p-4 rounded-xl border-2 transition-all ${view === 'asset' ? 'border-indigo-400 bg-white shadow-sm' : 'border-transparent hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-lg ${view === 'asset' ? 'bg-indigo-100 text-indigo-600' : 'bg-slate-200 text-slate-500'}`}>
<Briefcase size={18} />
</div>
<div>
<div className="font-bold text-slate-900 text-sm">Digitales Asset (Custom)</div>
<div className="text-[10px] text-slate-500 mt-0.5">Firmenwertaufbau, 100% IP</div>
</div>
</button>
</div>
{/* Visualization Panel */}
<div className="w-full md:w-2/3 p-6 md:p-8 bg-white min-h-[350px] flex flex-col justify-center">
{view === 'liability' ? (
<div className="animate-in fade-in slide-in-from-right-4 duration-500">
<div className="flex flex-col items-center justify-center space-y-6 mb-8 mt-4">
{/* Money Burn Animation */}
<div className="relative w-32 h-32 flex items-center justify-center">
{/* Fire background blob */}
<div className="absolute inset-4 bg-orange-400 rounded-full blur-xl opacity-20 animate-pulse" />
{/* Money flying away */}
<div className="absolute top-0 right-0 animate-bounce delay-75 text-red-500 opacity-50">-</div>
<div className="absolute top-4 left-4 animate-bounce delay-150 text-red-500 opacity-40">-</div>
<div className="absolute bottom-8 right-8 animate-bounce delay-300 text-red-500 opacity-60">-</div>
<div className="w-20 h-20 bg-red-50 border border-red-200 text-red-500 rounded-full flex items-center justify-center text-3xl shadow-sm z-10">
💸
</div>
</div>
<div className="text-center">
<div className="text-sm font-bold text-red-900">Der ewige Geldabfluss</div>
<p className="text-xs text-red-700 mt-2 max-w-[250px] mx-auto opacity-80">
Jeden Monat fließt Kapital an externe Software-Anbieter ab. Kündigen Sie, ist Ihr System sofort offline.
</p>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 grid grid-cols-2 gap-4">
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Unternehmenswert</div>
<div className="font-bold text-slate-900">0,00 </div>
<div className="text-[10px] text-slate-500 mt-0.5">Software gehört Anbieter</div>
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Skalierungs-Kosten</div>
<div className="font-bold text-red-600 flex items-center gap-1">Linear <TrendingUp size={12} /></div>
<div className="text-[10px] text-slate-500 mt-0.5">Mehr User = Mehr Kosten</div>
</div>
</div>
</div>
) : (
<div className="animate-in fade-in slide-in-from-right-4 duration-500">
<div className="flex flex-col items-center justify-center space-y-6 mb-8 mt-4">
{/* Asset Building Animation */}
<div className="relative w-32 h-32 flex items-center justify-center">
{/* Glow background blob */}
<div className="absolute inset-4 bg-indigo-400 rounded-full blur-xl opacity-20" />
{/* Value accumulating */}
<div className="absolute -top-2 left-1/2 -ml-3 animate-pulse text-indigo-500 text-xl font-bold">+</div>
<div className="w-20 h-20 bg-indigo-50 border border-indigo-200 text-indigo-600 rounded-2xl flex items-center justify-center shadow-sm z-10 rotate-3 transition-transform hover:rotate-6 hover:scale-105">
<PiggyBank size={32} />
</div>
</div>
<div className="text-center">
<div className="text-sm font-bold text-indigo-900">Das aktivierbare Asset</div>
<p className="text-xs text-indigo-700 mt-2 max-w-[280px] mx-auto opacity-80">
Maßgeschneiderte Software gehört zu 100% Ihnen. Das Intellectual Property (IP) mehrt den Wert Ihrer GmbH.
</p>
</div>
</div>
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 grid grid-cols-2 gap-4">
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Unternehmenswert</div>
<div className="font-bold text-emerald-600 flex items-center gap-1">Steigt aktiv <TrendingUp size={12} /></div>
<div className="text-[10px] text-slate-500 mt-0.5">Kann bilanziert werden</div>
</div>
<div>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Skalierungs-Kosten</div>
<div className="font-bold text-slate-900">Flach (0 Lizenzen)</div>
<div className="text-[10px] text-slate-500 mt-0.5">Nur reine Serverkosten</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</Reveal>
);
}

View File

@@ -0,0 +1,201 @@
"use client";
import React, { useState, useEffect } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
import { Play, RotateCcw } from 'lucide-react';
export function LoadTimeSimulator({ className = '' }: { className?: string }) {
const [isRunning, setIsRunning] = useState(false);
const [timeElapsed, setTimeElapsed] = useState(0);
const [legacyState, setLegacyState] = useState(0);
const [mintelState, setMintelState] = useState(0);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isRunning && timeElapsed < 7500) {
interval = setInterval(() => {
setTimeElapsed(prev => {
const next = prev + 50;
// Legacy Loading Logic (stuttering, slow, layout shifts)
if (next >= 1200 && next < 3000) setLegacyState(1); // Header only
else if (next >= 3000 && next < 5500) setLegacyState(2); // Text appears
else if (next >= 5500 && next < 7000) setLegacyState(3); // Image loads (CLS)
else if (next >= 7000) setLegacyState(4); // Finally interactive
// Mintel Loading Logic (Instant Edge Delivery)
if (next >= 350) setMintelState(1); // Fully loaded instantly
return next;
});
}, 50);
} else if (timeElapsed >= 7500) {
setIsRunning(false);
}
return () => clearInterval(interval);
}, [isRunning, timeElapsed]);
const startRace = () => {
setTimeElapsed(0);
setLegacyState(0);
setMintelState(0);
setIsRunning(true);
};
return (
<Reveal direction="up" delay={0.1}>
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
<div className="absolute -inset-1 bg-gradient-to-r from-red-100 to-emerald-100 rounded-3xl blur opacity-30" />
<div id="sim-load-time" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
<ComponentShareButton targetId="sim-load-time" title="Ladezeit Simulator" />
</div>
<div className="p-6 border-b border-slate-200 bg-slate-50 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xl">🏎</span>
<h3 className="font-bold text-slate-900 m-0">Architektur-Rennen: Legacy CMS vs. Mintel Stack</h3>
</div>
<p className="text-xs text-slate-500 m-0 leading-relaxed">
Simulieren Sie den Unterschied zwischen dynamischem Server-Rendering (PHP/MySQL) und statischer Edge-Auslieferung (<span className="font-mono bg-slate-200 px-1 rounded text-[10px]">TTV &lt; 500ms</span>).
</p>
</div>
<button
onClick={startRace}
className="shrink-0 flex items-center gap-2 px-6 py-2.5 bg-slate-900 !text-white rounded-full font-bold text-sm hover:hover:bg-black hover:scale-105 active:scale-95 transition-all shadow-md"
>
{timeElapsed > 0 ? <RotateCcw size={16} /> : <Play size={16} />}
{timeElapsed > 0 ? "Neustart" : "Rennen Starten"}
</button>
</div>
<div className="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-100 bg-slate-50/50">
{/* LEGACY LANE */}
<div className="p-6 flex flex-col items-center">
<div className="w-full flex justify-between items-center mb-6">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Legacy Monolith</span>
<span className={`font-mono font-bold ${legacyState === 4 ? 'text-red-500' : 'text-slate-400'}`}>
{legacyState === 4 ? "7.00s" : (timeElapsed / 1000).toFixed(2) + "s"}
</span>
</div>
{/* Browser Mockup */}
<div className="w-full max-w-[280px] aspect-[3/4] bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden flex flex-col">
<div className="h-6 bg-slate-100 border-b border-slate-200 flex items-center px-2 gap-1.5 shrink-0">
<div className="w-2 h-2 rounded-full bg-slate-300" />
<div className="w-2 h-2 rounded-full bg-slate-300" />
<div className="w-2 h-2 rounded-full bg-slate-300" />
</div>
<div className="p-4 flex-1 flex flex-col relative">
{/* Spinner */}
{legacyState === 0 && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-slate-300 gap-2">
<div className="w-6 h-6 border-2 border-slate-200 border-t-slate-400 rounded-full animate-spin" />
<span className="text-[10px] font-mono tracking-widest uppercase">TTFB Waiting...</span>
</div>
)}
{/* Content that shuffles around to simulate CLS */}
<div className={`transition-opacity duration-300 ${legacyState >= 1 ? 'opacity-100' : 'opacity-0'}`}>
<div className="h-4 w-3/4 bg-slate-200 rounded mb-4" /> {/* Header */}
</div>
{/*
Simulate Layout Shift:
Text loads first (state 2), then at state 3 an image drops in above it, pushing the text down.
*/}
<div className="flex flex-col gap-3 transition-opacity duration-300 mt-2">
{legacyState >= 3 && (
<div className="h-24 w-full bg-slate-200 relative overflow-hidden rounded animate-pulse">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl opacity-50">🖼</span>
</div>
</div>
)}
<div className={`space-y-2 transition-transform duration-300 ${legacyState >= 2 ? 'opacity-100' : 'opacity-0'} ${legacyState === 3 ? 'translate-y-2' : ''}`}>
<div className="h-2 w-full bg-slate-100 rounded" />
<div className="h-2 w-5/6 bg-slate-100 rounded" />
<div className="h-2 w-4/6 bg-slate-100 rounded" />
</div>
</div>
{legacyState === 4 && (
<div className="absolute bottom-4 left-4 right-4 animate-in slide-in-from-bottom-2 fade-in">
<div className="h-8 bg-slate-900 rounded flex items-center justify-center">
<span className="text-[10px] font-bold text-white uppercase">Interaktiv (Hydrated)</span>
</div>
</div>
)}
</div>
</div>
<div className="h-6 mt-4 flex items-center">
{legacyState === 0 && timeElapsed > 0 && <span className="text-[10px] text-slate-400 uppercase tracking-widest animate-pulse">Waiting for Server...</span>}
{legacyState === 1 && <span className="text-[10px] text-amber-500 uppercase tracking-widest">Parsing HTML...</span>}
{legacyState === 2 && <span className="text-[10px] text-amber-500 uppercase tracking-widest">Downloading Assets...</span>}
{legacyState === 3 && <span className="text-[10px] text-red-500 font-bold uppercase tracking-widest">Layout Shift Detected!</span>}
{legacyState === 4 && <span className="text-[10px] text-red-500 font-bold uppercase tracking-widest">Finished in 7.0s</span>}
</div>
</div>
{/* MINTEL LANE */}
<div className="p-6 flex flex-col items-center border-t md:border-t-0 border-slate-100">
<div className="w-full flex justify-between items-center mb-6">
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest">Mintel Stack (Edge)</span>
<span className={`font-mono font-bold ${mintelState === 1 ? 'text-emerald-500' : 'text-slate-400'}`}>
{mintelState === 1 ? "0.35s" : (timeElapsed / 1000).toFixed(2) + "s"}
</span>
</div>
{/* Browser Mockup */}
<div className="w-full max-w-[280px] aspect-[3/4] bg-white border-2 border-emerald-100 rounded-lg shadow-sm overflow-hidden flex flex-col relative ring-4 ring-emerald-50 ring-opacity-50 transition-all duration-500">
<div className="h-6 bg-emerald-50 border-b border-emerald-100 flex items-center px-2 gap-1.5 shrink-0">
<div className="w-2 h-2 rounded-full bg-emerald-200" />
<div className="w-2 h-2 rounded-full bg-emerald-200" />
<div className="w-2 h-2 rounded-full bg-emerald-200" />
</div>
<div className="p-4 flex-1 flex flex-col relative">
{mintelState === 0 && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-emerald-300 gap-2">
<span className="text-[10px] font-mono tracking-widest uppercase">CDN Hit...</span>
</div>
)}
{/* Instant Load: Everything drops in perfectly immediately */}
<div className={`transition-opacity duration-300 h-full flex flex-col ${mintelState === 1 ? 'opacity-100' : 'opacity-0'}`}>
<div className="h-4 w-3/4 bg-emerald-100 rounded mb-4 shrink-0" />
<div className="h-24 w-full bg-emerald-50 relative overflow-hidden rounded mb-3 shrink-0 flex items-center justify-center border border-emerald-100">
<span className="text-2xl opacity-80">🚀</span>
</div>
<div className="space-y-2 shrink-0">
<div className="h-2 w-full bg-slate-100 rounded" />
<div className="h-2 w-5/6 bg-slate-100 rounded" />
<div className="h-2 w-4/6 bg-slate-100 rounded" />
</div>
<div className="mt-auto pt-4 shrink-0">
<div className="h-8 bg-emerald-500 rounded flex items-center justify-center shadow-lg shadow-emerald-500/20">
<span className="text-[10px] font-bold text-white uppercase">Instant Interactive</span>
</div>
</div>
</div>
</div>
</div>
<div className="h-6 mt-4 flex items-center">
{mintelState === 0 && timeElapsed > 0 && <span className="text-[10px] text-emerald-400 uppercase tracking-widest">Routing to Edge...</span>}
{mintelState === 1 && <span className="text-[10px] text-emerald-500 font-bold uppercase tracking-widest">Loaded & Ready in 350ms</span>}
</div>
</div>
</div>
</div>
</div>
</Reveal>
);
}

View File

@@ -0,0 +1,124 @@
"use client";
import React, { useState } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
export function PerformanceROICalculator({ className = '' }: { className?: string }) {
const [traffic, setTraffic] = useState(100); // 1000 visitors/month (approx 30/day)
const [aov, setAov] = useState(100); // e.g. average value of a craftsman quote or small B2B service
const [loadTime, setLoadTime] = useState(2.0);
const [baseConv, setBaseConv] = useState(2.0);
// B2B Deloitte metric: 0.1s improvement = 8.4% conversion relative lift
// We cap the improvement to realistic bounds (e.g. optimizing to 1.5s)
const targetLoadTime = 1.5;
const secondsSaved = Math.max(0, loadTime - targetLoadTime);
// Relative lift calculation
const relativeLiftPercentage = secondsSaved * 10 * 8.4;
const newConv = baseConv * (1 + (relativeLiftPercentage / 100));
const currentRevenue = traffic * (baseConv / 100) * aov;
const newRevenue = traffic * (newConv / 100) * aov;
const additionalAnnualRevenue = (newRevenue - currentRevenue) * 12;
const formatCurrency = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(val);
return (
<Reveal direction="up" delay={0.1}>
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-3xl blur opacity-25" />
<div id="sim-performance-roi" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
<ComponentShareButton targetId="sim-performance-roi" title="Performance ROI Simulator" />
</div>
<div className="p-6 md:p-8 bg-slate-50 border-b border-slate-200">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl"></span>
<h3 className="font-bold text-slate-900 m-0">Performance ROI für den Mittelstand</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed">
Simulieren Sie, wie sich die Ladezeit auf die Jahresumsätze Ihres Betriebs (Handwerk, Kanzlei, B2B-Service) auswirkt. Basierend auf der Branchen-Erkenntnis: Schon 0.1s schnellere Ladezeit hebt die Kontakt-Wandlungsrate um bis zu 8,4%.
</p>
</div>
<div className="grid md:grid-cols-5 divide-y md:divide-y-0 md:divide-x divide-slate-100">
{/* Inputs */}
<div className="md:col-span-2 p-6 md:p-8 space-y-8 bg-white">
<div className="space-y-3">
<div className="flex justify-between items-end">
<label className="text-xs font-bold text-slate-400 uppercase tracking-widest">Ø Ladezeit (Sekunden)</label>
<span className="font-mono font-bold text-red-500">{loadTime.toFixed(1)}s</span>
</div>
<input
type="range" min="1.5" max="10" step="0.1"
value={loadTime} onChange={(e) => setLoadTime(Number(e.target.value))}
className="w-full accent-slate-900"
/>
</div>
<div className="space-y-3">
<div className="flex justify-between items-end">
<label className="text-xs font-bold text-slate-400 uppercase tracking-widest">Monatliche Besucher</label>
<span className="font-mono font-bold text-slate-700">{traffic.toLocaleString('de-DE')}</span>
</div>
<input
type="range" min="100" max="25000" step="100"
value={traffic} onChange={(e) => setTraffic(Number(e.target.value))}
className="w-full accent-slate-900"
/>
</div>
<div className="space-y-3">
<div className="flex justify-between items-end">
<label className="text-xs font-bold text-slate-400 uppercase tracking-widest">Ø Projektwert / Marge</label>
<span className="font-mono font-bold text-slate-700">{formatCurrency(aov)}</span>
</div>
<input
type="range" min="100" max="15000" step="100"
value={aov} onChange={(e) => setAov(Number(e.target.value))}
className="w-full accent-slate-900"
/>
</div>
</div>
{/* Outputs */}
<div className="md:col-span-3 p-6 md:p-8 bg-slate-50 flex flex-col justify-center">
<div className="grid grid-cols-2 gap-6 mb-8">
<div className="space-y-1">
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Status Quo Conv.</div>
<div className="text-2xl font-mono text-slate-400">{baseConv.toFixed(2)}%</div>
<div className="text-xs text-slate-400 font-mono mt-2">{formatCurrency(currentRevenue * 12)} / Jahr</div>
</div>
<div className="space-y-1 border-l pl-6 border-slate-200">
<div className="text-[10px] font-bold text-green-500 uppercase tracking-widest">Ziel Konversion (1.5s)</div>
<div className="text-2xl font-mono font-bold text-slate-900 flex items-center gap-2">
{newConv.toFixed(2)}%
{relativeLiftPercentage > 0 && <span className="text-xs font-bold text-green-500 bg-green-50 px-2 py-0.5 rounded-full">+{relativeLiftPercentage.toFixed(1)}%</span>}
</div>
<div className="text-xs text-slate-600 font-mono mt-2">{formatCurrency(newRevenue * 12)} / Jahr</div>
</div>
</div>
<div className="p-6 bg-slate-900 rounded-xl relative overflow-hidden">
<div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: "radial-gradient(#ffffff 1px, transparent 0)", backgroundSize: "15px 15px" }} />
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="text-[10px] font-bold text-emerald-400 uppercase tracking-widest mb-1">Potenzieller Mehrumsatz / Jahr</div>
<div className="text-xs text-slate-400">Durch {loadTime.toFixed(1)}s Ladezeit vs. Industrie-Standard (1.5s) entgangen.</div>
</div>
<div className="text-3xl md:text-4xl font-mono font-black text-white">
{additionalAnnualRevenue > 0 ? '+' : ''}{formatCurrency(additionalAnnualRevenue)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Reveal>
);
}