chore: enable TypeScript strict mode and drop dead Payload scripts

The shared base tsconfig had strict off, so null-safety and union narrowing
were not checked. Strict is now on for apps/web with 5 small typing fixes.
check-db.ts and migrate-drafts.js depended on Payload, which is no longer a
dependency, and are removed. CLAUDE.md no longer claims Payload.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:59:47 +02:00
co-authored by Claude Sonnet 5.5
parent 6e5d14882e
commit d33a5d6c63
9 changed files with 774 additions and 435 deletions
+2 -2
View File
@@ -93,7 +93,7 @@ export default async function BlogPostPage({
<Reveal delay={0.4} width="100%">
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
{post.tags?.map((tag) => (
{post.tags?.map((tag: string) => (
<span
key={tag}
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
@@ -106,7 +106,7 @@ export default async function BlogPostPage({
<div className="article-content max-w-none">
<TableOfContents />
<MDXContent code={post.mdxContent || ""} />
<MDXContent code={post.mdxContent || ""} />
</div>
</Reveal>
</div>
-12
View File
@@ -1,12 +0,0 @@
export const run = async ({ payload }) => {
const docs = await payload.find({
collection: "context-files",
limit: 100,
});
console.log(`--- DB CHECK ---`);
console.log(`Found ${docs.totalDocs} context files.`);
docs.docs.forEach((doc) => {
console.log(`- ${doc.filename}`);
});
process.exit(0);
};
+4 -2
View File
@@ -81,9 +81,11 @@ async function main() {
}
});
page.on("pageerror", (err: Error) => {
page.on("pageerror", (err: unknown) => {
if (currentScannedUrl.includes("showcase")) return;
console.error(` [PAGE EXCEPTION] ${err.message}`);
console.error(
` [PAGE EXCEPTION] ${err instanceof Error ? err.message : String(err)}`,
);
});
// Listen to ALL network responses to catch broken assets (404/500)
+61 -31
View File
@@ -22,7 +22,10 @@ interface MermaidProps {
legendFontSize?: string;
}
class MermaidErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
class MermaidErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
@@ -73,16 +76,17 @@ const MermaidInternal: React.FC<MermaidProps> = ({
// Extract text from React children nodes (MDX parses multi-line content as React nodes)
const extractTextFromChildren = (node: React.ReactNode): string => {
if (typeof node === 'string') return node;
if (typeof node === 'number') return String(node);
if (Array.isArray(node)) return node.map(extractTextFromChildren).join('\n');
if (typeof node === "string") return node;
if (typeof node === "number") return String(node);
if (Array.isArray(node))
return node.map(extractTextFromChildren).join("\n");
if (React.isValidElement(node)) {
const props = node.props as { children?: React.ReactNode };
if (props.children) {
return extractTextFromChildren(props.children);
}
}
return '';
return "";
};
const rawGraph = graph || extractTextFromChildren(children) || "";
@@ -91,8 +95,8 @@ const MermaidInternal: React.FC<MermaidProps> = ({
// We need to unescape these back to real newlines for Mermaid to parse
const sanitizedGraph = rawGraph
.trim()
.replace(/^`+|`+$/g, '')
.replace(/\\n/g, '\n')
.replace(/^`+|`+$/g, "")
.replace(/\\n/g, "\n")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
@@ -100,13 +104,16 @@ const MermaidInternal: React.FC<MermaidProps> = ({
if (!sanitizedGraph || sanitizedGraph.length < 5) {
return (
<div className="not-prose my-8 p-6 bg-slate-50 border border-slate-200 rounded-xl text-center">
<p className="text-sm text-slate-400 font-medium">Diagramm konnte nicht geladen werden</p>
<p className="text-sm text-slate-400 font-medium">
Diagramm konnte nicht geladen werden
</p>
</div>
);
}
useEffect(() => {
const generatedId = providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`;
const generatedId =
providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`;
setId(generatedId);
}, [providedId]);
@@ -123,7 +130,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
}
});
},
{ rootMargin: "200px" } // Start rendering 200px before it scrolls into view
{ rootMargin: "200px" }, // Start rendering 200px before it scrolls into view
);
observer.observe(wrapperRef.current);
@@ -143,17 +150,17 @@ const MermaidInternal: React.FC<MermaidProps> = ({
flowchart: {
useMaxWidth: true,
htmlLabels: false,
curve: 'basis',
curve: "basis",
nodeSpacing: 50,
rankSpacing: 60,
padding: 15
padding: 15,
},
sequence: {
useMaxWidth: true,
showSequenceNumbers: false,
height: 40,
actorMargin: 30,
messageMargin: 30
messageMargin: 30,
},
gantt: { useMaxWidth: true },
pie: { useMaxWidth: true },
@@ -175,7 +182,8 @@ const MermaidInternal: React.FC<MermaidProps> = ({
clusterBkg: "#f8fafc",
clusterBorder: "#cbd5e1",
edgeLabelBackground: "#ffffff",
fontFamily: "var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
fontFamily:
"var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
fontSize: fontSize || "18px",
nodeFontSize: nodeFontSize || fontSize || "18px",
labelFontSize: labelFontSize || fontSize || "16px",
@@ -216,15 +224,19 @@ const MermaidInternal: React.FC<MermaidProps> = ({
if (!mermaid.render) return;
const uniqueSvgId = `${id}-svg-${Date.now()}`;
const tempDiv = document.createElement('div');
const tempDiv = document.createElement("div");
document.body.appendChild(tempDiv);
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
tempDiv.style.visibility = 'hidden';
tempDiv.style.position = "absolute";
tempDiv.style.left = "-9999px";
tempDiv.style.visibility = "hidden";
let rawSvg: string;
try {
const result = await mermaid.render(uniqueSvgId, sanitizedGraph, tempDiv);
const result = await mermaid.render(
uniqueSvgId,
sanitizedGraph,
tempDiv,
);
rawSvg = result.svg;
} finally {
if (document.body.contains(tempDiv)) {
@@ -233,11 +245,13 @@ const MermaidInternal: React.FC<MermaidProps> = ({
}
let scaledSvg = rawSvg;
const svgStartEnd = rawSvg.indexOf('>');
const svgStartEnd = rawSvg.indexOf(">");
if (svgStartEnd > -1) {
const svgTag = rawSvg.substring(0, svgStartEnd + 1);
const rest = rawSvg.substring(svgStartEnd + 1);
const newSvgTag = svgTag.replace(/width="[^"]*"/, 'width="100%"').replace(/height="[^"]*"/, 'height="auto"');
const newSvgTag = svgTag
.replace(/width="[^"]*"/, 'width="100%"')
.replace(/height="[^"]*"/, 'height="auto"');
scaledSvg = newSvgTag + rest;
}
@@ -250,7 +264,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
setError("Diagramm konnte nicht geladen werden (Render-Fehler).");
setIsRendered(true);
} else {
await new Promise(resolve => setTimeout(resolve, attempt * 200));
await new Promise((resolve) => setTimeout(resolve, attempt * 200));
}
}
}
@@ -265,7 +279,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
}
});
resizeObserver.observe(wrapperRef.current);
if (wrapperRef.current) resizeObserver.observe(wrapperRef.current);
if (wrapperRef.current && wrapperRef.current.clientWidth > 0) {
renderGraph();
@@ -273,25 +287,41 @@ const MermaidInternal: React.FC<MermaidProps> = ({
}
return () => resizeObserver.disconnect();
}, [isVisible, id, sanitizedGraph, fontSize, nodeFontSize, labelFontSize, actorFontSize, messageFontSize, noteFontSize, titleFontSize, sectionFontSize, legendFontSize]);
}, [
isVisible,
id,
sanitizedGraph,
fontSize,
nodeFontSize,
labelFontSize,
actorFontSize,
messageFontSize,
noteFontSize,
titleFontSize,
sectionFontSize,
legendFontSize,
]);
if (!id) return null;
return (
<Reveal direction="up" delay={0.1}>
<figure ref={wrapperRef} className="mermaid-wrapper not-prose my-16 w-full max-w-full group relative transition-all duration-500 ease-out z-10">
<figure
ref={wrapperRef}
className="mermaid-wrapper not-prose my-16 w-full max-w-full group relative transition-all duration-500 ease-out z-10"
>
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100/50 rounded-[2rem] blur opacity-20 group-hover:opacity-40 transition duration-1000 -z-10" />
<div className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl shadow-sm group-hover:shadow-md group-hover:border-slate-200 transition-all duration-500 overflow-hidden relative">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white to-transparent opacity-80" />
<div className="w-full flex flex-col items-center justify-center p-6 md:p-8 lg:p-10 relative z-10">
{showShare && (
<div className="absolute top-6 right-6 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
<ComponentShareButton targetId={id} title={title || 'System Architecture'} />
<ComponentShareButton
targetId={id}
title={title || "System Architecture"}
/>
</div>
)}
@@ -322,7 +352,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
id={id}
style={{
maxWidth: "100%",
overflow: "visible"
overflow: "visible",
}}
>
{error ? (
@@ -332,7 +362,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
) : svgContent ? (
<div dangerouslySetInnerHTML={{ __html: svgContent }} />
) : (
<div style={{ display: 'none' }}>{sanitizedGraph}</div>
<div style={{ display: "none" }}>{sanitizedGraph}</div>
)}
</div>
</div>
@@ -1,175 +1,357 @@
"use client";
import React, { useState } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
import { ComposedChart, Line, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { ShieldAlert, Rocket, Database, Lock } from 'lucide-react';
import React, { useState } from "react";
import { ComponentShareButton } from "../ComponentShareButton";
import { Reveal } from "../Reveal";
import {
ComposedChart,
Line,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { ShieldAlert, Rocket, Database, Lock } from "lucide-react";
const archData = [
{ year: 'Jahr 1', saasInvest: 50, saasIndep: 40, customInvest: 120, customIndep: 80 },
{ year: 'Jahr 2', saasInvest: 100, saasIndep: 30, customInvest: 140, customIndep: 90 },
{ year: 'Jahr 3', saasInvest: 155, saasIndep: 20, customInvest: 155, customIndep: 95 },
{ year: 'Jahr 4', saasInvest: 220, saasIndep: 10, customInvest: 170, customIndep: 100 },
{ year: 'Jahr 5', saasInvest: 290, saasIndep: 5, customInvest: 185, customIndep: 100 },
{
year: "Jahr 1",
saasInvest: 50,
saasIndep: 40,
customInvest: 120,
customIndep: 80,
},
{
year: "Jahr 2",
saasInvest: 100,
saasIndep: 30,
customInvest: 140,
customIndep: 90,
},
{
year: "Jahr 3",
saasInvest: 155,
saasIndep: 20,
customInvest: 155,
customIndep: 95,
},
{
year: "Jahr 4",
saasInvest: 220,
saasIndep: 10,
customInvest: 170,
customIndep: 100,
},
{
year: "Jahr 5",
saasInvest: 290,
saasIndep: 5,
customInvest: 185,
customIndep: 100,
},
];
export function ArchitectureBuilder({ className = '' }: { className?: string }) {
const [stack, setStack] = useState<'saas' | 'custom'>('custom');
export function ArchitectureBuilder({
className = "",
}: {
className?: string;
}) {
const [stack, setStack] = useState<"saas" | "custom">("custom");
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-200 to-red-100' : 'bg-gradient-to-r from-emerald-200 to-blue-100'}`} />
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-200 to-red-100" : "bg-gradient-to-r from-emerald-200 to-blue-100"}`}
/>
<div id="sim-arch-builder" className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 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: Investment vs. Unabhängigkeit"
shareText="Sehen Sie sich den zeitlichen Verlauf an: Software-Miete vs. digitales Eigentum."
/>
</div>
<div
id="sim-arch-builder"
className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 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: Investment vs. Unabhängigkeit"
shareText="Sehen Sie sich den zeitlichen Verlauf an: Software-Miete vs. digitales Eigentum."
/>
</div>
<div className="p-6 md:p-8 border-b border-slate-200 bg-slate-50 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-slate-200 rounded-full blur-3xl -mr-32 -mt-32 opacity-50" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">🏗️</span>
<h3 className="font-bold text-slate-900 m-0 text-xl">Zeitlicher Verlauf: Investment vs. Unabhängigkeit</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Wie Plattform-Abhängigkeit (Lock-in) über die Jahre Ihre Skalierbarkeit zerstört, während ein Headless-System echte Souveränität aufbaut.
</p>
</div>
</div>
<div className="flex flex-col">
{/* Control Tabs */}
<div className="flex border-b border-slate-100">
<button
onClick={() => setStack('saas')}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${stack === 'saas' ? 'bg-white border-b-2 border-orange-500' : 'bg-slate-50 text-slate-400 hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-full ${stack === 'saas' ? 'bg-orange-100 text-orange-600' : 'bg-slate-200'}`}>
<Lock size={20} />
</div>
<div className="text-center">
<div className={`font-bold text-sm ${stack === 'saas' ? 'text-slate-900' : ''}`}>SaaS / Baukasten</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">Gekauft ist schneller</div>
</div>
</button>
<button
onClick={() => setStack('custom')}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${stack === 'custom' ? 'bg-white border-b-2 border-emerald-500' : 'bg-slate-50 text-slate-400 hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-full ${stack === 'custom' ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-200'}`}>
<Rocket size={20} />
</div>
<div className="text-center">
<div className={`font-bold text-sm ${stack === 'custom' ? 'text-slate-900' : ''}`}>Build-First / Headless</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">Architektur als Asset</div>
</div>
</button>
</div>
{/* Chart Area */}
<div className="p-6 md:p-8 bg-white">
<div className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={archData} margin={{ top: 20, right: 30, left: 20, bottom: 0 }}>
<defs>
<linearGradient id="barSaas" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fb923c" stopOpacity={0.8} />
<stop offset="100%" stopColor="#fb923c" stopOpacity={0.3} />
</linearGradient>
<linearGradient id="barCustom" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.8} />
<stop offset="100%" stopColor="#94a3b8" stopOpacity={0.3} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
<XAxis
dataKey="year"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 12, fontWeight: 500 }}
dy={10}
/>
{/* Left Y-Axis: Cost */}
<YAxis
yAxisId="left"
tickFormatter={(val) => `€${val}k`}
axisLine={false}
tickLine={false}
tick={{ fill: '#94a3b8', fontSize: 12 }}
dx={-10}
/>
{/* Right Y-Axis: Independence */}
<YAxis
yAxisId="right"
orientation="right"
tickFormatter={(val) => `${val}%`}
axisLine={false}
tickLine={false}
tick={{ fill: '#14b8a6', fontSize: 12, fontWeight: 'bold' }}
dx={10}
domain={[0, 100]}
/>
<Tooltip
contentStyle={{ borderRadius: '12px', border: '1px solid #e2e8f0', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}
formatter={(value: number, name: string) => {
if (name === 'saasInvest') return [`€${value}.000`, 'Kumuliertes Investment (Miete + Addons)'];
if (name === 'customInvest') return [`€${value}.000`, 'Kumuliertes Investment (Bau + Betrieb)'];
if (name === 'saasIndep') return [`${value}%`, 'Technische Unabhängigkeit (Lock-in sinkt)'];
if (name === 'customIndep') return [`${value}%`, 'Technische Unabhängigkeit & Datenhoheit'];
return [value, name];
}}
labelStyle={{ fontWeight: 'bold', color: '#0f172a', marginBottom: '8px' }}
/>
{stack === 'saas' ? (
<>
<Bar yAxisId="left" dataKey="saasInvest" fill="#fb923c" radius={[4, 4, 0, 0]} barSize={40} animationDuration={1000} />
<Line yAxisId="right" type="monotone" dataKey="saasIndep" stroke="#ef4444" strokeWidth={4} dot={{ r: 6, fill: "#ef4444", strokeWidth: 2, stroke: "#fff" }} animationDuration={1500} />
</>
) : (
<>
<Bar yAxisId="left" dataKey="customInvest" fill="#94a3b8" radius={[4, 4, 0, 0]} barSize={40} animationDuration={1000} />
<Line yAxisId="right" type="monotone" dataKey="customIndep" stroke="#10b981" strokeWidth={4} dot={{ r: 6, fill: "#10b981", strokeWidth: 2, stroke: "#fff" }} animationDuration={1500} />
</>
)}
</ComposedChart>
</ResponsiveContainer>
</div>
{/* Impact Summary */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
<div className={`p-4 rounded-xl border flex gap-4 ${stack === 'saas' ? 'bg-orange-50 border-orange-100' : 'bg-emerald-50 border-emerald-100'}`}>
<div className={`mt-1 rounded-full p-2 h-fit ${stack === 'saas' ? 'bg-orange-100 text-orange-600' : 'bg-emerald-100 text-emerald-600'}`}>
{stack === 'saas' ? <ShieldAlert size={20} /> : <Database size={20} />}
</div>
<div>
<div className={`font-bold text-sm ${stack === 'saas' ? 'text-orange-900' : 'text-emerald-900'}`}>
{stack === 'saas' ? 'Massiver Platform Lock-in' : '100% Datenhoheit'}
</div>
<div className="text-xs mt-1 text-slate-600 leading-relaxed">
{stack === 'saas'
? 'Ihre Daten und Prozesse liegen bei Dritten. Umzüge werden nach Jahr 2 exponentiell teuers und riskant.'
: 'Sie besitzen die Kern-Datenbanken und APIs. Keine Lizenzschocks, keine Blackbox-Updates.'}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="p-6 md:p-8 border-b border-slate-200 bg-slate-50 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-slate-200 rounded-full blur-3xl -mr-32 -mt-32 opacity-50" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">🏗️</span>
<h3 className="font-bold text-slate-900 m-0 text-xl">
Zeitlicher Verlauf: Investment vs. Unabhängigkeit
</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Wie Plattform-Abhängigkeit (Lock-in) über die Jahre Ihre
Skalierbarkeit zerstört, während ein Headless-System echte
Souveränität aufbaut.
</p>
</div>
</Reveal>
);
</div>
<div className="flex flex-col">
{/* Control Tabs */}
<div className="flex border-b border-slate-100">
<button
onClick={() => setStack("saas")}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${stack === "saas" ? "bg-white border-b-2 border-orange-500" : "bg-slate-50 text-slate-400 hover:bg-slate-100"}`}
>
<div
className={`p-2 rounded-full ${stack === "saas" ? "bg-orange-100 text-orange-600" : "bg-slate-200"}`}
>
<Lock size={20} />
</div>
<div className="text-center">
<div
className={`font-bold text-sm ${stack === "saas" ? "text-slate-900" : ""}`}
>
SaaS / Baukasten
</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">
Gekauft ist schneller
</div>
</div>
</button>
<button
onClick={() => setStack("custom")}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${stack === "custom" ? "bg-white border-b-2 border-emerald-500" : "bg-slate-50 text-slate-400 hover:bg-slate-100"}`}
>
<div
className={`p-2 rounded-full ${stack === "custom" ? "bg-emerald-100 text-emerald-600" : "bg-slate-200"}`}
>
<Rocket size={20} />
</div>
<div className="text-center">
<div
className={`font-bold text-sm ${stack === "custom" ? "text-slate-900" : ""}`}
>
Build-First / Headless
</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">
Architektur als Asset
</div>
</div>
</button>
</div>
{/* Chart Area */}
<div className="p-6 md:p-8 bg-white">
<div className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={archData}
margin={{ top: 20, right: 30, left: 20, bottom: 0 }}
>
<defs>
<linearGradient id="barSaas" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="#fb923c"
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor="#fb923c"
stopOpacity={0.3}
/>
</linearGradient>
<linearGradient
id="barCustom"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor="#94a3b8"
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor="#94a3b8"
stopOpacity={0.3}
/>
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke="#e2e8f0"
/>
<XAxis
dataKey="year"
axisLine={false}
tickLine={false}
tick={{ fill: "#64748b", fontSize: 12, fontWeight: 500 }}
dy={10}
/>
{/* Left Y-Axis: Cost */}
<YAxis
yAxisId="left"
tickFormatter={(val) => `€${val}k`}
axisLine={false}
tickLine={false}
tick={{ fill: "#94a3b8", fontSize: 12 }}
dx={-10}
/>
{/* Right Y-Axis: Independence */}
<YAxis
yAxisId="right"
orientation="right"
tickFormatter={(val) => `${val}%`}
axisLine={false}
tickLine={false}
tick={{
fill: "#14b8a6",
fontSize: 12,
fontWeight: "bold",
}}
dx={10}
domain={[0, 100]}
/>
<Tooltip
contentStyle={{
borderRadius: "12px",
border: "1px solid #e2e8f0",
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)",
}}
formatter={(value, name): [string, string] => {
if (name === "saasInvest")
return [
`€${value}.000`,
"Kumuliertes Investment (Miete + Addons)",
];
if (name === "customInvest")
return [
`€${value}.000`,
"Kumuliertes Investment (Bau + Betrieb)",
];
if (name === "saasIndep")
return [
`${value}%`,
"Technische Unabhängigkeit (Lock-in sinkt)",
];
if (name === "customIndep")
return [
`${value}%`,
"Technische Unabhängigkeit & Datenhoheit",
];
return [String(value), String(name)];
}}
labelStyle={{
fontWeight: "bold",
color: "#0f172a",
marginBottom: "8px",
}}
/>
{stack === "saas" ? (
<>
<Bar
yAxisId="left"
dataKey="saasInvest"
fill="#fb923c"
radius={[4, 4, 0, 0]}
barSize={40}
animationDuration={1000}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="saasIndep"
stroke="#ef4444"
strokeWidth={4}
dot={{
r: 6,
fill: "#ef4444",
strokeWidth: 2,
stroke: "#fff",
}}
animationDuration={1500}
/>
</>
) : (
<>
<Bar
yAxisId="left"
dataKey="customInvest"
fill="#94a3b8"
radius={[4, 4, 0, 0]}
barSize={40}
animationDuration={1000}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="customIndep"
stroke="#10b981"
strokeWidth={4}
dot={{
r: 6,
fill: "#10b981",
strokeWidth: 2,
stroke: "#fff",
}}
animationDuration={1500}
/>
</>
)}
</ComposedChart>
</ResponsiveContainer>
</div>
{/* Impact Summary */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
<div
className={`p-4 rounded-xl border flex gap-4 ${stack === "saas" ? "bg-orange-50 border-orange-100" : "bg-emerald-50 border-emerald-100"}`}
>
<div
className={`mt-1 rounded-full p-2 h-fit ${stack === "saas" ? "bg-orange-100 text-orange-600" : "bg-emerald-100 text-emerald-600"}`}
>
{stack === "saas" ? (
<ShieldAlert size={20} />
) : (
<Database size={20} />
)}
</div>
<div>
<div
className={`font-bold text-sm ${stack === "saas" ? "text-orange-900" : "text-emerald-900"}`}
>
{stack === "saas"
? "Massiver Platform Lock-in"
: "100% Datenhoheit"}
</div>
<div className="text-xs mt-1 text-slate-600 leading-relaxed">
{stack === "saas"
? "Ihre Daten und Prozesse liegen bei Dritten. Umzüge werden nach Jahr 2 exponentiell teuers und riskant."
: "Sie besitzen die Kern-Datenbanken und APIs. Keine Lizenzschocks, keine Blackbox-Updates."}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Reveal>
);
}
@@ -1,201 +1,371 @@
"use client";
import React, { useState } from 'react';
import { ComponentShareButton } from '../ComponentShareButton';
import { Reveal } from '../Reveal';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { Briefcase, Coins, PiggyBank, TrendingUp } from 'lucide-react';
import React, { useState } from "react";
import { ComponentShareButton } from "../ComponentShareButton";
import { Reveal } from "../Reveal";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Briefcase, Coins, PiggyBank, TrendingUp } from "lucide-react";
const data = [
{ year: 'Jahr 1', saasCost: 40000, saasAsset: 0, customCost: 120000, customAsset: 120000 },
{ year: 'Jahr 2', saasCost: 85000, saasAsset: 0, customCost: 140000, customAsset: 110000 },
{ year: 'Jahr 3', saasCost: 135000, saasAsset: 0, customCost: 160000, customAsset: 100000 },
{ year: 'Jahr 4', saasCost: 190000, saasAsset: 0, customCost: 180000, customAsset: 90000 },
{ year: 'Jahr 5', saasCost: 250000, saasAsset: 0, customCost: 200000, customAsset: 85000 },
{
year: "Jahr 1",
saasCost: 40000,
saasAsset: 0,
customCost: 120000,
customAsset: 120000,
},
{
year: "Jahr 2",
saasCost: 85000,
saasAsset: 0,
customCost: 140000,
customAsset: 110000,
},
{
year: "Jahr 3",
saasCost: 135000,
saasAsset: 0,
customCost: 160000,
customAsset: 100000,
},
{
year: "Jahr 4",
saasCost: 190000,
saasAsset: 0,
customCost: 180000,
customAsset: 90000,
},
{
year: "Jahr 5",
saasCost: 250000,
saasAsset: 0,
customCost: 200000,
customAsset: 85000,
},
];
const formatCurrency = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(val);
const formatCurrency = (val: number) =>
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
maximumFractionDigits: 0,
}).format(val);
export function DigitalAssetVisualizer({ className = '' }: { className?: string }) {
const [view, setView] = useState<'saas' | 'custom'>('custom');
export function DigitalAssetVisualizer({
className = "",
}: {
className?: string;
}) {
const [view, setView] = useState<"saas" | "custom">("custom");
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 === 'saas' ? 'bg-gradient-to-r from-red-200 to-orange-100' : 'bg-gradient-to-r from-indigo-200 to-emerald-100'}`} />
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 === "saas" ? "bg-gradient-to-r from-red-200 to-orange-100" : "bg-gradient-to-r from-indigo-200 to-emerald-100"}`}
/>
<div id="sim-digital-asset" className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 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="Software: Ausgabe oder Investment?"
shareText="Sehen Sie sich den finanziellen Verlauf von Software-Miete (SaaS) gegenüber einer Inhouse-Eigenentwicklung an:"
/>
</div>
<div
id="sim-digital-asset"
className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 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="Software: Ausgabe oder Investment?"
shareText="Sehen Sie sich den finanziellen Verlauf von Software-Miete (SaaS) gegenüber einer Inhouse-Eigenentwicklung an:"
/>
</div>
<div className="p-6 md:p-8 border-b border-slate-200 bg-slate-50 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-slate-200 rounded-full blur-3xl -mr-32 -mt-32 opacity-50" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">💰</span>
<h3 className="font-bold text-slate-900 m-0 text-xl">Software: Ausgabe oder Investment?</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Kumulierte Kosten im Vergleich zum bilanzierbaren Unternehmenswert über 5 Jahre.
</p>
</div>
</div>
<div className="flex flex-col">
{/* Control Tabs */}
<div className="flex border-b border-slate-100">
<button
onClick={() => setView('saas')}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${view === 'saas' ? 'bg-white border-b-2 border-red-500' : 'bg-slate-50 text-slate-400 hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-full ${view === 'saas' ? 'bg-red-100 text-red-600' : 'bg-slate-200'}`}>
<Coins size={20} />
</div>
<div className="text-center">
<div className={`font-bold text-sm ${view === 'saas' ? 'text-slate-900' : ''}`}>SaaS-Miete (Verbindlichkeit)</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">100% OpEx</div>
</div>
</button>
<button
onClick={() => setView('custom')}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${view === 'custom' ? 'bg-white border-b-2 border-indigo-500' : 'bg-slate-50 text-slate-400 hover:bg-slate-100'}`}
>
<div className={`p-2 rounded-full ${view === 'custom' ? 'bg-indigo-100 text-indigo-600' : 'bg-slate-200'}`}>
<Briefcase size={20} />
</div>
<div className="text-center">
<div className={`font-bold text-sm ${view === 'custom' ? 'text-slate-900' : ''}`}>Eigenentwicklung (Asset)</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">CapEx + IP-Aufbau</div>
</div>
</button>
</div>
{/* Chart Area */}
<div className="p-6 md:p-8 bg-white">
<div className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 0 }}>
<defs>
<linearGradient id="colorCostSaas" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.15} />
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorCostCustom" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#94a3b8" stopOpacity={0.15} />
<stop offset="95%" stopColor="#94a3b8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorAsset" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#4f46e5" stopOpacity={0.2} />
<stop offset="95%" stopColor="#4f46e5" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
<XAxis
dataKey="year"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 12, fontWeight: 500 }}
dy={10}
/>
<YAxis
tickFormatter={(val) => `€${val / 1000}k`}
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 12 }}
dx={-10}
/>
<Tooltip
contentStyle={{ borderRadius: '8px', border: '1px solid #f1f5f9', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', padding: '6px 10px', fontSize: '11px', backgroundColor: 'rgba(255, 255, 255, 0.95)', backdropFilter: 'blur(4px)' }}
itemStyle={{ fontSize: '11px', padding: '2px 0' }}
formatter={(value: number, name: string) => {
if (name === 'saasCost') return [formatCurrency(value), 'Kumulierte Kosten (Miete)'];
if (name === 'customCost') return [formatCurrency(value), 'Kumulierte Kosten (Bau + Wartung)'];
if (name === 'customAsset') return [formatCurrency(value), 'Firmenwert (Digitales Eigentum)'];
if (name === 'saasAsset') return [formatCurrency(value), 'Firmenwert (IP)'];
return [value, name];
}}
labelStyle={{ fontWeight: 'bold', color: '#0f172a', marginBottom: '4px', fontSize: '11px' }}
/>
{view === 'saas' ? (
<>
<Area
type="monotone"
dataKey="saasCost"
stroke="#ef4444"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorCostSaas)"
animationDuration={1500}
/>
<Area
type="monotone"
dataKey="saasAsset"
stroke="#cbd5e1"
strokeWidth={2}
fillOpacity={0}
animationDuration={1500}
/>
</>
) : (
<>
<Area
type="monotone"
dataKey="customAsset"
stroke="#4f46e5"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorAsset)"
animationDuration={1500}
/>
<Area
type="monotone"
dataKey="customCost"
stroke="#64748b"
strokeWidth={2}
strokeDasharray="5 5"
fillOpacity={1}
fill="url(#colorCostCustom)"
animationDuration={1500}
/>
</>
)}
</AreaChart>
</ResponsiveContainer>
</div>
{/* Summary Metrics */}
<div className="grid grid-cols-2 gap-4 mt-8">
<div className={`p-4 rounded-xl border ${view === 'saas' ? 'bg-red-50 border-red-100' : 'bg-slate-50 border-slate-200'}`}>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
Total Spend (Jahr 5)
</div>
<div className={`text-2xl font-bold flex items-center gap-2 ${view === 'saas' ? 'text-red-600' : 'text-slate-700'}`}>
{view === 'saas' ? '250.000 €' : '200.000 €'}
{view === 'saas' && <TrendingUp size={16} className="text-red-500" />}
</div>
<div className="text-xs text-slate-500 mt-1">Sunk Costs (Geldabfluss)</div>
</div>
<div className={`p-4 rounded-xl border ${view === 'saas' ? 'bg-slate-50 border-slate-200' : 'bg-indigo-50 border-indigo-100'}`}>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
Bilanzierbarer Wert (IP)
</div>
<div className={`text-2xl font-bold flex items-center gap-2 ${view === 'saas' ? 'text-slate-400' : 'text-indigo-600'}`}>
{view === 'saas' ? '0 €' : '85.000 €'}
{view === 'custom' && <PiggyBank size={18} className="text-indigo-500" />}
</div>
<div className="text-xs text-slate-500 mt-1">Erhöht den Unternehmenswert</div>
</div>
</div>
</div>
</div>
</div>
<div className="p-6 md:p-8 border-b border-slate-200 bg-slate-50 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-slate-200 rounded-full blur-3xl -mr-32 -mt-32 opacity-50" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">💰</span>
<h3 className="font-bold text-slate-900 m-0 text-xl">
Software: Ausgabe oder Investment?
</h3>
</div>
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
Kumulierte Kosten im Vergleich zum bilanzierbaren
Unternehmenswert über 5 Jahre.
</p>
</div>
</Reveal>
);
</div>
<div className="flex flex-col">
{/* Control Tabs */}
<div className="flex border-b border-slate-100">
<button
onClick={() => setView("saas")}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${view === "saas" ? "bg-white border-b-2 border-red-500" : "bg-slate-50 text-slate-400 hover:bg-slate-100"}`}
>
<div
className={`p-2 rounded-full ${view === "saas" ? "bg-red-100 text-red-600" : "bg-slate-200"}`}
>
<Coins size={20} />
</div>
<div className="text-center">
<div
className={`font-bold text-sm ${view === "saas" ? "text-slate-900" : ""}`}
>
SaaS-Miete (Verbindlichkeit)
</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">
100% OpEx
</div>
</div>
</button>
<button
onClick={() => setView("custom")}
className={`flex-1 p-4 flex flex-col items-center justify-center gap-2 transition-all ${view === "custom" ? "bg-white border-b-2 border-indigo-500" : "bg-slate-50 text-slate-400 hover:bg-slate-100"}`}
>
<div
className={`p-2 rounded-full ${view === "custom" ? "bg-indigo-100 text-indigo-600" : "bg-slate-200"}`}
>
<Briefcase size={20} />
</div>
<div className="text-center">
<div
className={`font-bold text-sm ${view === "custom" ? "text-slate-900" : ""}`}
>
Eigenentwicklung (Asset)
</div>
<div className="text-[10px] uppercase tracking-wider mt-0.5 opacity-70">
CapEx + IP-Aufbau
</div>
</div>
</button>
</div>
{/* Chart Area */}
<div className="p-6 md:p-8 bg-white">
<div className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 20, right: 30, left: 20, bottom: 0 }}
>
<defs>
<linearGradient
id="colorCostSaas"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="#ef4444"
stopOpacity={0.15}
/>
<stop
offset="95%"
stopColor="#ef4444"
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id="colorCostCustom"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="#94a3b8"
stopOpacity={0.15}
/>
<stop
offset="95%"
stopColor="#94a3b8"
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id="colorAsset"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="#4f46e5"
stopOpacity={0.2}
/>
<stop
offset="95%"
stopColor="#4f46e5"
stopOpacity={0.02}
/>
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke="#e2e8f0"
/>
<XAxis
dataKey="year"
axisLine={false}
tickLine={false}
tick={{ fill: "#64748b", fontSize: 12, fontWeight: 500 }}
dy={10}
/>
<YAxis
tickFormatter={(val) => `€${val / 1000}k`}
axisLine={false}
tickLine={false}
tick={{ fill: "#64748b", fontSize: 12 }}
dx={-10}
/>
<Tooltip
contentStyle={{
borderRadius: "8px",
border: "1px solid #f1f5f9",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
padding: "6px 10px",
fontSize: "11px",
backgroundColor: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(4px)",
}}
itemStyle={{ fontSize: "11px", padding: "2px 0" }}
formatter={(value, name): [string, string] => {
if (name === "saasCost")
return [
formatCurrency(Number(value)),
"Kumulierte Kosten (Miete)",
];
if (name === "customCost")
return [
formatCurrency(Number(value)),
"Kumulierte Kosten (Bau + Wartung)",
];
if (name === "customAsset")
return [
formatCurrency(Number(value)),
"Firmenwert (Digitales Eigentum)",
];
if (name === "saasAsset")
return [
formatCurrency(Number(value)),
"Firmenwert (IP)",
];
return [String(value), String(name)];
}}
labelStyle={{
fontWeight: "bold",
color: "#0f172a",
marginBottom: "4px",
fontSize: "11px",
}}
/>
{view === "saas" ? (
<>
<Area
type="monotone"
dataKey="saasCost"
stroke="#ef4444"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorCostSaas)"
animationDuration={1500}
/>
<Area
type="monotone"
dataKey="saasAsset"
stroke="#cbd5e1"
strokeWidth={2}
fillOpacity={0}
animationDuration={1500}
/>
</>
) : (
<>
<Area
type="monotone"
dataKey="customAsset"
stroke="#4f46e5"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorAsset)"
animationDuration={1500}
/>
<Area
type="monotone"
dataKey="customCost"
stroke="#64748b"
strokeWidth={2}
strokeDasharray="5 5"
fillOpacity={1}
fill="url(#colorCostCustom)"
animationDuration={1500}
/>
</>
)}
</AreaChart>
</ResponsiveContainer>
</div>
{/* Summary Metrics */}
<div className="grid grid-cols-2 gap-4 mt-8">
<div
className={`p-4 rounded-xl border ${view === "saas" ? "bg-red-50 border-red-100" : "bg-slate-50 border-slate-200"}`}
>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
Total Spend (Jahr 5)
</div>
<div
className={`text-2xl font-bold flex items-center gap-2 ${view === "saas" ? "text-red-600" : "text-slate-700"}`}
>
{view === "saas" ? "250.000 €" : "200.000 €"}
{view === "saas" && (
<TrendingUp size={16} className="text-red-500" />
)}
</div>
<div className="text-xs text-slate-500 mt-1">
Sunk Costs (Geldabfluss)
</div>
</div>
<div
className={`p-4 rounded-xl border ${view === "saas" ? "bg-slate-50 border-slate-200" : "bg-indigo-50 border-indigo-100"}`}
>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
Bilanzierbarer Wert (IP)
</div>
<div
className={`text-2xl font-bold flex items-center gap-2 ${view === "saas" ? "text-slate-400" : "text-indigo-600"}`}
>
{view === "saas" ? "0 €" : "85.000 €"}
{view === "custom" && (
<PiggyBank size={18} className="text-indigo-500" />
)}
</div>
<div className="text-xs text-slate-500 mt-1">
Erhöht den Unternehmenswert
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Reveal>
);
}
+1
View File
@@ -2,6 +2,7 @@
"extends": "@mintel/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"strict": true,
"paths": {
"@/*": ["./*"],
"@payload-config": ["./payload.config.ts"]