feat: complete MDX migration for blog, fix diagram fidelity and refactor styling architecture
This commit is contained in:
@@ -19,19 +19,19 @@ interface BlockquoteProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Blockquote: React.FC<BlockquoteProps> = ({ children, className = '' }) => (
|
||||
<blockquote className={`border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-xl md:text-2xl font-serif ${className}`}>
|
||||
export const ArticleBlockquote: React.FC<BlockquoteProps> = ({ children, className = '' }) => (
|
||||
<blockquote className={`not-prose border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-xl md:text-2xl font-serif ${className}`}>
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
|
||||
interface CodeBlockProps {
|
||||
code?: string;
|
||||
children?: React.ReactNode;
|
||||
language?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
code?: string;
|
||||
children?: React.ReactNode;
|
||||
language?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
// Language mapping for Prism.js
|
||||
@@ -70,54 +70,54 @@ const highlightCode = (code: string, language: string): { html: string; prismLan
|
||||
console.warn('Prism highlighting failed:', error);
|
||||
return { html: code.trim(), prismLanguage: 'text' };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
code,
|
||||
children,
|
||||
language = 'text',
|
||||
showLineNumbers = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const codeContent = code || (typeof children === 'string' ? children : String(children || '')).trim();
|
||||
const { html: highlightedCode, prismLanguage } = language !== 'text' ? highlightCode(codeContent, language) : { html: codeContent, prismLanguage: 'text' };
|
||||
const lines = codeContent.split('\n');
|
||||
code,
|
||||
children,
|
||||
language = 'text',
|
||||
showLineNumbers = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const codeContent = code || (typeof children === 'string' ? children : String(children || '')).trim();
|
||||
const { html: highlightedCode, prismLanguage } = language !== 'text' ? highlightCode(codeContent, language) : { html: codeContent, prismLanguage: 'text' };
|
||||
const lines = codeContent.split('\n');
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: syntaxHighlightingStyles }} />
|
||||
<div className="relative my-6">
|
||||
{language !== 'text' && (
|
||||
<div className="absolute top-3 right-3 text-[10px] font-bold uppercase tracking-widest bg-white text-slate-500 px-2 py-1 rounded-md z-10 border border-slate-100 shadow-sm">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className={`m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border border-slate-200 bg-white rounded-2xl ${className} ${showLineNumbers ? 'pl-12' : ''}`}
|
||||
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
|
||||
>
|
||||
{showLineNumbers ? (
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
|
||||
{lines.map((_, i) => (
|
||||
<div key={i} className="leading-relaxed">{i + 1}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pl-10">
|
||||
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: syntaxHighlightingStyles }} />
|
||||
<div className="relative my-6">
|
||||
{language !== 'text' && (
|
||||
<div className="absolute top-3 right-3 text-[10px] font-bold uppercase tracking-widest bg-white text-slate-500 px-2 py-1 rounded-md z-10 border border-slate-100 shadow-sm">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className={`not-prose m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border border-slate-200 bg-white rounded-2xl ${className} ${showLineNumbers ? 'pl-12' : ''}`}
|
||||
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
|
||||
>
|
||||
{showLineNumbers ? (
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
|
||||
{lines.map((_, i) => (
|
||||
<div key={i} className="leading-relaxed">{i + 1}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pl-10">
|
||||
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const InlineCode: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => (
|
||||
<code className={`bg-white text-slate-800 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] border border-slate-200 ${className}`}>
|
||||
<code className={`not-prose bg-white text-slate-800 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] border border-slate-200 ${className}`}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ interface HeadingProps {
|
||||
|
||||
export const H1: React.FC<HeadingProps> = ({ children, className = "" }) => (
|
||||
<h1
|
||||
className={`text-4xl md:text-5xl font-bold text-slate-900 mb-6 mt-8 leading-[1.1] tracking-tight ${className}`}
|
||||
className={`not-prose text-4xl md:text-5xl font-bold text-slate-900 mb-6 mt-8 leading-[1.1] tracking-tight ${className}`}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
@@ -15,7 +15,7 @@ export const H1: React.FC<HeadingProps> = ({ children, className = "" }) => (
|
||||
|
||||
export const H2: React.FC<HeadingProps> = ({ children, className = "" }) => (
|
||||
<h2
|
||||
className={`text-3xl md:text-4xl font-bold text-slate-900 mb-4 mt-10 leading-[1.2] tracking-tight ${className}`}
|
||||
className={`not-prose text-3xl md:text-4xl font-bold text-slate-900 mb-4 mt-10 leading-[1.2] tracking-tight ${className}`}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
@@ -23,7 +23,7 @@ export const H2: React.FC<HeadingProps> = ({ children, className = "" }) => (
|
||||
|
||||
export const H3: React.FC<HeadingProps> = ({ children, className = "" }) => (
|
||||
<h3
|
||||
className={`text-2xl md:text-3xl font-bold text-slate-900 mb-3 mt-8 leading-[1.3] tracking-tight ${className}`}
|
||||
className={`not-prose text-2xl md:text-3xl font-bold text-slate-900 mb-3 mt-8 leading-[1.3] tracking-tight ${className}`}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
|
||||
@@ -9,20 +9,20 @@ export const Paragraph: React.FC<ParagraphProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => (
|
||||
<p
|
||||
className={`text-slate-700 font-serif text-lg md:text-xl leading-[1.6] mb-4 ${className}`}
|
||||
<div
|
||||
className={`not-prose text-slate-700 font-serif text-lg md:text-xl leading-[1.6] mb-4 [&_p]:mb-4 last:[&_p]:mb-0 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LeadParagraph: React.FC<ParagraphProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => (
|
||||
<p
|
||||
className={`text-xl md:text-2xl text-slate-700 font-serif italic leading-snug mb-6 ${className}`}
|
||||
<div
|
||||
className={`not-prose text-xl md:text-2xl text-slate-700 font-serif italic leading-snug mb-6 [&_p]:mb-6 last:[&_p]:mb-0 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ interface DiagramGanttProps {
|
||||
caption?: string;
|
||||
id?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
}
|
||||
|
||||
export const DiagramGantt: React.FC<DiagramGanttProps> = ({
|
||||
@@ -25,25 +26,32 @@ export const DiagramGantt: React.FC<DiagramGanttProps> = ({
|
||||
caption,
|
||||
id,
|
||||
showShare = true,
|
||||
fontSize = "16px",
|
||||
}) => {
|
||||
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")}`;
|
||||
${(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 (
|
||||
<div className="my-12">
|
||||
<Mermaid graph={ganttGraph} id={id} title={title} showShare={showShare} />
|
||||
<Mermaid
|
||||
graph={ganttGraph}
|
||||
id={id}
|
||||
title={title}
|
||||
showShare={showShare}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{caption && (
|
||||
<p className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ interface DiagramPieProps {
|
||||
caption?: string;
|
||||
id?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
}
|
||||
|
||||
export const DiagramPie: React.FC<DiagramPieProps> = ({
|
||||
@@ -22,17 +23,24 @@ export const DiagramPie: React.FC<DiagramPieProps> = ({
|
||||
caption,
|
||||
id,
|
||||
showShare = true,
|
||||
fontSize = "16px",
|
||||
}) => {
|
||||
const pieGraph = `pie
|
||||
${data.map((slice) => ` "${slice.label}" : ${slice.value}`).join("\n")}`;
|
||||
${(data || []).map((slice) => ` "${slice.label}" : ${slice.value}`).join("\n")}`;
|
||||
|
||||
return (
|
||||
<div className="my-12">
|
||||
<Mermaid graph={pieGraph} id={id} title={title} showShare={showShare} />
|
||||
<Mermaid
|
||||
graph={pieGraph}
|
||||
id={id}
|
||||
title={title}
|
||||
showShare={showShare}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{caption && (
|
||||
<p className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ interface DiagramSequenceProps {
|
||||
caption?: string;
|
||||
id?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
}
|
||||
|
||||
export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
|
||||
@@ -26,6 +27,7 @@ export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
|
||||
caption,
|
||||
id,
|
||||
showShare = true,
|
||||
fontSize = "16px",
|
||||
}) => {
|
||||
const getArrow = (type?: string) => {
|
||||
switch (type) {
|
||||
@@ -39,8 +41,8 @@ export const DiagramSequence: React.FC<DiagramSequenceProps> = ({
|
||||
};
|
||||
|
||||
const sequenceGraph = `sequenceDiagram
|
||||
${participants.map((p) => ` participant ${p}`).join("\n")}
|
||||
${messages.map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join("\n")}`;
|
||||
${(participants || []).map((p) => ` participant ${p}`).join("\n")}
|
||||
${(messages || []).map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).join("\n")}`;
|
||||
|
||||
return (
|
||||
<div className="my-12">
|
||||
@@ -49,11 +51,12 @@ ${messages.map((m) => ` ${m.from}${getArrow(m.type)}${m.to}: ${m.message}`).j
|
||||
id={id}
|
||||
title={title}
|
||||
showShare={showShare}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{caption && (
|
||||
<p className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ interface DiagramStateProps {
|
||||
caption?: string;
|
||||
id?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
}
|
||||
|
||||
export const DiagramState: React.FC<DiagramStateProps> = ({
|
||||
@@ -29,24 +30,31 @@ export const DiagramState: React.FC<DiagramStateProps> = ({
|
||||
caption,
|
||||
id,
|
||||
showShare = true,
|
||||
fontSize = "16px",
|
||||
}) => {
|
||||
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")}`;
|
||||
${(transitions || [])
|
||||
.map((t) => {
|
||||
const label = t.label ? ` : ${t.label}` : "";
|
||||
return ` ${t.from} --> ${t.to}${label}`;
|
||||
})
|
||||
.join("\n")}
|
||||
${(finalStates || []).map((s) => ` ${s} --> [*]`).join("\n")}`;
|
||||
|
||||
return (
|
||||
<div className="my-12">
|
||||
<Mermaid graph={stateGraph} id={id} title={title} showShare={showShare} />
|
||||
<Mermaid
|
||||
graph={stateGraph}
|
||||
id={id}
|
||||
title={title}
|
||||
showShare={showShare}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{caption && (
|
||||
<p className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ interface DiagramTimelineProps {
|
||||
caption?: string;
|
||||
id?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
}
|
||||
|
||||
export const DiagramTimeline: React.FC<DiagramTimelineProps> = ({
|
||||
@@ -22,10 +23,11 @@ export const DiagramTimeline: React.FC<DiagramTimelineProps> = ({
|
||||
caption,
|
||||
id,
|
||||
showShare = true,
|
||||
fontSize = "16px",
|
||||
}) => {
|
||||
const timelineGraph = `timeline
|
||||
title ${title || "Timeline"}
|
||||
${events.map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
|
||||
${(events || []).map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
|
||||
|
||||
return (
|
||||
<div className="my-12">
|
||||
@@ -34,11 +36,12 @@ ${events.map((event) => ` ${event.year} : ${event.title}`).join("\n")}`;
|
||||
id={id}
|
||||
title={title}
|
||||
showShare={showShare}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{caption && (
|
||||
<p className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
<div className="text-center text-xs text-slate-400 mt-4 italic">
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FileExample } from './FileExample';
|
||||
import type { FileExampleGroup } from '../data/fileExamples';
|
||||
|
||||
interface FileExamplesListProps {
|
||||
groups: FileExampleGroup[];
|
||||
}
|
||||
|
||||
export const FileExamplesList: React.FC<FileExamplesListProps> = ({ groups }) => {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleAllInGroup = (groupId: string, files: any[]) => {
|
||||
const isAnyExpanded = files.some(f => expandedGroups[f.id]);
|
||||
const newExpanded = { ...expandedGroups };
|
||||
files.forEach(f => {
|
||||
newExpanded[f.id] = !isAnyExpanded;
|
||||
});
|
||||
setExpandedGroups(newExpanded);
|
||||
};
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg px-4 py-6 text-center">
|
||||
<svg className="w-6 h-6 mx-auto text-slate-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-slate-500 text-sm">No files found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{groups.map((group) => (
|
||||
<section
|
||||
key={group.groupId}
|
||||
className="not-prose bg-white border border-slate-200/80 rounded-lg w-full overflow-hidden"
|
||||
data-file-examples-group
|
||||
>
|
||||
<header className="px-3 py-2 grid grid-cols-[1fr_auto] items-center gap-3 border-b border-slate-200/80 bg-white">
|
||||
<h3 className="m-0 text-xs font-semibold text-slate-900 truncate tracking-tight leading-none">{group.title}</h3>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] text-slate-600 bg-slate-100/80 border border-slate-200/60 rounded-full px-2 py-0.5 tabular-nums">
|
||||
{group.files.length} files
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="toggle-all-btn h-8 w-8 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100"
|
||||
title="Toggle all"
|
||||
onClick={() => toggleAllInGroup(group.groupId, group.files)}
|
||||
>
|
||||
{group.files.some(f => expandedGroups[f.id]) ? (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="px-2 py-2 space-y-2">
|
||||
{group.files.map((file) => (
|
||||
<FileExample
|
||||
key={file.id}
|
||||
filename={file.filename}
|
||||
content={file.content}
|
||||
language={file.language}
|
||||
description={file.description}
|
||||
tags={file.tags}
|
||||
id={file.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ interface IconListItemProps {
|
||||
export const IconList: React.FC<IconListProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => <ul className={`space-y-4 ${className}`}>{children}</ul>;
|
||||
}) => <ul className={`not-prose space-y-4 ${className}`}>{children}</ul>;
|
||||
|
||||
export const IconListItem: React.FC<IconListItemProps> = ({
|
||||
children,
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ComparisonRow: React.FC<ComparisonRowProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<Reveal delay={delay}>
|
||||
<div className="space-y-4">
|
||||
<div className="not-prose space-y-4">
|
||||
{description && (
|
||||
<Label className="text-slate-400 text-[10px] tracking-[0.2em] uppercase">
|
||||
{description}
|
||||
|
||||
@@ -5,22 +5,67 @@ import mermaid from "mermaid";
|
||||
import { DiagramShareButton } from "./DiagramShareButton";
|
||||
|
||||
interface MermaidProps {
|
||||
graph: string;
|
||||
graph?: string;
|
||||
children?: React.ReactNode;
|
||||
id?: string;
|
||||
title?: string;
|
||||
showShare?: boolean;
|
||||
fontSize?: string;
|
||||
nodeFontSize?: string;
|
||||
labelFontSize?: string;
|
||||
actorFontSize?: string;
|
||||
messageFontSize?: string;
|
||||
noteFontSize?: string;
|
||||
titleFontSize?: string;
|
||||
sectionFontSize?: string;
|
||||
legendFontSize?: string;
|
||||
}
|
||||
|
||||
export const Mermaid: React.FC<MermaidProps> = ({
|
||||
graph,
|
||||
children,
|
||||
id: providedId,
|
||||
title,
|
||||
showShare = false,
|
||||
fontSize = "16px",
|
||||
nodeFontSize,
|
||||
labelFontSize,
|
||||
actorFontSize,
|
||||
messageFontSize,
|
||||
noteFontSize,
|
||||
titleFontSize,
|
||||
sectionFontSize,
|
||||
legendFontSize,
|
||||
}) => {
|
||||
const [id, setId] = useState<string | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [svgContent, setSvgContent] = useState<string>("");
|
||||
|
||||
// Extract text from React children nodes (MDX parses multi-line content as React nodes)
|
||||
const extractTextFromChildren = (node: React.ReactNode): string => {
|
||||
if (typeof node === 'string') return node;
|
||||
if (typeof node === 'number') return String(node);
|
||||
if (Array.isArray(node)) return node.map(extractTextFromChildren).join('\n');
|
||||
if (React.isValidElement(node)) {
|
||||
const props = node.props as { children?: React.ReactNode };
|
||||
if (props.children) {
|
||||
return extractTextFromChildren(props.children);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const rawGraph = graph || extractTextFromChildren(children) || "";
|
||||
|
||||
// MDXRemote double-escapes \n in plain string props (e.g., "graph TD\\nA-->B" becomes "graph TD\\\\nA-->B")
|
||||
// We need to unescape these back to real newlines for Mermaid to parse
|
||||
const sanitizedGraph = rawGraph
|
||||
.trim()
|
||||
.replace(/^`+|`+$/g, '')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\'/g, "'");
|
||||
|
||||
useEffect(() => {
|
||||
setId(
|
||||
providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`,
|
||||
@@ -63,7 +108,15 @@ export const Mermaid: React.FC<MermaidProps> = ({
|
||||
|
||||
// Font
|
||||
fontFamily: "Inter, system-ui, sans-serif",
|
||||
fontSize: "14px",
|
||||
fontSize: fontSize,
|
||||
nodeFontSize: nodeFontSize || fontSize,
|
||||
labelFontSize: labelFontSize || fontSize,
|
||||
actorFontSize: actorFontSize || fontSize,
|
||||
messageFontSize: messageFontSize || fontSize,
|
||||
noteFontSize: noteFontSize || fontSize,
|
||||
titleFontSize: titleFontSize || "20px",
|
||||
sectionFontSize: sectionFontSize || fontSize,
|
||||
legendFontSize: legendFontSize || fontSize,
|
||||
|
||||
// Pie Chart Colors - High Contrast Industrial Palette
|
||||
pie1: "#0f172a", // Deep Navy
|
||||
@@ -84,14 +137,21 @@ export const Mermaid: React.FC<MermaidProps> = ({
|
||||
|
||||
const render = async () => {
|
||||
if (containerRef.current && id) {
|
||||
if (!sanitizedGraph || sanitizedGraph.trim() === "") {
|
||||
console.warn("Mermaid: Empty or invalid graph provided, skipping render.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { svg } = await mermaid.render(`${id}-svg`, graph);
|
||||
const { svg } = await mermaid.render(`${id}-svg`, sanitizedGraph);
|
||||
containerRef.current.innerHTML = svg;
|
||||
setSvgContent(svg);
|
||||
setIsRendered(true);
|
||||
} catch (err) {
|
||||
console.error("Mermaid rendering failed:", err);
|
||||
setError("Failed to render diagram. Please check the syntax.");
|
||||
console.error("Graph that failed:", sanitizedGraph);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setError(`Failed to render diagram: ${errorMessage}`);
|
||||
setIsRendered(true);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +160,7 @@ export const Mermaid: React.FC<MermaidProps> = ({
|
||||
if (id) {
|
||||
render();
|
||||
}
|
||||
}, [graph, id]);
|
||||
}, [sanitizedGraph, id]);
|
||||
|
||||
if (!id) return null;
|
||||
|
||||
@@ -127,7 +187,7 @@ export const Mermaid: React.FC<MermaidProps> = ({
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
graph
|
||||
sanitizedGraph
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
27
apps/web/src/components/StatsDisplay.tsx
Normal file
27
apps/web/src/components/StatsDisplay.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface StatsDisplayProps {
|
||||
value: string | number;
|
||||
label: string;
|
||||
subtext?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const StatsDisplay: React.FC<StatsDisplayProps> = ({ value, label, subtext, className = '' }) => {
|
||||
return (
|
||||
<div className={`not-prose flex flex-col items-center justify-center p-8 my-10 bg-gradient-to-br from-slate-50 to-slate-100 border border-slate-200 rounded-2xl shadow-sm text-center ${className}`}>
|
||||
<span className="text-7xl font-black text-slate-900 tracking-tighter tabular-nums leading-none">
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-slate-700 mt-3 uppercase tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
{subtext && (
|
||||
<span className="text-sm font-medium text-slate-500 mt-2 max-w-xs leading-relaxed">
|
||||
{subtext}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export function TextSelectionShare() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { trackEvent } = useAnalytics();
|
||||
const selectionTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const selectionTimeoutRef = useRef<NodeJS.Timeout>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelection = () => {
|
||||
|
||||
@@ -41,22 +41,22 @@ export const LeadText: React.FC<TypographyProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => (
|
||||
<p
|
||||
<div
|
||||
className={`text-sm md:text-xl font-serif italic text-slate-500 leading-relaxed ${className}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const BodyText: React.FC<TypographyProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
}) => (
|
||||
<p
|
||||
<div
|
||||
className={`text-sm md:text-base text-slate-500 leading-relaxed ${className}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Label: React.FC<TypographyProps> = ({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
/* eslint-disable react/prop-types */
|
||||
import type {
|
||||
ThumbnailIcon,
|
||||
BlogThumbnailConfig,
|
||||
} from "../../data/blogThumbnails";
|
||||
import { blogThumbnails } from "../../data/blogThumbnails";
|
||||
} from "./blogThumbnails";
|
||||
import { blogThumbnails } from "./blogThumbnails";
|
||||
|
||||
interface BlogThumbnailSVGProps {
|
||||
slug: string;
|
||||
|
||||
140
apps/web/src/components/blog/blogThumbnails.ts
Normal file
140
apps/web/src/components/blog/blogThumbnails.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
export type ThumbnailIcon =
|
||||
| "gauge"
|
||||
| "bottleneck"
|
||||
| "plugin"
|
||||
| "shield"
|
||||
| "cookie"
|
||||
| "cloud"
|
||||
| "lock"
|
||||
| "chart"
|
||||
| "leaf"
|
||||
| "price"
|
||||
| "prototype"
|
||||
| "gear"
|
||||
| "hourglass"
|
||||
| "code"
|
||||
| "responsive"
|
||||
| "server"
|
||||
| "template"
|
||||
| "sync";
|
||||
|
||||
export interface BlogThumbnailConfig {
|
||||
icon: ThumbnailIcon;
|
||||
accent: string;
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of blog post slugs to their unique thumbnail configuration.
|
||||
* Each entry defines the abstract SVG illustration style for a given post.
|
||||
* Updated to match the new MDX slugs.
|
||||
*/
|
||||
export const blogThumbnails: Record<string, BlogThumbnailConfig> = {
|
||||
// Group 1: Pain Points & Troubleshooting
|
||||
"why-pagespeed-fails": {
|
||||
icon: "gauge",
|
||||
accent: "#ef4444",
|
||||
keyword: "SPEED",
|
||||
},
|
||||
"slow-loading-costs-customers": {
|
||||
icon: "gauge",
|
||||
accent: "#f97316",
|
||||
keyword: "LATENCY",
|
||||
},
|
||||
"why-agencies-are-slow": {
|
||||
icon: "bottleneck",
|
||||
accent: "#8b5cf6",
|
||||
keyword: "PROCESS",
|
||||
},
|
||||
"hidden-costs-of-wordpress-plugins": {
|
||||
icon: "plugin",
|
||||
accent: "#ec4899",
|
||||
keyword: "PLUGINS",
|
||||
},
|
||||
"why-websites-break-after-updates": {
|
||||
icon: "shield",
|
||||
accent: "#f59e0b",
|
||||
keyword: "STABILITY",
|
||||
},
|
||||
|
||||
// Group 2: Sovereignty & Law
|
||||
"website-without-cookie-banners": {
|
||||
icon: "cookie",
|
||||
accent: "#10b981",
|
||||
keyword: "PRIVACY",
|
||||
},
|
||||
"no-us-cloud-platforms": {
|
||||
icon: "cloud",
|
||||
accent: "#3b82f6",
|
||||
keyword: "SOVEREIGN",
|
||||
},
|
||||
"gdpr-conformity-system-approach": {
|
||||
icon: "shield",
|
||||
accent: "#06b6d4",
|
||||
keyword: "DSGVO",
|
||||
},
|
||||
"builder-systems-threaten-independence": {
|
||||
icon: "lock",
|
||||
accent: "#f43f5e",
|
||||
keyword: "LOCK-IN",
|
||||
},
|
||||
"analytics-without-tracking": {
|
||||
icon: "chart",
|
||||
accent: "#8b5cf6",
|
||||
keyword: "ANALYTICS",
|
||||
},
|
||||
|
||||
// Group 3: Efficiency & Investment
|
||||
"green-it-sustainable-web": {
|
||||
icon: "leaf",
|
||||
accent: "#22c55e",
|
||||
keyword: "GREEN",
|
||||
},
|
||||
"fixed-price-digital-projects": {
|
||||
icon: "price",
|
||||
accent: "#0ea5e9",
|
||||
keyword: "PRICING",
|
||||
},
|
||||
"build-first-digital-architecture": {
|
||||
icon: "prototype",
|
||||
accent: "#a855f7",
|
||||
keyword: "PROTOTYPE",
|
||||
},
|
||||
"maintenance-for-headless-systems": {
|
||||
icon: "gear",
|
||||
accent: "#64748b",
|
||||
keyword: "MAINTAIN",
|
||||
},
|
||||
"digital-longevity-architecture": {
|
||||
icon: "hourglass",
|
||||
accent: "#0d9488",
|
||||
keyword: "LONGEVITY",
|
||||
},
|
||||
|
||||
// Group 4: Tech & Craft
|
||||
"clean-code-for-business-value": {
|
||||
icon: "code",
|
||||
accent: "#2563eb",
|
||||
keyword: "QUALITY",
|
||||
},
|
||||
"responsive-design-high-fidelity": {
|
||||
icon: "responsive",
|
||||
accent: "#7c3aed",
|
||||
keyword: "ADAPTIVE",
|
||||
},
|
||||
"professional-hosting-operations": {
|
||||
icon: "server",
|
||||
accent: "#475569",
|
||||
keyword: "INFRA",
|
||||
},
|
||||
"why-no-templates-matter": {
|
||||
icon: "template",
|
||||
accent: "#e11d48",
|
||||
keyword: "CUSTOM",
|
||||
},
|
||||
"crm-synchronization-headless": {
|
||||
icon: "sync",
|
||||
accent: "#0891b2",
|
||||
keyword: "SYNC",
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user