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:
@@ -1,6 +1,6 @@
|
||||
# mintel.me
|
||||
|
||||
Personal website of a senior freelance engineer. Next.js 16 (App Router) + Payload, pnpm monorepo.
|
||||
Personal website of a senior freelance engineer. Next.js 16 (App Router), pnpm monorepo.
|
||||
App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first).
|
||||
|
||||
## Commands (run from repo root)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "@mintel/tsconfig/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@payload-config": ["./payload.config.ts"]
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "./apps/web/payload.config.js";
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
console.log(`Found ${docs.length} posts. Checking status...`);
|
||||
|
||||
for (const doc of docs) {
|
||||
if (doc._status !== "published") {
|
||||
try {
|
||||
await payload.update({
|
||||
collection: "posts",
|
||||
id: doc.id,
|
||||
data: {
|
||||
_status: "published",
|
||||
},
|
||||
});
|
||||
console.log(`Updated "${doc.title}" to published.`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to update ${doc.title}:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Migration complete.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user