diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index 72774cd..b8258ce 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -1,12 +1,13 @@ import * as React from "react"; import { notFound } from "next/navigation"; import { blogPosts } from "../../../src/data/blogPosts"; -import { PageHeader } from "../../../src/components/PageHeader"; +import { BlogPostHeader } from "../../../src/components/blog/BlogPostHeader"; import { Section } from "../../../src/components/Section"; import { Reveal } from "../../../src/components/Reveal"; import { BlogPostClient } from "../../../src/components/BlogPostClient"; import { PostComponents } from "../../../src/components/blog/posts"; -import { Card } from "../../../src/components/Layout"; +import { TextSelectionShare } from "../../../src/components/TextSelectionShare"; +import { BlogPostStickyBar } from "../../../src/components/blog/BlogPostStickyBar"; export async function generateStaticParams() { return blogPosts.map((post) => ({ @@ -41,71 +42,50 @@ export default async function BlogPostPage({
-
+ {/* Sticky Progress Bar */} + +
-
+
- - {/* Decorative background grid inside the card */} -
- -
-
-
- - -
-
-
- - | - - {readingTime} min Lesezeit -
- - {slug.substring(0, 4).toUpperCase()}- - {Math.floor(Math.random() * 999)} - -
-
- - {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((tag, index) => ( - - #{tag} - - ))} -
- )} - - {PostContent ? ( - - ) : ( -
- Inhalt wird bald veröffentlicht... -
- )} + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag, index) => ( + + #{tag} + + ))}
- + )} + + {PostContent ? ( + + ) : ( +
+ Inhalt wird bald veröffentlicht... +
+ )}
+ +
); } diff --git a/apps/web/app/blog/page.tsx b/apps/web/app/blog/page.tsx index 31b822c..1f9927d 100644 --- a/apps/web/app/blog/page.tsx +++ b/apps/web/app/blog/page.tsx @@ -9,10 +9,15 @@ import { SectionHeader } from "../../src/components/SectionHeader"; import { Reveal } from "../../src/components/Reveal"; import { Section } from "../../src/components/Section"; import { AbstractCircuit, GradientMesh } from "../../src/components/Effects"; +import { Share2 } from "lucide-react"; +import { ShareModal } from "../../src/components/ShareModal"; +import { useAnalytics } from "../../src/components/analytics/useAnalytics"; export default function BlogPage() { const [searchQuery, setSearchQuery] = useState(""); const [activeTags, setActiveTags] = useState([]); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const { trackEvent } = useAnalytics(); // Memoize allPosts const allPosts = React.useMemo( @@ -78,21 +83,41 @@ export default function BlogPage() {
-
} - className="pb-32 pt-12 md:pt-20" - containerVariant="wide" - > -
- {/* Section Header & Filters - Centered & Compact */} -
- - + {/* Header Section */} +
+ +
+
+ +
+
+ Knowledge Base +
+
+ + +
+

+ Alle Artikel. +

+

+ Gedanken über Engineering, Design und die Architektur der + Zukunft. +

+
+
+
+
+
+ + {/* Sticky Filter Bar */} +
+
+
+ +
+ +
+
+
+ setIsShareModalOpen(false)} + url={typeof window !== "undefined" ? window.location.href : ""} + title="Mintel Knowledge Base - Alle Artikel" + /> + +
+
{/* Posts List (Vertical & Minimal) */}
{postsToShow.length === 0 ? ( @@ -123,7 +178,12 @@ export default function BlogPage() { ) : (
{postsToShow.map((post, i) => ( - + ))} diff --git a/apps/web/src/components/BlogPostClient.tsx b/apps/web/src/components/BlogPostClient.tsx index 340d3a2..9704d0e 100644 --- a/apps/web/src/components/BlogPostClient.tsx +++ b/apps/web/src/components/BlogPostClient.tsx @@ -1,131 +1,33 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import * as React from "react"; +import { useEffect } from "react"; interface BlogPostClientProps { readingTime: number; title: string; } -export const BlogPostClient: React.FC = ({ readingTime, title }) => { - const router = useRouter(); - const [isScrolled, setIsScrolled] = useState(false); - +export const BlogPostClient: React.FC = () => { useEffect(() => { const handleScroll = () => { - setIsScrolled(window.scrollY > 100); - // Update progress bar - const winScroll = document.body.scrollTop || document.documentElement.scrollTop; - const height = document.documentElement.scrollHeight - document.documentElement.clientHeight; - const scrolled = (winScroll / height); - const progressBar = document.querySelector('.reading-progress-bar') as HTMLElement; + const winScroll = + document.body.scrollTop || document.documentElement.scrollTop; + const height = + document.documentElement.scrollHeight - + document.documentElement.clientHeight; + const scrolled = winScroll / height; + const progressBar = document.querySelector( + ".reading-progress-bar", + ) as HTMLElement; if (progressBar) { progressBar.style.transform = `scaleX(${scrolled})`; } }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); }, []); - const handleBack = () => { - // Lovely exit animation - const content = document.getElementById('post-content'); - if (content) { - content.style.transition = 'opacity 0.5s ease-out, transform 0.5s ease-out'; - content.style.opacity = '0'; - content.style.transform = 'translateY(20px) scale(0.98)'; - } - - const topNav = document.getElementById('top-nav'); - if (topNav) { - topNav.style.transition = 'opacity 0.4s ease-out'; - topNav.style.opacity = '0'; - } - - const overlay = document.createElement('div'); - overlay.className = 'fixed inset-0 bg-gradient-to-br from-white via-slate-50 to-white z-[100] opacity-0 transition-opacity duration-500'; - document.body.appendChild(overlay); - - setTimeout(() => { - overlay.style.opacity = '1'; - }, 100); - - setTimeout(() => { - router.push('/blog?from=post'); - }, 500); - }; - - const handleShare = async () => { - const url = window.location.href; - if (navigator.share) { - try { - await navigator.share({ title, url }); - } catch (err) { - console.error('Share failed:', err); - } - } else { - // Fallback: copy to clipboard - try { - await navigator.clipboard.writeText(url); - alert('Link copied to clipboard!'); - } catch (err) { - console.error('Copy failed:', err); - } - } - }; - - return ( - <> -
- - -
- -
- - ); + return
; }; diff --git a/apps/web/src/components/DiagramGantt.tsx b/apps/web/src/components/DiagramGantt.tsx new file mode 100644 index 0000000..05ee88e --- /dev/null +++ b/apps/web/src/components/DiagramGantt.tsx @@ -0,0 +1,50 @@ +"use client"; + +import React from "react"; +import { Mermaid } from "./Mermaid"; + +interface GanttTask { + id: string; + name: string; + start: string; + duration: string; + dependencies?: string[]; +} + +interface DiagramGanttProps { + tasks: GanttTask[]; + title?: string; + caption?: string; + id?: string; + showShare?: boolean; +} + +export const DiagramGantt: React.FC = ({ + tasks, + title, + caption, + id, + showShare = true, +}) => { + const ganttGraph = `gantt + dateFormat YYYY-MM-DD +${tasks + .map((task) => { + const deps = task.dependencies?.length + ? `, after ${task.dependencies.join(" ")}` + : ""; + return ` ${task.name} :${task.id}, ${task.start}, ${task.duration}${deps}`; + }) + .join("\n")}`; + + return ( +
+ + {caption && ( +

+ {caption} +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/DiagramPie.tsx b/apps/web/src/components/DiagramPie.tsx new file mode 100644 index 0000000..20f362a --- /dev/null +++ b/apps/web/src/components/DiagramPie.tsx @@ -0,0 +1,39 @@ +"use client"; + +import React from "react"; +import { Mermaid } from "./Mermaid"; + +interface PieSlice { + label: string; + value: number; +} + +interface DiagramPieProps { + data: PieSlice[]; + title?: string; + caption?: string; + id?: string; + showShare?: boolean; +} + +export const DiagramPie: React.FC = ({ + data, + title, + caption, + id, + showShare = true, +}) => { + const pieGraph = `pie +${data.map((slice) => ` "${slice.label}" : ${slice.value}`).join("\n")}`; + + return ( +
+ + {caption && ( +

+ {caption} +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/DiagramSequence.tsx b/apps/web/src/components/DiagramSequence.tsx new file mode 100644 index 0000000..263505a --- /dev/null +++ b/apps/web/src/components/DiagramSequence.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Mermaid } from "./Mermaid"; + +interface SequenceMessage { + from: string; + to: string; + message: string; + type?: "solid" | "dotted" | "async"; +} + +interface DiagramSequenceProps { + participants: string[]; + messages: SequenceMessage[]; + title?: string; + caption?: string; + id?: string; + showShare?: boolean; +} + +export const DiagramSequence: React.FC = ({ + participants, + messages, + title, + caption, + id, + showShare = true, +}) => { + const getArrow = (type?: string) => { + switch (type) { + case "dotted": + return "-->"; + case "async": + return "->>"; + default: + return "->"; + } + }; + + const sequenceGraph = `sequenceDiagram +${participants.map((p) => ` participant ${p}`).join("\n")} +${messages.map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join("\n")}`; + + return ( +
+ + {caption && ( +

+ {caption} +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/DiagramShareButton.tsx b/apps/web/src/components/DiagramShareButton.tsx new file mode 100644 index 0000000..585cac2 --- /dev/null +++ b/apps/web/src/components/DiagramShareButton.tsx @@ -0,0 +1,104 @@ +"use client"; + +import React, { useState } from "react"; +import { Share2 } from "lucide-react"; +import { ShareModal } from "./ShareModal"; +import { useAnalytics } from "./analytics/useAnalytics"; + +interface DiagramShareButtonProps { + diagramId: string; + title?: string; + svgContent?: string; +} + +export const DiagramShareButton: React.FC = ({ + diagramId, + title = "Diagram", + svgContent, +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const { trackEvent } = useAnalytics(); + + const currentUrl = + typeof window !== "undefined" + ? `${window.location.origin}${window.location.pathname}#${diagramId}` + : ""; + + // Convert SVG to PNG for sharing + const generateDiagramImage = async (): Promise => { + if (!svgContent) return undefined; + + try { + // Create a canvas to render the SVG + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return undefined; + + // Create an image from the SVG + const img = new Image(); + const svgBlob = new Blob([svgContent], { + type: "image/svg+xml;charset=utf-8", + }); + const url = URL.createObjectURL(svgBlob); + + return new Promise((resolve) => { + img.onload = () => { + // Set canvas size to match image + canvas.width = img.width * 2; // 2x for better quality + canvas.height = img.height * 2; + + // Fill with white background + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw the image + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + // Convert to data URL + const dataUrl = canvas.toDataURL("image/png"); + URL.revokeObjectURL(url); + resolve(dataUrl); + }; + + img.onerror = () => { + URL.revokeObjectURL(url); + resolve(undefined); + }; + + img.src = url; + }); + } catch (err) { + console.error("Failed to generate diagram image:", err); + return undefined; + } + }; + + const handleOpenModal = async () => { + setIsModalOpen(true); + trackEvent("diagram_share_opened", { + diagram_id: diagramId, + diagram_title: title, + }); + }; + + return ( + <> + + + setIsModalOpen(false)} + url={currentUrl} + title={title} + diagramImage={svgContent} + /> + + ); +}; diff --git a/apps/web/src/components/DiagramState.tsx b/apps/web/src/components/DiagramState.tsx new file mode 100644 index 0000000..766aa1d --- /dev/null +++ b/apps/web/src/components/DiagramState.tsx @@ -0,0 +1,53 @@ +"use client"; + +import React from "react"; +import { Mermaid } from "./Mermaid"; + +interface StateTransition { + from: string; + to: string; + label?: string; +} + +interface DiagramStateProps { + states: string[]; + transitions: StateTransition[]; + initialState?: string; + finalStates?: string[]; + title?: string; + caption?: string; + id?: string; + showShare?: boolean; +} + +export const DiagramState: React.FC = ({ + states, + transitions, + initialState, + finalStates = [], + title, + caption, + id, + showShare = true, +}) => { + const stateGraph = `stateDiagram-v2 +${initialState ? ` [*] --> ${initialState}` : ""} +${transitions + .map((t) => { + const label = t.label ? ` : ${t.label}` : ""; + return ` ${t.from} --> ${t.to}${label}`; + }) + .join("\n")} +${finalStates.map((s) => ` ${s} --> [*]`).join("\n")}`; + + return ( +
+ + {caption && ( +

+ {caption} +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/DiagramTimeline.tsx b/apps/web/src/components/DiagramTimeline.tsx new file mode 100644 index 0000000..220f148 --- /dev/null +++ b/apps/web/src/components/DiagramTimeline.tsx @@ -0,0 +1,45 @@ +"use client"; + +import React from "react"; +import { Mermaid } from "./Mermaid"; + +interface TimelineEvent { + year: string; + title: string; +} + +interface DiagramTimelineProps { + events: TimelineEvent[]; + title?: string; + caption?: string; + id?: string; + showShare?: boolean; +} + +export const DiagramTimeline: React.FC = ({ + events, + title, + caption, + id, + showShare = true, +}) => { + const timelineGraph = `timeline + title ${title || "Timeline"} +${events.map((event) => ` ${event.year} : ${event.title}`).join("\n")}`; + + return ( +
+ + {caption && ( +

+ {caption} +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/Embeds/index.ts b/apps/web/src/components/Embeds/index.ts index 62fd3cc..8ba1ee2 100644 --- a/apps/web/src/components/Embeds/index.ts +++ b/apps/web/src/components/Embeds/index.ts @@ -1,15 +1,23 @@ // Embed Components Index // Re-export for convenience -export { YouTubeEmbed } from '../YouTubeEmbed'; -export { TwitterEmbed } from '../TwitterEmbed'; -export { GenericEmbed } from '../GenericEmbed'; -export { Mermaid } from '../Mermaid'; +export { YouTubeEmbed } from "../YouTubeEmbed"; +export { TwitterEmbed } from "../TwitterEmbed"; +export { GenericEmbed } from "../GenericEmbed"; +export { Mermaid } from "../Mermaid"; +export { DiagramTimeline } from "../DiagramTimeline"; +export { DiagramSequence } from "../DiagramSequence"; +export { DiagramPie } from "../DiagramPie"; +export { DiagramGantt } from "../DiagramGantt"; +export { DiagramState } from "../DiagramState"; +export { DiagramShareButton } from "../DiagramShareButton"; // Type definitions for props export interface MermaidProps { graph: string; id?: string; + title?: string; + showShare?: boolean; } export interface YouTubeEmbedProps { @@ -17,19 +25,19 @@ export interface YouTubeEmbedProps { title?: string; className?: string; aspectRatio?: string; - style?: 'default' | 'minimal' | 'rounded' | 'flat'; + style?: "default" | "minimal" | "rounded" | "flat"; } export interface TwitterEmbedProps { tweetId: string; - theme?: 'light' | 'dark'; + theme?: "light" | "dark"; className?: string; - align?: 'left' | 'center' | 'right'; + align?: "left" | "center" | "right"; } export interface GenericEmbedProps { url: string; className?: string; maxWidth?: string; - type?: 'video' | 'article' | 'rich'; + type?: "video" | "article" | "rich"; } diff --git a/apps/web/src/components/Mermaid.tsx b/apps/web/src/components/Mermaid.tsx index 4b8c39c..8bb3a4f 100644 --- a/apps/web/src/components/Mermaid.tsx +++ b/apps/web/src/components/Mermaid.tsx @@ -2,15 +2,24 @@ import React, { useEffect, useRef, useState } from "react"; import mermaid from "mermaid"; +import { DiagramShareButton } from "./DiagramShareButton"; interface MermaidProps { graph: string; id?: string; + title?: string; + showShare?: boolean; } -export const Mermaid: React.FC = ({ graph, id: providedId }) => { +export const Mermaid: React.FC = ({ + graph, + id: providedId, + title, + showShare = false, +}) => { const [id, setId] = useState(null); const containerRef = useRef(null); + const [svgContent, setSvgContent] = useState(""); useEffect(() => { setId( @@ -23,15 +32,52 @@ export const Mermaid: React.FC = ({ graph, id: providedId }) => { useEffect(() => { mermaid.initialize({ startOnLoad: false, - theme: "default", + theme: "base", darkMode: false, themeVariables: { - fontFamily: "Inter, system-ui, sans-serif", - fontSize: "16px", + // Base colors - industrial slate/white palette primaryColor: "#ffffff", - nodeBorder: "#e2e8f0", + primaryTextColor: "#1e293b", + primaryBorderColor: "#cbd5e1", + lineColor: "#94a3b8", + secondaryColor: "#f8fafc", + tertiaryColor: "#f1f5f9", + + // Background colors + background: "#ffffff", mainBkg: "#ffffff", - lineColor: "#cbd5e1", + secondBkg: "#f8fafc", + tertiaryBkg: "#f1f5f9", + + // Text colors + textColor: "#1e293b", + labelTextColor: "#475569", + + // Node styling + nodeBorder: "#cbd5e1", + clusterBkg: "#f8fafc", + clusterBorder: "#cbd5e1", + + // Edge/line styling + edgeLabelBackground: "#ffffff", + + // Font + fontFamily: "Inter, system-ui, sans-serif", + fontSize: "14px", + + // Pie Chart Colors - High Contrast Industrial Palette + pie1: "#0f172a", // Deep Navy + pie2: "#334155", // Slate Blue + pie3: "#64748b", // Steel Gray + pie4: "#94a3b8", // Muted Steel + pie5: "#cbd5e1", // Concrete + pie6: "#1e293b", // Slate 800 + pie7: "#475569", // Slate 600 + pie8: "#000000", // Pure Black for accents + pie9: "#e2e8f0", // Light Concrete + pie10: "#020617", // Slate 950 + pie11: "#525252", // Neutral 600 + pie12: "#262626", // Neutral 800 }, securityLevel: "loose", }); @@ -41,6 +87,7 @@ export const Mermaid: React.FC = ({ graph, id: providedId }) => { try { const { svg } = await mermaid.render(`${id}-svg`, graph); containerRef.current.innerHTML = svg; + setSvgContent(svg); setIsRendered(true); } catch (err) { console.error("Mermaid rendering failed:", err); @@ -58,21 +105,41 @@ export const Mermaid: React.FC = ({ graph, id: providedId }) => { if (!id) return null; return ( -
-
-
- {error ? ( -
- {error} -
- ) : ( - graph - )} +
+
+ {title && ( +

+ {title} +

+ )} +
+
+ {error ? ( +
+ {error} +
+ ) : ( + graph + )} +
+ {showShare && id && ( +
+ +
+ )}
); diff --git a/apps/web/src/components/Modal.tsx b/apps/web/src/components/Modal.tsx index 137d7e1..3b8e36b 100644 --- a/apps/web/src/components/Modal.tsx +++ b/apps/web/src/components/Modal.tsx @@ -1,27 +1,45 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { X } from 'lucide-react'; +import * as React from "react"; +import { createPortal } from "react-dom"; +import { motion, AnimatePresence } from "framer-motion"; +import { X } from "lucide-react"; +import { cn } from "../utils/cn"; interface ModalProps { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode; + maxWidth?: string; } -export function Modal({ isOpen, onClose, title, children }: ModalProps) { +export function Modal({ + isOpen, + onClose, + title, + children, + maxWidth = "max-w-lg", +}: ModalProps) { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + // Close on escape key React.useEffect(() => { const handleEsc = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); + if (e.key === "Escape") onClose(); }; - window.addEventListener('keydown', handleEsc); - return () => window.removeEventListener('keydown', handleEsc); + window.addEventListener("keydown", handleEsc); + return () => window.removeEventListener("keydown", handleEsc); }, [onClose]); - return ( + if (!mounted) return null; + + return createPortal( {isOpen && ( <> @@ -30,31 +48,35 @@ export function Modal({ isOpen, onClose, title, children }: ModalProps) { animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} - className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100]" + className="fixed inset-0 bg-slate-900/60 backdrop-blur-md z-[9999]" /> -
+
-
-

{title}

+
+

+ {title} +

-
- {children} -
+
{children}
)} - + , + document.body, ); } diff --git a/apps/web/src/components/PageHeader.tsx b/apps/web/src/components/PageHeader.tsx index daea4df..d165e2f 100644 --- a/apps/web/src/components/PageHeader.tsx +++ b/apps/web/src/components/PageHeader.tsx @@ -1,7 +1,12 @@ +"use client"; + import * as React from "react"; import { Reveal } from "./Reveal"; import { H1, LeadText } from "./Typography"; import { cn } from "../utils/cn"; +import { Share2, Clock, Calendar } from "lucide-react"; +import { ShareModal } from "./ShareModal"; +import { useAnalytics } from "./analytics/useAnalytics"; interface PageHeaderProps { title: React.ReactNode; @@ -9,6 +14,10 @@ interface PageHeaderProps { backgroundSymbol?: string; className?: string; variant?: "default" | "blog"; + showShare?: boolean; + date?: string; + readingTime?: number; + slug?: string; } export const PageHeader: React.FC = ({ @@ -17,15 +26,31 @@ export const PageHeader: React.FC = ({ backgroundSymbol, className = "", variant = "default", + showShare = false, + date, + readingTime, + slug, }) => { + const [isShareModalOpen, setIsShareModalOpen] = React.useState(false); + const { trackEvent } = useAnalytics(); const isBlog = variant === "blog"; + const handleOpenShare = () => { + setIsShareModalOpen(true); + trackEvent("header_share_opened", { + title: typeof title === "string" ? title : "Blog Post", + slug: slug, + }); + }; + + const currentUrl = typeof window !== "undefined" ? window.location.href : ""; + return (
= ({ )}
- -

- {title} -

-
- - {description && ( - -

- {description} -

-
+ className={cn( + "space-y-4 md:space-y-12 relative", + isBlog && "max-w-7xl", )} + > +
+ +
+

+ {title} +

+ {showShare && isBlog && ( + + )} +
+
+ + {description && ( + +

+ {description} +

+
+ )} +
{isBlog && ( -
-
- - Technical ID:{" "} - {Math.random().toString(36).substring(7).toUpperCase()} - -
+ +
+
+ {date && ( +
+ + +
+ )} + {readingTime && ( +
+ + {readingTime} min Lesezeit +
+ )} +
+
+ + Technical ID: {slug?.substring(0, 4).toUpperCase() || "BLOG"}- + {Math.random().toString(36).substring(7).toUpperCase()} + + +
+
+
)}
+ + setIsShareModalOpen(false)} + url={currentUrl} + title={typeof title === "string" ? title : "Mintel Blog"} + />
); }; diff --git a/apps/web/src/components/Reveal.tsx b/apps/web/src/components/Reveal.tsx index 2650982..eb0009f 100644 --- a/apps/web/src/components/Reveal.tsx +++ b/apps/web/src/components/Reveal.tsx @@ -11,9 +11,14 @@ interface RevealProps { direction?: "up" | "down" | "left" | "right" | "none"; scale?: number; blur?: boolean; + viewport?: { + once?: boolean; + margin?: string; + amount?: "some" | "all" | number; + }; } -export const Reveal: React.FC = ({ +const Reveal: React.FC = ({ children, width = "100%", delay = 0.25, @@ -21,7 +26,22 @@ export const Reveal: React.FC = ({ direction = "up", scale = 0.98, blur = true, + viewport = { once: true, margin: "-10%" }, }) => { + const ref = useRef(null); + const isInView = useInView(ref, { + once: viewport.once ?? true, + margin: (viewport.margin as any) ?? "-10%", + amount: (viewport.amount as any) ?? 0.1, + }); + const mainControls = useAnimation(); + + useEffect(() => { + if (isInView) { + mainControls.start("visible"); + } + }, [isInView, mainControls]); + const variants: Variants = { hidden: { opacity: 0, @@ -41,6 +61,7 @@ export const Reveal: React.FC = ({ return (
= ({ = ({
); }; + +export { Reveal }; diff --git a/apps/web/src/components/ShareModal.tsx b/apps/web/src/components/ShareModal.tsx index 7f9a052..d8ecad7 100644 --- a/apps/web/src/components/ShareModal.tsx +++ b/apps/web/src/components/ShareModal.tsx @@ -1,19 +1,68 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { Modal } from './Modal'; -import { Copy, Check, Share2 } from 'lucide-react'; -import { useState } from 'react'; +import * as React from "react"; +import { Modal } from "./Modal"; +import { Copy, Check, Share2, Download } from "lucide-react"; +import { useState, useEffect } from "react"; interface ShareModalProps { isOpen: boolean; onClose: () => void; url: string; qrCodeData?: string; + title?: string; + diagramImage?: string; } -export function ShareModal({ isOpen, onClose, url, qrCodeData }: ShareModalProps) { +export function ShareModal({ + isOpen, + onClose, + url, + qrCodeData, + title, + diagramImage, +}: ShareModalProps) { const [copied, setCopied] = useState(false); + const [imagePreview, setImagePreview] = useState(null); + const [timestamp] = useState( + new Date().toLocaleTimeString("de-DE", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + ); + + useEffect(() => { + if (diagramImage && isOpen) { + // Convert SVG to PNG for preview with higher resolution (3x) + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const img = new Image(); + const svgBlob = new Blob([diagramImage], { + type: "image/svg+xml;charset=utf-8", + }); + const svgUrl = URL.createObjectURL(svgBlob); + + img.onload = () => { + const scale = 3; // 3x scaling for sharpness + canvas.width = img.width * scale; + canvas.height = img.height * scale; + + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw image with scaling + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + setImagePreview(canvas.toDataURL("image/png")); + URL.revokeObjectURL(svgUrl); + }; + + img.src = svgUrl; + } + }, [diagramImage, isOpen]); const handleCopy = () => { navigator.clipboard.writeText(url); @@ -24,56 +73,236 @@ export function ShareModal({ isOpen, onClose, url, qrCodeData }: ShareModalProps const handleNativeShare = async () => { if (navigator.share) { try { - await navigator.share({ - title: 'Meine Projekt-Konfiguration', - url: url - }); + const shareData: ShareData = { + title: title || "Mintel Diagramm", + url: url, + }; + + // If we have a diagram image, try to include it as a file + if (imagePreview) { + try { + // Convert base64 preview back to a File object + const response = await fetch(imagePreview); + const blob = await response.blob(); + const file = new File( + [blob], + `${title?.replace(/\s+/g, "-").toLowerCase() || "diagram"}.png`, + { type: "image/png" }, + ); + + // Check if sharing files is supported + if (navigator.canShare && navigator.canShare({ files: [file] })) { + shareData.files = [file]; + } + } catch (fileError) { + console.error("Could not prepare file for sharing", fileError); + } + } + + await navigator.share(shareData); } catch (e) { console.error("Share failed", e); } } }; - return ( - -
-

- Speichern Sie diesen Link, um Ihre Konfiguration später fortzusetzen oder teilen Sie ihn mit anderen. -

+ const handleDownloadImage = () => { + if (!imagePreview) return; - {qrCodeData && ( -
- QR Code -

QR-Code scannen

+ const link = document.createElement("a"); + link.download = `${title?.replace(/\s+/g, "-").toLowerCase() || "diagram"}.png`; + link.href = imagePreview; + link.click(); + }; + + const handleShareX = () => { + const text = encodeURIComponent(title || "Mintel Diagramm"); + const shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(url)}`; + window.open(shareUrl, "_blank", "width=550,height=420"); + }; + + const handleShareLinkedIn = () => { + const shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`; + window.open(shareUrl, "_blank", "width=550,height=420"); + }; + + const modalTitle = diagramImage + ? "Diagramm teilen" + : title + ? "Artikel teilen" + : "Konfiguration teilen"; + + return ( + +
+ {imagePreview ? ( +
+ {/* Social Post Preview Section */} +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Industrial Blueprint Preview */} +
+
+ +
+ {title +
+ +
+
+ + SYS:{" "} + {Math.random() + .toString(36) + .substring(7) + .toUpperCase()} + + + MINTEL.ME +
+ {timestamp} +
+
+
+
+
+ +

+ + Diagramm Vorschau + +

+
+ ) : ( + title && ( +
+ +

+ "{title}" +

+
+ ) + )} + + {!diagramImage && qrCodeData && ( +
+ QR Code +

+ QR-Code scannen +

)}
-
- - +
+
+
+ + +
- {typeof navigator !== 'undefined' && !!navigator.share && ( - - )} +
+
+ + + + + {typeof navigator !== "undefined" && !!navigator.share && ( + + )} +
+ + {imagePreview && ( + + )} +
diff --git a/apps/web/src/components/TextSelectionShare.tsx b/apps/web/src/components/TextSelectionShare.tsx new file mode 100644 index 0000000..f8e2ab9 --- /dev/null +++ b/apps/web/src/components/TextSelectionShare.tsx @@ -0,0 +1,184 @@ +"use client"; + +import React, { useEffect, useState, useRef } from "react"; +import { Share2, Copy, Check } from "lucide-react"; +import { useAnalytics } from "./analytics/useAnalytics"; +import { motion, AnimatePresence } from "framer-motion"; + +export function TextSelectionShare() { + const [isVisible, setIsVisible] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [selectedText, setSelectedText] = useState(""); + const [copied, setCopied] = useState(false); + const menuRef = useRef(null); + const { trackEvent } = useAnalytics(); + const selectionTimeoutRef = useRef(); + + useEffect(() => { + const handleSelection = () => { + // Clear any pending timeout + if (selectionTimeoutRef.current) { + clearTimeout(selectionTimeoutRef.current); + } + + // Small delay to ensure selection is complete + selectionTimeoutRef.current = setTimeout(() => { + const selection = window.getSelection(); + const text = selection?.toString().trim(); + + if (text && text.length > 10) { + const range = selection?.getRangeAt(0); + const rect = range?.getBoundingClientRect(); + + if (rect) { + // Position menu above selection, centered + setPosition({ + x: rect.left + rect.width / 2, + y: rect.top + window.scrollY - 10, + }); + setSelectedText(text); + setIsVisible(true); + } + } else { + setIsVisible(false); + setCopied(false); + } + }, 100); + }; + + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + const selection = window.getSelection(); + if (!selection?.toString()) { + setIsVisible(false); + setCopied(false); + } + } + }; + + // Listen to both mouseup and selectionchange for better reliability + document.addEventListener("mouseup", handleSelection); + document.addEventListener("selectionchange", handleSelection); + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mouseup", handleSelection); + document.removeEventListener("selectionchange", handleSelection); + document.removeEventListener("mousedown", handleClickOutside); + if (selectionTimeoutRef.current) { + clearTimeout(selectionTimeoutRef.current); + } + }; + }, []); + + const handleCopy = async () => { + const url = window.location.href; + const shareText = `"${selectedText}"\n\n${url}`; + + try { + await navigator.clipboard.writeText(shareText); + setCopied(true); + trackEvent("text_selection_copied", { + text_length: selectedText.length, + url: url, + }); + setTimeout(() => { + setIsVisible(false); + setCopied(false); + }, 1500); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + const handleShareX = () => { + const url = window.location.href; + const text = encodeURIComponent(`"${selectedText}"\n\n`); + const shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(url)}`; + + window.open(shareUrl, "_blank", "width=550,height=420"); + trackEvent("text_selection_shared_x", { + text_length: selectedText.length, + url: url, + }); + setIsVisible(false); + }; + + const handleShareLinkedIn = () => { + const url = window.location.href; + const shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`; + + window.open(shareUrl, "_blank", "width=550,height=420"); + trackEvent("text_selection_shared_linkedin", { + text_length: selectedText.length, + url: url, + }); + setIsVisible(false); + }; + + return ( + + {isVisible && ( + +
+ + +
+ + + + +
+ {/* Arrow */} +
+ + )} + + ); +} diff --git a/apps/web/src/components/blog/BlogPostHeader.tsx b/apps/web/src/components/blog/BlogPostHeader.tsx new file mode 100644 index 0000000..a9a32a7 --- /dev/null +++ b/apps/web/src/components/blog/BlogPostHeader.tsx @@ -0,0 +1,79 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { Reveal } from "../Reveal"; +import { Clock, Calendar, ArrowLeft } from "lucide-react"; + +interface BlogPostHeaderProps { + title: string; + description: string; + date: string; + readingTime: number; + slug: string; +} + +export const BlogPostHeader: React.FC = ({ + title, + description, + date, + readingTime, + slug, +}) => { + return ( +
+
+ + + + Zurück zur Übersicht + +
+

+ {title} +

+

+ {description} +

+
+
+ + +
+
+
+ + +
+
+ + {readingTime} min Lesezeit +
+
+ +
+ + {slug?.substring(0, 4).toUpperCase() || "BLOG"}- + {slug + ? slug + .split("") + .reduce((acc, char) => acc + char.charCodeAt(0), 0) + .toString(16) + .toUpperCase() + .padStart(4, "0") + : "0000"} + + +
+
+
+
+
+ ); +}; diff --git a/apps/web/src/components/blog/BlogPostStickyBar.tsx b/apps/web/src/components/blog/BlogPostStickyBar.tsx new file mode 100644 index 0000000..d48a068 --- /dev/null +++ b/apps/web/src/components/blog/BlogPostStickyBar.tsx @@ -0,0 +1,99 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { Share2, ArrowLeft, ArrowUp } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ShareModal } from "../ShareModal"; +import { useAnalytics } from "../analytics/useAnalytics"; + +interface BlogPostStickyBarProps { + title: string; + url: string; +} + +export function BlogPostStickyBar({ title, url }: BlogPostStickyBarProps) { + const [isVisible, setIsVisible] = React.useState(false); + const [isShareModalOpen, setIsShareModalOpen] = React.useState(false); + const { trackEvent } = useAnalytics(); + + React.useEffect(() => { + const handleScroll = () => { + // Show start appearing after scrolling past the header area (approx 600px) + const show = window.scrollY > 600; + setIsVisible(show); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( + <> + + {isVisible && ( + +
+ {/* Left Side - Absolute Positioned */} +
+ +
+ +
+ Blog + +
+ + {/* Center Title - Guaranteed dead center */} +
+ + {title} + +
+ + {/* Right Side - Absolute Positioned */} +
+ + + +
+
+
+ )} +
+ + setIsShareModalOpen(false)} + url={url} + title={title} + /> + + ); +} diff --git a/apps/web/src/components/blog/posts/Group1/AgencySlowdown.tsx b/apps/web/src/components/blog/posts/Group1/AgencySlowdown.tsx index 7e461b0..11b4e86 100644 --- a/apps/web/src/components/blog/posts/Group1/AgencySlowdown.tsx +++ b/apps/web/src/components/blog/posts/Group1/AgencySlowdown.tsx @@ -3,6 +3,7 @@ import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; import { Mermaid } from "../../../Mermaid"; +import { DiagramGantt } from "../../../DiagramGantt"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -63,6 +64,9 @@ export const AgencySlowdown: React.FC = () => ( style AM fill:#fca5a5,stroke:#333 style PM fill:#fca5a5,stroke:#333 style Ticket fill:#fca5a5,stroke:#333`} + id="agency-bottleneck" + title="Agentur-Hierarchie Flaschenhals" + showShare={true} />

Die traditionelle 'Stille Post': Jede Schnittstelle kostet Sie Zeit, @@ -152,6 +156,27 @@ export const AgencySlowdown: React.FC = () => ( + +

Für wen ich die Bremse löse

Mein Angebot richtet sich an Gründer und Entscheider, die{" "} diff --git a/apps/web/src/components/blog/posts/Group1/PageSpeedFails.tsx b/apps/web/src/components/blog/posts/Group1/PageSpeedFails.tsx index 6b667da..e41c930 100644 --- a/apps/web/src/components/blog/posts/Group1/PageSpeedFails.tsx +++ b/apps/web/src/components/blog/posts/Group1/PageSpeedFails.tsx @@ -3,6 +3,7 @@ import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; import { Mermaid } from "../../../Mermaid"; +import { DiagramPie } from "../../../DiagramPie"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -92,6 +93,9 @@ export const PageSpeedFails: React.FC = () => ( style B fill:#fca5a5,stroke:#333 style F fill:#fca5a5,stroke:#333 style G fill:#fca5a5,stroke:#333`} + id="legacy-loading-bottleneck" + title="Legacy System Ladezeit-Flaschenhals" + showShare={true} />

Der Flaschenhals der Standard-Systeme: Rechenzeit am Server raubt Ihnen @@ -134,6 +138,20 @@ export const PageSpeedFails: React.FC = () => ( + +

Der wirtschaftliche Case

Baukästen wirken "auf den ersten Blick" günstiger. Doch das ist eine diff --git a/apps/web/src/components/blog/posts/Group1/SlowLoadingDebt.tsx b/apps/web/src/components/blog/posts/Group1/SlowLoadingDebt.tsx index ec409f4..e469ef7 100644 --- a/apps/web/src/components/blog/posts/Group1/SlowLoadingDebt.tsx +++ b/apps/web/src/components/blog/posts/Group1/SlowLoadingDebt.tsx @@ -68,6 +68,9 @@ export const SlowLoadingDebt: React.FC = () => ( Loss --> Debt["Explodierende Akquisekosten"] style Loss fill:#ef4444,color:#fff style Debt fill:#ef4444,color:#fff`} + id="loading-debt-cycle" + title="Ladezeit Teufelskreis" + showShare={true} />

Der fatale Teufelskreis der Ladezeit: Technische Schulden führen zu diff --git a/apps/web/src/components/blog/posts/Group1/WebsiteStability.tsx b/apps/web/src/components/blog/posts/Group1/WebsiteStability.tsx index 5c597fc..1b20b00 100644 --- a/apps/web/src/components/blog/posts/Group1/WebsiteStability.tsx +++ b/apps/web/src/components/blog/posts/Group1/WebsiteStability.tsx @@ -3,6 +3,7 @@ import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; import { Mermaid } from "../../../Mermaid"; +import { DiagramState } from "../../../DiagramState"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -54,6 +55,9 @@ export const WebsiteStability: React.FC = () => ( style Stable fill:#4ade80,stroke:#333 style Alert fill:#ef4444,color:#fff style Deploy fill:#4ade80,stroke:#333`} + id="deployment-safety-net" + title="Deployment Sicherheitsnetz" + showShare={true} />

Mein defensives Sicherheitsnetz: Keine Änderung erreicht den Nutzer, @@ -107,6 +111,24 @@ export const WebsiteStability: React.FC = () => ( + +

( Slow --> Bounce["Besucher springen ab"] style Slow fill:#fca5a5,stroke:#333 style Bounce fill:#ef4444,color:#fff`} + id="plugin-dependency-trap" + title="Plugin Dependency Trap" + showShare={true} />

Das Plugin-Paradoxon: Jedes 'Feature' erhöht die Wahrscheinlichkeit diff --git a/apps/web/src/components/blog/posts/Group2/CookieFreeDesign.tsx b/apps/web/src/components/blog/posts/Group2/CookieFreeDesign.tsx index 7d576fe..5bc17e9 100644 --- a/apps/web/src/components/blog/posts/Group2/CookieFreeDesign.tsx +++ b/apps/web/src/components/blog/posts/Group2/CookieFreeDesign.tsx @@ -62,6 +62,9 @@ export const CookieFreeDesign: React.FC = () => ( NoBanner --> Experience["Sofortige Experience & Vertrauen"] style NoBanner fill:#4ade80,stroke:#333 style Experience fill:#4ade80,stroke:#333`} + id="cookie-free-architecture" + title="Cookie-freie Architektur" + showShare={true} />

Privacy by Design: Wenn die Architektur den Schutz bereits garantiert, diff --git a/apps/web/src/components/blog/posts/Group2/GDPRSystem.tsx b/apps/web/src/components/blog/posts/Group2/GDPRSystem.tsx index d82228a..448c763 100644 --- a/apps/web/src/components/blog/posts/Group2/GDPRSystem.tsx +++ b/apps/web/src/components/blog/posts/Group2/GDPRSystem.tsx @@ -58,6 +58,9 @@ export const GDPRSystem: React.FC = () => ( style Safe fill:#4ade80,stroke:#333 style Minimize fill:#4ade80,stroke:#333 style Encrypt fill:#4ade80,stroke:#333`} + id="gdpr-compliance-flow" + title="DSGVO Compliance System" + showShare={true} />

Der Kreislauf der systemischen Sicherheit: Jede Stufe schützt Ihre diff --git a/apps/web/src/components/blog/posts/Group2/LocalCloud.tsx b/apps/web/src/components/blog/posts/Group2/LocalCloud.tsx index 0472c28..e7bbf7b 100644 --- a/apps/web/src/components/blog/posts/Group2/LocalCloud.tsx +++ b/apps/web/src/components/blog/posts/Group2/LocalCloud.tsx @@ -53,6 +53,9 @@ export const LocalCloud: React.FC = () => ( Compliance --> Speed["Niedrige Latenz & Absolute Kontrolle"] style Local fill:#4ade80,stroke:#333 style Risk fill:#fca5a5,stroke:#333`} + id="local-cloud-hybrid" + title="Local Cloud Strategie" + showShare={true} />

Architektonische Entscheidung: Geopolitische Risiken minimieren durch diff --git a/apps/web/src/components/blog/posts/Group2/PrivacyAnalytics.tsx b/apps/web/src/components/blog/posts/Group2/PrivacyAnalytics.tsx index 11033c1..db48fe2 100644 --- a/apps/web/src/components/blog/posts/Group2/PrivacyAnalytics.tsx +++ b/apps/web/src/components/blog/posts/Group2/PrivacyAnalytics.tsx @@ -53,6 +53,9 @@ export const PrivacyAnalytics: React.FC = () => ( Zero --> Compliance["100% DSGVO & Banner-Frei"] style Insights fill:#4ade80,stroke:#333 style Compliance fill:#4ade80,stroke:#333`} + id="privacy-analytics-flow" + title="Privacy Analytics Workflow" + showShare={true} />

Ethisches Tracking: Wir gewinnen wertvolle Business-Insights, während diff --git a/apps/web/src/components/blog/posts/Group2/VendorLockIn.tsx b/apps/web/src/components/blog/posts/Group2/VendorLockIn.tsx index 218ef1e..ebf566b 100644 --- a/apps/web/src/components/blog/posts/Group2/VendorLockIn.tsx +++ b/apps/web/src/components/blog/posts/Group2/VendorLockIn.tsx @@ -3,6 +3,7 @@ import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; import { Mermaid } from "../../../Mermaid"; +import { DiagramState } from "../../../DiagramState"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -57,6 +58,9 @@ export const VendorLockIn: React.FC = () => ( Flex --> Evolution["Permanente Innovation"] style Open fill:#4ade80,stroke:#333 style Crisis fill:#fca5a5,stroke:#333`} + id="vendor-lockin-fork" + title="Vendor Lock-In vs. Offene Architektur" + showShare={true} />

Die Gabelung der digitalen Strategie: Wählen Sie Freiheit durch @@ -102,6 +106,24 @@ export const VendorLockIn: React.FC = () => ( + +

( Competitive --> Growth["Skalierung ohne Grenzen"] style Build fill:#4ade80,stroke:#333 style Growth fill:#4ade80,stroke:#333`} + id="build-vs-buy-decision" + title="Build vs. Buy Entscheidung" + showShare={true} />

Build vs. Buy: Investieren Sie in Ihr eigenes geistiges Eigentum statt @@ -104,6 +108,19 @@ export const BuildFirst: React.FC = () => ( Software-Miete ist ein Kostenblock, Software-Bau ist eine Investition. + + Exakter Prozess-Match: Das System passt sich Ihren diff --git a/apps/web/src/components/blog/posts/Group3/FixedPrice.tsx b/apps/web/src/components/blog/posts/Group3/FixedPrice.tsx index 4b23c73..126ecb2 100644 --- a/apps/web/src/components/blog/posts/Group3/FixedPrice.tsx +++ b/apps/web/src/components/blog/posts/Group3/FixedPrice.tsx @@ -3,6 +3,7 @@ import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; import { Mermaid } from "../../../Mermaid"; +import { DiagramGantt } from "../../../DiagramGantt"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -57,6 +58,9 @@ export const FixedPrice: React.FC = () => ( Safety --> Quality["Maximale Qualität durch Effizienz"] style Fixed fill:#4ade80,stroke:#333 style Quality fill:#4ade80,stroke:#333`} + id="fixed-price-model" + title="Festpreis vs. Stundensatz Modell" + showShare={true} />

Das Modell des Vertrauens: Fixe Budgets schaffen den Raum für @@ -109,6 +113,49 @@ export const FixedPrice: React.FC = () => ( + +

( Energy --> Profit["Geringere Hosting-Kosten"] style Profit fill:#4ade80,stroke:#333 style Impact fill:#4ade80,stroke:#333`} + id="green-it-efficiency" + title="Green IT Effizienz-Kreislauf" + showShare={true} />

Die grüne Rendite: Effizienz in der Software führt direkt zu @@ -104,6 +108,19 @@ export const GreenIT: React.FC = () => ( + +

( Update --> ROI style ROI fill:#4ade80,stroke:#333 style Decade fill:#4ade80,stroke:#333`} + id="technology-longevity" + title="Technologie Langlebigkeit" + showShare={true} />

Architektur der Langlebigkeit: Durch die Trennung von Logik und Trends diff --git a/apps/web/src/components/blog/posts/Group3/MaintenanceNoCMS.tsx b/apps/web/src/components/blog/posts/Group3/MaintenanceNoCMS.tsx index c3bd9dd..559ba70 100644 --- a/apps/web/src/components/blog/posts/Group3/MaintenanceNoCMS.tsx +++ b/apps/web/src/components/blog/posts/Group3/MaintenanceNoCMS.tsx @@ -56,6 +56,9 @@ export const MaintenanceNoCMS: React.FC = () => ( Speed --> Focus["Fokus auf Kunden & Strategie"] style Git fill:#4ade80,stroke:#333 style Focus fill:#4ade80,stroke:#333`} + id="maintenance-workflow" + title="Wartungs-Workflow Vergleich" + showShare={true} />

Der schlanke Workflow: Wir eliminieren die Datenbank-Ebene, um diff --git a/apps/web/src/components/blog/posts/Group4/CRMSync.tsx b/apps/web/src/components/blog/posts/Group4/CRMSync.tsx index 06f96de..89a2a3d 100644 --- a/apps/web/src/components/blog/posts/Group4/CRMSync.tsx +++ b/apps/web/src/components/blog/posts/Group4/CRMSync.tsx @@ -2,7 +2,7 @@ import React from "react"; import { H2, H3 } from "../../../ArticleHeading"; import { Paragraph, LeadParagraph } from "../../../ArticleParagraph"; import { IconList, IconListItem } from "../../../IconList"; -import { Mermaid } from "../../../Mermaid"; +import { DiagramSequence } from "../../../DiagramSequence"; import { Marker } from "../../../Marker"; import { ComparisonRow } from "../../../Landing/ComparisonRow"; @@ -47,20 +47,44 @@ export const CRMSync: React.FC = () => (

- Edge["Mintel Validation Layer"] - Edge --> Transform["Intelligente Daten-Aufbereitung"] - Transform --> CRM["CRM (Salesforce/HubSpot/etc.)"] - CRM --> Notify["Instat Sales-Benachrichtigung"] - CRM --> AutoResp["Personalisierte Auto-Antwort"] - style CRM fill:#4ade80,stroke:#333 - style Notify fill:#4ade80,stroke:#333`} + -

- Der automatisierte Lead-Fluss: Von der ersten Interaktion bis zum - CRM-Eintrag in Millisekunden – ohne menschliches Eingreifen. -

Echtzeit-Synchronität als Wettbewerbsvorteil

diff --git a/apps/web/src/components/blog/posts/Group4/CleanCode.tsx b/apps/web/src/components/blog/posts/Group4/CleanCode.tsx index e38fea7..0b86dc8 100644 --- a/apps/web/src/components/blog/posts/Group4/CleanCode.tsx +++ b/apps/web/src/components/blog/posts/Group4/CleanCode.tsx @@ -55,6 +55,9 @@ export const CleanCode: React.FC = () => ( Market --> Profit style Profit fill:#4ade80,stroke:#333 style Clean fill:#4ade80,stroke:#333`} + id="clean-code-architecture" + title="Clean Code Architektur" + showShare={true} />

Die Logik der Qualität: Sauberer Code zahlt sich durch sinkende diff --git a/apps/web/src/components/blog/posts/Group4/HostingOps.tsx b/apps/web/src/components/blog/posts/Group4/HostingOps.tsx index 0ecb4da..4756a73 100644 --- a/apps/web/src/components/blog/posts/Group4/HostingOps.tsx +++ b/apps/web/src/components/blog/posts/Group4/HostingOps.tsx @@ -54,6 +54,9 @@ export const HostingOps: React.FC = () => ( Global --> Failover["Automatisches Failover (Sicherheit)"] style Global fill:#4ade80,stroke:#333 style Failover fill:#4ade80,stroke:#333`} + id="cloud-native-operations" + title="Cloud-Native Operations" + showShare={true} />

Die Cloud-Native Architektur: Skalierung per Knopfdruck und diff --git a/apps/web/src/components/blog/posts/Group4/NoTemplates.tsx b/apps/web/src/components/blog/posts/Group4/NoTemplates.tsx index 81eb015..2207a6c 100644 --- a/apps/web/src/components/blog/posts/Group4/NoTemplates.tsx +++ b/apps/web/src/components/blog/posts/Group4/NoTemplates.tsx @@ -56,6 +56,9 @@ export const NoTemplates: React.FC = () => ( Distinct --> Authority["Marken-Autorität"] style Custom fill:#4ade80,stroke:#333 style Authority fill:#4ade80,stroke:#333`} + id="bespoke-vs-template" + title="Bespoke vs. Template Vergleich" + showShare={true} />

Bespoke vs. Template: Investieren Sie in ein digitales Unikat, das Ihre diff --git a/apps/web/src/components/blog/posts/Group4/ResponsiveDesign.tsx b/apps/web/src/components/blog/posts/Group4/ResponsiveDesign.tsx index 94fda43..88f92be 100644 --- a/apps/web/src/components/blog/posts/Group4/ResponsiveDesign.tsx +++ b/apps/web/src/components/blog/posts/Group4/ResponsiveDesign.tsx @@ -53,6 +53,9 @@ export const ResponsiveDesign: React.FC = () => ( Desktop --> UX style UX fill:#4ade80,stroke:#333 style Logic fill:#4ade80,stroke:#333`} + id="responsive-ux-strategy" + title="Responsive UX Strategie" + showShare={true} />

Plattformübergreifende Brillanz: Ein System, das sich nicht nur anpasst,