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

@@ -3,6 +3,8 @@ import { allPosts } from "contentlayer/generated";
import { blogThumbnails } from "../../../src/components/blog/blogThumbnails";
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
import * as fs from "node:fs/promises";
import * as path from "node:path";
export const size = OG_IMAGE_SIZE;
export const contentType = "image/png";
@@ -15,6 +17,24 @@ export default async function Image({
}) {
const { slug } = await params;
const post = allPosts.find((p) => p.slug === slug);
// If we have a custom generated thumbnail, serve it directly as the OG image
if (post?.thumbnail) {
try {
const filePath = path.join(process.cwd(), "public", post.thumbnail);
const fileBuffer = await fs.readFile(filePath);
return new Response(fileBuffer, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch (err) {
console.warn(`[OG Image Generator] Could not read thumbnail file for ${slug}:`, err);
// Fall through to dynamic generation
}
}
const thumbnail = blogThumbnails[slug];
const title = post?.title || "Marc Mintel";

View File

@@ -7,27 +7,29 @@ tags: ["privacy", "analytics"]
---
<LeadParagraph>
"Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." Diese weit verbreitete Fehlannahme kostet B2B-Unternehmen täglich Vertrauen und Datenqualität.
"Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." Diese weit verbreitete Fehlannahme kostet B2B-Unternehmen täglich Vertrauen und massive Datenqualität.
</LeadParagraph>
<LeadParagraph>
In meiner täglichen Arbeit als Digital Architect beweise ich das Gegenteil: <Marker>Maximale geschäftliche Erkenntnis erfordert keine maximale private Überwachung.</Marker>
</LeadParagraph>
<LeadParagraph>
Ich zeige Ihnen, wie wir Erfolg präzise messen, die DSGVO-Konformität systemisch verankern und dabei die Privatsphäre Ihrer Kunden schützen.
Ich zeige Ihnen, wie wir Erfolg präzise messen, die DSGVO-Konformität systemisch verankern und dabei die Privatsphäre Ihrer Kunden schützen, ohne auf valide Daten zu verzichten.
</LeadParagraph>
<TableOfContents />
**TL;DR:** Klassisches Tracking via Google Analytics verliert durch Consent-Ablehnung bis zu 60% der Daten. Privacy-First Analytics arbeitet serverseitig, kommt ohne Banner aus und liefert 100% genauere Trends bei maximalem Datenschutz und besserer PageSpeed.
**TL;DR:** Klassisches Tracking via Google Analytics verliert durch Consent-Ablehnung bis zu 70% der Daten. Privacy-First Analytics arbeitet serverseitig, kommt ohne Banner aus und liefert 100% genauere Trends bei maximalem Datenschutz und besserem PageSpeed.
<H2>Analytics ohne den Beigeschmack der Überwachung</H2>
<Paragraph>
Klassische Analytics-Tools funktionieren oft wie ein Trojaner. Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu bilden, die weit über Ihre Website hinausgehen. Dies zwingt Sie rechtlich in die Knie: Sie benötigen komplexe Consent-Tools und riskieren dennoch Abmahnungen. Laut einer Untersuchung des <ExternalLink href="https://httparchive.org/">HTTP Archive</ExternalLink> macht die mediane Webseite zahlreiche Anfragen an Drittanbieter-Tracking-Domains, was die Ladezeit massiv belastet, ohne immer direkten Business-Value zu generieren.
Klassische Analytics-Tools funktionieren oft wie ein Trojaner. Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu bilden, die weit über Ihre Website hinausgehen. Dies zwingt Sie rechtlich in die Knie: Sie benötigen komplexe Consent-Management-Plattformen (CMPs) und riskieren dennoch Abmahnungen. Laut einer Untersuchung des <ExternalLink href="https://httparchive.org/">HTTP Archive</ExternalLink> machen Third-Party-Requests einen massiven Teil des Datenaufkommens aus, was die Ladezeit unnötig belastet.
</Paragraph>
<Paragraph>
Das Paradoxon: <Marker>Die meisten dieser Daten benötigen Sie für Ihre operativen Entscheidungen gar nicht.</Marker> Sie wollen wissen, welche Inhalte funktionieren, welche Kampagnen konvertieren und wo Nutzer abspringen nicht, welche anderen Websites Ihre Besucher in ihrer Freizeit besuchen. Viele Organisationen analysieren laut <ExternalLink href="https://www2.deloitte.com/">Deloitte</ExternalLink> ohnehin weniger als 5% der gesammelten Daten.
Das Paradoxon: <Marker>Die meisten dieser Daten benötigen Sie für Ihre operativen Entscheidungen gar nicht.</Marker> Sie wollen wissen, welche Inhalte funktionieren, welche Kampagnen konvertieren und wo Nutzer abspringen nicht, welche anderen Websites Ihre Besucher in ihrer Freizeit besuchen. Studien von <ExternalLink href="https://developers.google.com/">Google Developers</ExternalLink> zeigen zudem, dass Third-Party-JavaScript oft zum Performance-Flaschenhals wird.
</Paragraph>
<div className="my-12">
@@ -43,38 +45,33 @@ graph LR
</Mermaid>
</div>
<H2>Qualität der Daten vor Quantität der Profile</H2>
<H2>Das Problem der Datenlücke durch Consent-Zwang</H2>
<Paragraph>
Mein Ansatz basiert auf aggregierten Trends statt auf individueller Verfolgung. Wir messen Seitenaufrufe, Verweildauer und Conversions anonym und oft direkt am [Edge-Server](/blog/professional-hosting-operations). Eine Studie von <ExternalLink href="https://www.mckinsey.com/">McKinsey</ExternalLink> zeigt, dass Unternehmen, die Daten konsequent ins Zentrum ihrer Entscheidungen stellen, ihren Marketing-ROI um 15-20% steigern können. Dies gelingt jedoch nur mit validen Daten nicht mit lückenhaften Statistiken aus abgelehnten Cookie-Bannern.
Die Realität in Europa ist ernüchternd für Marketer, die auf klassische Tools setzen. Eine Studie von <ExternalLink href="https://www2.deloitte.com/">Deloitte</ExternalLink> zeigt bei europäischen Websites Datenverluste zwischen 30% und über 70% allein durch DSGVO-Consent-Anforderungen. Wenn der Nutzer "Ablehnen" klickt, bleibt das Unternehmen blind.
</Paragraph>
<BoldNumber
value="15-20%"
label="Steigerung des Marketing-ROI durch datengestützte Entscheidungen"
source="McKinsey"
sourceUrl="https://www.mckinsey.com/capabilities/growth-marketing-and-sales/our-insights/the-creative-dilemma-how-to-build-a-data-driven-creative-engine"
/>
<StatsGrid stats="70%|Datenverlust|bei strengem Consent~20%|Akzeptanzrate|auf manchen EU-Portalen~3x|Höhere Conversion|bei <1s Ladezeit" />
<Paragraph>
Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von der Komplexität eines GA4 abhängig zu machen. Wer [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners) betreibt, signalisiert Professionalität und Respekt gegenüber dem Nutzer.
Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von der Komplexität eines GA4-Setups abhängig zu machen. Wer [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners) betreibt, signalisiert Professionalität und Respekt gegenüber dem Nutzer. Zudem heben Studien der <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> hervor, dass langsame Ladezeiten oft verursacht durch drittanbieter-lastige Tracking-Skripte direkt zu Frustration und Abbruch führen.
</Paragraph>
<H2>Die Architektur für ethische Insights</H2>
<Paragraph>
Ich integriere Analytics direkt in Ihre Plattform-Architektur. Das bedeutet: Keine externen Scripte, was die Performance massiv verbessert. <ExternalLink href="https://developers.google.com/">Google Developers</ExternalLink> betont regelmäßig, dass schlecht konfigurierte Tracking-Skripte die Ladezeiten signifikant bremsen.
Ich integriere Analytics direkt in Ihre Plattform-Architektur. Das bedeutet: Keine externen Scripte von US-Servern, was die Performance massiv verbessert. Ein sauberer [systemischer Architektur-Ansatz für die DSGVO](/blog/gdpr-conformity-system-approach) ist hier der Schlüssel.
</Paragraph>
<IconList>
<IconListItem check>
<strong>Cookieless Tracking:</strong> Wir erkennen wiederkehrende Nutzer über kurzlebige, anonyme Hashes. Keine Speicherung am Endgerät (LocalStorage/Cookies) notwendig.
<strong>Cookieless Tracking:</strong> Wir erkennen wiederkehrende Nutzer über kurzlebige, anonyme Hashes innerhalb von 24h. Keine Speicherung am Endgerät notwendig.
</IconListItem>
<IconListItem check>
<strong>First-Party Data:</strong> Alle Daten bleiben in Ihrem Hoheitsbereich. Kein Abfluss an Werbenetzwerke zur Profilbildung.
<strong>First-Party Data:</strong> Alle Daten bleiben in Ihrem Hoheitsbereich (z.B. auf einem [professionellen Hosting](/blog/professional-hosting-operations)). Kein Datenabfluss an Werbenetzwerke.
</IconListItem>
<IconListItem check>
<strong>Lightweight Implementation:</strong> Statt 100KB Analytics-Ballast nutzen wir Lösungen unter 1KB. <Marker>Geschwindigkeit trifft auf Erkenntnis.</Marker>
<strong>Lightweight Implementation:</strong> Statt 100KB Tracking-Ballast nutzen wir Lösungen unter 1KB. <Marker>Geschwindigkeit trifft auf Erkenntnis.</Marker>
</IconListItem>
</IconList>
@@ -82,57 +79,68 @@ graph LR
<YouTubeEmbed videoId="chk4m7mqkdI" title="Privacy as an Opportunity in Analytics" />
</div>
<Paragraph>
Laut <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> führen intrusive oder unklare Cookie-Banner zu deutlich niedrigeren Akzeptanzraten. Ein Verzicht auf diese Barrieren verbessert also nicht nur die UX, sondern ironischerweise auch die Datenbasis für Ihre Business-Entscheidungen.
</Paragraph>
<H2>Der strategische Haken: Transparenz schafft Vertrauen</H2>
<Paragraph>
Als Digital Architect ist es meine Pflicht, auch die Grenzen moderner Cookieless-Systeme aufzuzeigen. Während wir die Privatsphäre schützen, müssen wir technische Kompromisse bei der Langzeit-Erkennung eingehen.
Als Digital Architect ist es meine Pflicht, auch die Grenzen moderner Cookieless-Systeme aufzuzeigen. Während wir die Privatsphäre schützen, müssen wir bei der Langzeit-Kohorten-Analyse Kompromisse eingehen.
</Paragraph>
<ComparisonRow
description="Technischer Vergleich: Cookie vs. Cookieless"
negativeLabel="Legacy Cookie-Tracking"
negativeText="Präzise Wiedererkennung über Monate, aber rechtlich riskant und durch Ad-Blocker (steigende Raten laut HTTP Archive) unterdrückt."
positiveLabel="Mintel Cookieless"
positiveText="Datenschutz-konform ohne Banner, aber geringere Match-Rate (ca. 10-20% niedriger laut Deloitte) bei wiederkehrenden Usern nach 24h."
description="Technischer Vergleich: Legacy vs. Modern"
negativeLabel="Legacy Tracking (GA4)"
negativeText="Präzise Wiedererkennung über Monate, aber rechtlich riskant, Banner-pflichtig und 40-70% Datenverlust durch Opt-Out."
positiveLabel="Mintel Privacy Stack"
positiveText="100% DSGVO-konform ohne Banner. Erfasst jeden Besucher, verliert aber die Verknüpfung bei Usern nach 24h ohne Cookies."
showShare={true}
/>
<Paragraph>
Studien von <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> zeigen, dass Usability-Probleme oft durch mangelnden Fokus auf Nutzerverhalten entstehen. Privacy-Analytics löst dies, indem es den Fokus auf *was* getan wird lenkt, statt auf das *wer* es tut. Dennoch: IP-basiertes Tracking ohne Cookies erreicht laut Google nur eine Genauigkeit von ca. 60-70%, da dynamische IPs und VPNs die Zuordnung erschweren.
Google versucht mit dem "Consent Mode" zwar Datenlücken durch Modellierung zu schließen (<ExternalLink href="https://developers.google.com/">Google Developers</ExternalLink>), doch eine vollständige Wiederherstellung der Daten ist technisch nicht möglich und bleibt rechtlich oft eine Grauzone. Mein Ansatz verfolgt das Prinzip der [Digital Longevity](/blog/digital-longevity-architecture): Ein System, das stabil und ohne rechtliche Zitterpartie funktioniert.
</Paragraph>
<H2>Der unternehmerische Hebel: Banner-freie Leads</H2>
<H2>Der unternehmerische Hebel: Performance ist Umsatz</H2>
<Paragraph>
Wenn Sie auf Ihrer Website kein Banner benötigen, messen Sie 100% Ihres Traffics. Bei klassischen Lösungen verlieren Sie oft 40-60% der Daten, weil Nutzer den Consent ablehnen. Mein System liefert Ihnen <Marker>die echten Zahlen</Marker>, da die technische Hürde der Zustimmung entfällt.
Daten von <ExternalLink href="https://www2.deloitte.com/">Deloitte</ExternalLink> belegen, dass die Verbesserung der Ladezeit die Conversion-Rates signifikant steigert. Jedes gesparte Tracking-Skript zahlt direkt auf diesen ROI ein. Ein stabiles System ist kein Kostenfaktor, sondern ein [ROI-Beschleuniger](/blog/clean-code-for-business-value).
</Paragraph>
<BoldNumber
value="8.4%"
label="Conversion-Steigerung pro 0.1s schnellere Ladezeit"
source="Deloitte Digital"
sourceUrl="https://www2.deloitte.com/ie/en/pages/technology-media-and-telecommunications/articles/speed-up-your-business.html"
/>
<div className="my-8">
<ArticleMeme
template="drake"
captions="Cookie-Banner nerven & 60% Daten verlieren|Privacy-Analytics & 100% DSGVO-konforme Daten"
captions="Cookie-Banner & 60% Datenverlust akzeptieren|Privacy-Analytics & 100% ehrliche Insights nutzen"
/>
</div>
<Paragraph>
Ehrlichkeit zahlt sich hier direkt in der Genauigkeit Ihrer Marketing-Planung aus. Ein stabiles System ist kein Kostenfaktor, sondern ein [ROI-Beschleuniger](/blog/clean-code-for-business-value).
Wenn Sie auf Ihrer Website kein Banner benötigen, messen Sie 100% Ihres Traffics aggregiert. Es ist die ehrlichere Metrik für Ihre Marketing-Planung. Erfahren Sie hier mehr über die [versteckten Kosten von zu vielen Plugins](/blog/hidden-costs-of-wordpress-plugins), die oft auch Tracking-Balast mitbringen.
</Paragraph>
<Button href="/contact" variant="outline" size="large">Ethisches Tracking anfragen</Button>
<Button href="/contact" variant="outline" size="large">Performance-Check anfragen</Button>
<H2>Fazit: Wissen ist Macht, Respekt ist Zukunft</H2>
<Paragraph>
Messen Sie, was zählt und schützen Sie, wer zählt. Ein moderner [systemischer Architektur-Ansatz für die DSGVO](/blog/gdpr-conformity-system-approach) macht Datenschutz zu Ihrem Wettbewerbsvorteil. Ich begleite Sie bei der Installation einer Lösung, die Professionalität und Ethik brillant vereint. Insights ohne Reue das ist modernes digitales Management.
Messen Sie, was zählt und schützen Sie, wer zählt. Ein moderner Ansatz macht Datenschutz zu Ihrem Wettbewerbsvorteil statt zum Compliance-Albtraum. Die Kombination aus maximaler Performance, Banner-Freiheit und präzisen Business-Insights ist heute kein "Entweder-oder" mehr. Wer seine Architektur heute auf Privacy-First umstellt, sichert sich die Datenhoheit für das nächste Jahrzehnt.
</Paragraph>
<FAQSection>
<H3>Wofür brauche ich Analytics ohne Cookies?</H3>
<Paragraph>Es ermöglicht Ihnen, 100% Ihrer Besucherströme DSGVO-konform zu erfassen, ohne Nutzer mit nervigen Cookie-Bannern abzuschrecken. Gleichzeitig verbessern Sie Ihre Ladezeiten massiv, da schwerfällige externe Skripte entfallen.</Paragraph>
<H3>Verliere ich durch Cookieless Analytics wichtige Funktionen?</H3>
<Paragraph>Sie verlieren die Möglichkeit, Einzelnutzer über Monate hinweg zu verfolgen. Für die meisten B2B-Unternehmen ist jedoch der Gewinn von 100% sauberen Trend-Daten und einer bannerfreien User Experience weitaus wertvoller.</Paragraph>
<H3>Ist Cookieless Tracking wirklich genau genug?</H3>
<Paragraph>Für Business-Insights und Trend-Analysen ist es exzellent geeignet. Obwohl die Wiedererkennung über sehr lange Zeiträume (Wochen/Monate) schwieriger ist als mit Cookies, überwiegt der Vorteil der lückenlosen Datenerfassung ohne Consent-Verluste.</Paragraph>
<H3>Ist Analytics ohne Banner wirklich 100% rechtssicher?</H3>
<Paragraph>Ja, sofern keine personenbezogenen Daten (PII) gespeichert werden und die Anonymisierung bereits auf dem Server (First-Party) stattfindet. Dies eliminiert die Notwendigkeit für eine Einwilligung nach TTDSG/DSGVO.</Paragraph>
<H3>Erhöht der Verzicht auf Tracker die SEO-Performance?</H3>
<Paragraph>Ja, indirekt sehr stark. Durch den Verzicht auf schwere Tracking-Bibliotheken sinkt die Zeit bis zur Interaktion (INP) und der Lade-Speed (LCP) verbessert sich, was positive Signale an Google sendet.</Paragraph>
<H3>Wie wirkt sich der Verzicht auf Tracker auf SEO aus?</H3>
<Paragraph>Extrem positiv. Durch verbesserte Core Web Vitals (da weniger JavaScript die Main-Thread-Arbeit blockiert) wertet Google die Seite höher ein, was zu besseren Rankings führen kann.</Paragraph>
</FAQSection>

View File

@@ -43,24 +43,7 @@ tags: ["architecture", "business"]
durch maßgeschneiderte Systeme.
</Paragraph>
<div className="my-12">
<Mermaid id="build-vs-buy-decision" title="Build vs. Buy Entscheidung" showShare={true}>
graph TD
Need["Individuelles Business-Bedürfnis"] --> Path["Strategische Entscheidung"]
Path --> Buy["Software-Abo (SaaS)"]
Path --> Build["Bespoke Architecture (Mintel)"]
Buy --> Compromise["Kompromisse & Monatliche Fixkosten"]
Build --> Competitive["Wettbewerbsvorteil & Unendliche Freiheit"]
Compromise --> Stagnation["Digitaler Stillstand"]
Competitive --> Growth["Skalierung ohne Grenzen"]
style Build fill:#4ade80,stroke:#333
style Growth fill:#4ade80,stroke:#333
</Mermaid>
<div className="text-center text-xs text-slate-400 mt-4 italic">
Build vs. Buy: Investieren Sie in Ihr eigenes geistiges Eigentum statt
in die monatliche Miete von Fremdprodukten.
</div>
</div>
<ArchitectureBuilder />
<H3>Bauen bedeutet heute: Strategisches Kombinieren</H3>
<Paragraph>

View File

@@ -44,23 +44,7 @@ tags: ["architecture", "longevity"]
Abrissbirne.
</Paragraph>
<div className="my-12">
<Mermaid id="technology-longevity" title="Technologie Langlebigkeit" showShare={true}>
graph TD
Logic["Zukunftsfähige Kern-Logik"] --> Standards["Offene Web-Standards"]
Logic --> Modular["Modulare Komponenten"]
Standards --> Decade["Lebensdauer > 10 Jahre"]
Modular --> Update["Einfache Teil-Modernisierung"]
Decade --> ROI["Maximaler Return on Investment"]
Update --> ROI
style ROI fill:#4ade80,stroke:#333
style Decade fill:#4ade80,stroke:#333
</Mermaid>
<div className="text-center text-xs text-slate-400 mt-4 italic">
Architektur der Langlebigkeit: Durch die Trennung von Logik und Trends
sichern wir den Wert Ihrer digitalen Investition über Generationen.
</div>
</div>
<DigitalAssetVisualizer />
<H3>Die Ästhetik der Zeitlosigkeit</H3>
<Paragraph>Langlebigkeit hat auch eine visuelle Komponente.</Paragraph>

View File

@@ -30,7 +30,6 @@ tags: ["performance", "seo", "conversion-optimization"]
isCompany={true}
source="Amazon CDN Study"
sourceUrl="https://vwo.com/blog/100ms-latency-cost-amazon-1-percent-sales/"
translated={true}
/>
<Paragraph>
@@ -181,6 +180,8 @@ graph TD
<StatsGrid stats="3x|Conversion Boost|bei 1s vs 5s Load~24%|Mehr Leads|bei bestandenen Web Vitals~8.4%|Conversion Plus|pro 100ms Verbesserung" />
<PerformanceROICalculator />
<Paragraph>
Mein System fungiert als ROI-Beschleuniger für Ihren gesamten digitalen Auftritt. Warum viele Agenturen bei diesem Thema scheitern, erkläre ich im Detail in meinem Artikel [Warum Ihre Agentur für kleine Änderungen Wochen braucht](/blog/why-agencies-are-slow).
</Paragraph>

View File

@@ -122,6 +122,8 @@ graph LR
</IconList>
<div className="my-12 space-y-8">
<LoadTimeSimulator />
<ComparisonRow
description="Status Quo (Altlasten) vs. Mintel Standard"
negativeLabel="Historisch gewachsene Last"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -3,9 +3,9 @@ import * as path from 'node:path';
import * as fs from 'node:fs/promises';
async function run() {
const apiKey = process.env.REPLICATE_API_KEY;
const apiKey = process.env.REPLICATE_API_TOKEN || process.env.REPLICATE_API_KEY;
if (!apiKey) {
console.error("❌ Missing REPLICATE_API_KEY in environment.");
console.error("❌ Missing REPLICATE_API_TOKEN in environment.");
process.exit(1);
}
@@ -34,9 +34,35 @@ async function run() {
console.log(`Generating abstract thumbnail for topic: "${topic}"`);
const generator = new ThumbnailGenerator({ replicateApiKey: apiKey });
const outputPath = path.join(process.cwd(), 'public', 'blog', filename);
const isRoot = process.cwd().endsWith('mintel.me');
const baseDir = isRoot ? path.join(process.cwd(), 'apps', 'web') : process.cwd();
await generator.generateImage(topic, outputPath);
const outputPath = path.join(baseDir, 'public', 'blog', filename);
// Check if thumbnail already exists to avoid redundant generation
try {
await fs.access(outputPath);
console.log(`⏭️ Thumbnail already exists, skipping: ${filename}`);
return;
} catch {
// File does not exist, proceed with generation
}
const inspirationPath = path.join(baseDir, 'public', 'blog', 'inspiration.png');
let hasInspiration = false;
try {
await fs.access(inspirationPath);
hasInspiration = true;
} catch {
hasInspiration = false;
}
const customPrompt = `Extremely clean, flat, abstract geometric illustration. Use the provided image prompt ONLY as a STRICT style, color, and texture reference. Do not copy the image content, just the aesthetic. Characteristics: Flat vector design, 2D only (no 3D), tech/startup/agency aesthetics, highly professional, abstract data representations, extensive use of whitespace. No text, no chaotic lines, no humans.`;
await generator.generateImage(topic, outputPath, {
systemPrompt: customPrompt,
imagePrompt: hasInspiration ? inspirationPath : undefined,
});
}
run().catch((e) => {

View File

@@ -4,7 +4,7 @@ import puppeteer from 'puppeteer';
try {
console.log("Starting Chrome...");
const browser = await puppeteer.launch({
headless: 'new',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});

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

View File

@@ -37,6 +37,10 @@ import { TableOfContents } from '../components/TableOfContents';
import { RevenueLossCalculator } from "../components/RevenueLossCalculator";
import { PerformanceChart } from "../components/PerformanceChart";
import { PerformanceROICalculator } from "../components/simulations/PerformanceROICalculator";
import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator";
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
import { TwitterEmbed } from '../components/TwitterEmbed';
import { YouTubeEmbed } from '../components/YouTubeEmbed';
@@ -79,6 +83,10 @@ export const mdxComponents = {
TableOfContents,
RevenueLossCalculator,
PerformanceChart,
PerformanceROICalculator,
LoadTimeSimulator,
ArchitectureBuilder,
DigitalAssetVisualizer,
TwitterEmbed,
YouTubeEmbed,
LinkedInEmbed,

View File

@@ -260,12 +260,32 @@ timeline
},
{
name: 'Button',
description: 'Premium pill-shaped button for high-impact CTAs. Variants: primary (solid dark), outline (bordered), ghost (text only). use size="large" for main sections.',
description: 'Premium pill-shaped button for high-impact CTAs. Variants: primary (solid dark), outline (bordered), ghost (text only). use size="large" for main sections. IMPORTANT: Write natural, punchy, conversational German CTAs (e.g. "Webprojekt anfragen", "Zur Beratung", "Mehr erfahren"). NEVER generate robotic or awkward phrases like "Ethisches Tracking anfragen" or "Ladezeit optimieren lassen". Keep it human and professional.',
usageExample: '<Button href="/contact" variant="outline">Webprojekt anfragen</Button>'
},
{
name: 'PerformanceROICalculator',
description: 'Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.',
usageExample: '<PerformanceROICalculator />'
},
{
name: 'LoadTimeSimulator',
description: 'Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.',
usageExample: '<LoadTimeSimulator />'
},
{
name: 'FAQSection',
description: 'Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.',
usageExample: '<FAQSection>\n <H3>Frage 1</H3>\n <Paragraph>Antwort 1</Paragraph>\n</FAQSection>'
},
{
name: 'ArchitectureBuilder',
description: 'Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.',
usageExample: '<ArchitectureBuilder />'
},
{
name: 'DigitalAssetVisualizer',
description: 'Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.',
usageExample: '<DigitalAssetVisualizer />'
}
];