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
|
# 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).
|
App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first).
|
||||||
|
|
||||||
## Commands (run from repo root)
|
## Commands (run from repo root)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default async function BlogPostPage({
|
|||||||
<Reveal delay={0.4} width="100%">
|
<Reveal delay={0.4} width="100%">
|
||||||
{post.tags && post.tags.length > 0 && (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||||
{post.tags?.map((tag) => (
|
{post.tags?.map((tag: string) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
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"
|
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">
|
<div className="article-content max-w-none">
|
||||||
<TableOfContents />
|
<TableOfContents />
|
||||||
<MDXContent code={post.mdxContent || ""} />
|
<MDXContent code={post.mdxContent || ""} />
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</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;
|
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)
|
// Listen to ALL network responses to catch broken assets (404/500)
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ interface MermaidProps {
|
|||||||
legendFontSize?: string;
|
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 }) {
|
constructor(props: { children: React.ReactNode }) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false };
|
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)
|
// Extract text from React children nodes (MDX parses multi-line content as React nodes)
|
||||||
const extractTextFromChildren = (node: React.ReactNode): string => {
|
const extractTextFromChildren = (node: React.ReactNode): string => {
|
||||||
if (typeof node === 'string') return node;
|
if (typeof node === "string") return node;
|
||||||
if (typeof node === 'number') return String(node);
|
if (typeof node === "number") return String(node);
|
||||||
if (Array.isArray(node)) return node.map(extractTextFromChildren).join('\n');
|
if (Array.isArray(node))
|
||||||
|
return node.map(extractTextFromChildren).join("\n");
|
||||||
if (React.isValidElement(node)) {
|
if (React.isValidElement(node)) {
|
||||||
const props = node.props as { children?: React.ReactNode };
|
const props = node.props as { children?: React.ReactNode };
|
||||||
if (props.children) {
|
if (props.children) {
|
||||||
return extractTextFromChildren(props.children);
|
return extractTextFromChildren(props.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawGraph = graph || extractTextFromChildren(children) || "";
|
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
|
// We need to unescape these back to real newlines for Mermaid to parse
|
||||||
const sanitizedGraph = rawGraph
|
const sanitizedGraph = rawGraph
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^`+|`+$/g, '')
|
.replace(/^`+|`+$/g, "")
|
||||||
.replace(/\\n/g, '\n')
|
.replace(/\\n/g, "\n")
|
||||||
.replace(/\\"/g, '"')
|
.replace(/\\"/g, '"')
|
||||||
.replace(/\\'/g, "'");
|
.replace(/\\'/g, "'");
|
||||||
|
|
||||||
@@ -100,13 +104,16 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
if (!sanitizedGraph || sanitizedGraph.length < 5) {
|
if (!sanitizedGraph || sanitizedGraph.length < 5) {
|
||||||
return (
|
return (
|
||||||
<div className="not-prose my-8 p-6 bg-slate-50 border border-slate-200 rounded-xl text-center">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
setId(generatedId);
|
||||||
}, [providedId]);
|
}, [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);
|
observer.observe(wrapperRef.current);
|
||||||
@@ -143,17 +150,17 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
flowchart: {
|
flowchart: {
|
||||||
useMaxWidth: true,
|
useMaxWidth: true,
|
||||||
htmlLabels: false,
|
htmlLabels: false,
|
||||||
curve: 'basis',
|
curve: "basis",
|
||||||
nodeSpacing: 50,
|
nodeSpacing: 50,
|
||||||
rankSpacing: 60,
|
rankSpacing: 60,
|
||||||
padding: 15
|
padding: 15,
|
||||||
},
|
},
|
||||||
sequence: {
|
sequence: {
|
||||||
useMaxWidth: true,
|
useMaxWidth: true,
|
||||||
showSequenceNumbers: false,
|
showSequenceNumbers: false,
|
||||||
height: 40,
|
height: 40,
|
||||||
actorMargin: 30,
|
actorMargin: 30,
|
||||||
messageMargin: 30
|
messageMargin: 30,
|
||||||
},
|
},
|
||||||
gantt: { useMaxWidth: true },
|
gantt: { useMaxWidth: true },
|
||||||
pie: { useMaxWidth: true },
|
pie: { useMaxWidth: true },
|
||||||
@@ -175,7 +182,8 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
clusterBkg: "#f8fafc",
|
clusterBkg: "#f8fafc",
|
||||||
clusterBorder: "#cbd5e1",
|
clusterBorder: "#cbd5e1",
|
||||||
edgeLabelBackground: "#ffffff",
|
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",
|
fontSize: fontSize || "18px",
|
||||||
nodeFontSize: nodeFontSize || fontSize || "18px",
|
nodeFontSize: nodeFontSize || fontSize || "18px",
|
||||||
labelFontSize: labelFontSize || fontSize || "16px",
|
labelFontSize: labelFontSize || fontSize || "16px",
|
||||||
@@ -216,15 +224,19 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
if (!mermaid.render) return;
|
if (!mermaid.render) return;
|
||||||
|
|
||||||
const uniqueSvgId = `${id}-svg-${Date.now()}`;
|
const uniqueSvgId = `${id}-svg-${Date.now()}`;
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement("div");
|
||||||
document.body.appendChild(tempDiv);
|
document.body.appendChild(tempDiv);
|
||||||
tempDiv.style.position = 'absolute';
|
tempDiv.style.position = "absolute";
|
||||||
tempDiv.style.left = '-9999px';
|
tempDiv.style.left = "-9999px";
|
||||||
tempDiv.style.visibility = 'hidden';
|
tempDiv.style.visibility = "hidden";
|
||||||
|
|
||||||
let rawSvg: string;
|
let rawSvg: string;
|
||||||
try {
|
try {
|
||||||
const result = await mermaid.render(uniqueSvgId, sanitizedGraph, tempDiv);
|
const result = await mermaid.render(
|
||||||
|
uniqueSvgId,
|
||||||
|
sanitizedGraph,
|
||||||
|
tempDiv,
|
||||||
|
);
|
||||||
rawSvg = result.svg;
|
rawSvg = result.svg;
|
||||||
} finally {
|
} finally {
|
||||||
if (document.body.contains(tempDiv)) {
|
if (document.body.contains(tempDiv)) {
|
||||||
@@ -233,11 +245,13 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scaledSvg = rawSvg;
|
let scaledSvg = rawSvg;
|
||||||
const svgStartEnd = rawSvg.indexOf('>');
|
const svgStartEnd = rawSvg.indexOf(">");
|
||||||
if (svgStartEnd > -1) {
|
if (svgStartEnd > -1) {
|
||||||
const svgTag = rawSvg.substring(0, svgStartEnd + 1);
|
const svgTag = rawSvg.substring(0, svgStartEnd + 1);
|
||||||
const rest = rawSvg.substring(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;
|
scaledSvg = newSvgTag + rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +264,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
setError("Diagramm konnte nicht geladen werden (Render-Fehler).");
|
setError("Diagramm konnte nicht geladen werden (Render-Fehler).");
|
||||||
setIsRendered(true);
|
setIsRendered(true);
|
||||||
} else {
|
} 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) {
|
if (wrapperRef.current && wrapperRef.current.clientWidth > 0) {
|
||||||
renderGraph();
|
renderGraph();
|
||||||
@@ -273,25 +287,41 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => resizeObserver.disconnect();
|
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;
|
if (!id) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Reveal direction="up" delay={0.1}>
|
<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="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="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="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">
|
<div className="w-full flex flex-col items-center justify-center p-6 md:p-8 lg:p-10 relative z-10">
|
||||||
|
|
||||||
{showShare && (
|
{showShare && (
|
||||||
<div className="absolute top-6 right-6 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -322,7 +352,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
id={id}
|
id={id}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
overflow: "visible"
|
overflow: "visible",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -332,7 +362,7 @@ const MermaidInternal: React.FC<MermaidProps> = ({
|
|||||||
) : svgContent ? (
|
) : svgContent ? (
|
||||||
<div dangerouslySetInnerHTML={{ __html: svgContent }} />
|
<div dangerouslySetInnerHTML={{ __html: svgContent }} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'none' }}>{sanitizedGraph}</div>
|
<div style={{ display: "none" }}>{sanitizedGraph}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,175 +1,357 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { ComponentShareButton } from '../ComponentShareButton';
|
import { ComponentShareButton } from "../ComponentShareButton";
|
||||||
import { Reveal } from '../Reveal';
|
import { Reveal } from "../Reveal";
|
||||||
import { ComposedChart, Line, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
import {
|
||||||
import { ShieldAlert, Rocket, Database, Lock } from 'lucide-react';
|
ComposedChart,
|
||||||
|
Line,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from "recharts";
|
||||||
|
import { ShieldAlert, Rocket, Database, Lock } from "lucide-react";
|
||||||
|
|
||||||
const archData = [
|
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 1",
|
||||||
{ year: 'Jahr 3', saasInvest: 155, saasIndep: 20, customInvest: 155, customIndep: 95 },
|
saasInvest: 50,
|
||||||
{ year: 'Jahr 4', saasInvest: 220, saasIndep: 10, customInvest: 170, customIndep: 100 },
|
saasIndep: 40,
|
||||||
{ year: 'Jahr 5', saasInvest: 290, saasIndep: 5, customInvest: 185, customIndep: 100 },
|
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 }) {
|
export function ArchitectureBuilder({
|
||||||
const [stack, setStack] = useState<'saas' | 'custom'>('custom');
|
className = "",
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [stack, setStack] = useState<"saas" | "custom">("custom");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Reveal direction="up" delay={0.1}>
|
<Reveal direction="up" delay={0.1}>
|
||||||
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
|
<div
|
||||||
<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'}`} />
|
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
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
|
id="sim-arch-builder"
|
||||||
<ComponentShareButton
|
className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 overflow-hidden flex flex-col"
|
||||||
targetId="sim-arch-builder"
|
>
|
||||||
title="Architektur: Investment vs. Unabhängigkeit"
|
<div
|
||||||
shareText="Sehen Sie sich den zeitlichen Verlauf an: Software-Miete vs. digitales Eigentum."
|
data-share-wrapper="true"
|
||||||
/>
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50"
|
||||||
</div>
|
>
|
||||||
|
<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="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="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="relative z-10">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<span className="text-2xl">🏗️</span>
|
<span className="text-2xl">🏗️</span>
|
||||||
<h3 className="font-bold text-slate-900 m-0 text-xl">Zeitlicher Verlauf: Investment vs. Unabhängigkeit</h3>
|
<h3 className="font-bold text-slate-900 m-0 text-xl">
|
||||||
</div>
|
Zeitlicher Verlauf: Investment vs. Unabhängigkeit
|
||||||
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
|
</h3>
|
||||||
Wie Plattform-Abhängigkeit (Lock-in) über die Jahre Ihre Skalierbarkeit zerstört, während ein Headless-System echte Souveränität aufbaut.
|
</div>
|
||||||
</p>
|
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
|
||||||
</div>
|
Wie Plattform-Abhängigkeit (Lock-in) über die Jahre Ihre
|
||||||
</div>
|
Skalierbarkeit zerstört, während ein Headless-System echte
|
||||||
|
Souveränität aufbaut.
|
||||||
<div className="flex flex-col">
|
</p>
|
||||||
{/* 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>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { ComponentShareButton } from '../ComponentShareButton';
|
import { ComponentShareButton } from "../ComponentShareButton";
|
||||||
import { Reveal } from '../Reveal';
|
import { Reveal } from "../Reveal";
|
||||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import {
|
||||||
import { Briefcase, Coins, PiggyBank, TrendingUp } from 'lucide-react';
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import { Briefcase, Coins, PiggyBank, TrendingUp } from "lucide-react";
|
||||||
|
|
||||||
const data = [
|
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 1",
|
||||||
{ year: 'Jahr 3', saasCost: 135000, saasAsset: 0, customCost: 160000, customAsset: 100000 },
|
saasCost: 40000,
|
||||||
{ year: 'Jahr 4', saasCost: 190000, saasAsset: 0, customCost: 180000, customAsset: 90000 },
|
saasAsset: 0,
|
||||||
{ year: 'Jahr 5', saasCost: 250000, saasAsset: 0, customCost: 200000, customAsset: 85000 },
|
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 }) {
|
export function DigitalAssetVisualizer({
|
||||||
const [view, setView] = useState<'saas' | 'custom'>('custom');
|
className = "",
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [view, setView] = useState<"saas" | "custom">("custom");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Reveal direction="up" delay={0.1}>
|
<Reveal direction="up" delay={0.1}>
|
||||||
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
|
<div
|
||||||
<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'}`} />
|
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
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
|
id="sim-digital-asset"
|
||||||
<ComponentShareButton
|
className="relative bg-white rounded-2xl border border-slate-200 shadow-xl shadow-slate-200/50 overflow-hidden flex flex-col"
|
||||||
targetId="sim-digital-asset"
|
>
|
||||||
title="Software: Ausgabe oder Investment?"
|
<div
|
||||||
shareText="Sehen Sie sich den finanziellen Verlauf von Software-Miete (SaaS) gegenüber einer Inhouse-Eigenentwicklung an:"
|
data-share-wrapper="true"
|
||||||
/>
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50"
|
||||||
</div>
|
>
|
||||||
|
<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="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="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="relative z-10">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<span className="text-2xl">💰</span>
|
<span className="text-2xl">💰</span>
|
||||||
<h3 className="font-bold text-slate-900 m-0 text-xl">Software: Ausgabe oder Investment?</h3>
|
<h3 className="font-bold text-slate-900 m-0 text-xl">
|
||||||
</div>
|
Software: Ausgabe oder Investment?
|
||||||
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
|
</h3>
|
||||||
Kumulierte Kosten im Vergleich zum bilanzierbaren Unternehmenswert über 5 Jahre.
|
</div>
|
||||||
</p>
|
<p className="text-sm text-slate-500 m-0 leading-relaxed max-w-2xl">
|
||||||
</div>
|
Kumulierte Kosten im Vergleich zum bilanzierbaren
|
||||||
</div>
|
Unternehmenswert über 5 Jahre.
|
||||||
|
</p>
|
||||||
<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>
|
</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",
|
"extends": "@mintel/tsconfig/nextjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./*"],
|
||||||
"@payload-config": ["./payload.config.ts"]
|
"@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