Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
1108 lines
26 KiB
TypeScript
1108 lines
26 KiB
TypeScript
import type { ThumbnailIcon } from "./blogThumbnails";
|
|
import { blogThumbnails } from "./blogThumbnails";
|
|
|
|
interface BlogThumbnailSVGProps {
|
|
slug: string;
|
|
variant?: "square" | "banner";
|
|
className?: string;
|
|
}
|
|
|
|
// Grid pattern used in the background
|
|
const GridPattern: React.FC<{ size: number }> = ({ size }) => (
|
|
<defs>
|
|
<pattern id="grid" width={size} height={size} patternUnits="userSpaceOnUse">
|
|
<path
|
|
d={`M ${size} 0 L 0 0 0 ${size}`}
|
|
fill="none"
|
|
stroke="#94a3b8"
|
|
strokeWidth="0.5"
|
|
opacity="0.12"
|
|
/>
|
|
</pattern>
|
|
</defs>
|
|
);
|
|
|
|
// ─── Icon Renderers ───────────────────────────────────────────────
|
|
|
|
function renderGauge(cx: number, cy: number, accent: string) {
|
|
const r = 52;
|
|
// Arc from ~210° to ~330° (lower half open)
|
|
const startAngle = (210 * Math.PI) / 180;
|
|
const endAngle = (330 * Math.PI) / 180;
|
|
const x1 = cx + r * Math.cos(startAngle);
|
|
const y1 = cy + r * Math.sin(startAngle);
|
|
const x2 = cx + r * Math.cos(endAngle);
|
|
const y2 = cy + r * Math.sin(endAngle);
|
|
// Needle at ~280° (pointing upper-right = fast/danger zone)
|
|
const needleAngle = (280 * Math.PI) / 180;
|
|
const nx = cx + (r - 14) * Math.cos(needleAngle);
|
|
const ny = cy + (r - 14) * Math.sin(needleAngle);
|
|
|
|
return (
|
|
<g>
|
|
<path
|
|
d={`M ${x1} ${y1} A ${r} ${r} 0 1 1 ${x2} ${y2}`}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="6"
|
|
strokeLinecap="round"
|
|
/>
|
|
<path
|
|
d={`M ${x1} ${y1} A ${r} ${r} 0 1 1 ${x2} ${y2}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="6"
|
|
strokeLinecap="round"
|
|
strokeDasharray="200"
|
|
strokeDashoffset="60"
|
|
/>
|
|
<line
|
|
x1={cx}
|
|
y1={cy}
|
|
x2={nx}
|
|
y2={ny}
|
|
stroke="#0f172a"
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
/>
|
|
<circle cx={cx} cy={cy} r="5" fill="#0f172a" />
|
|
{/* Tick marks */}
|
|
{[210, 240, 270, 300, 330].map((deg) => {
|
|
const rad = (deg * Math.PI) / 180;
|
|
const tx1 = cx + (r + 6) * Math.cos(rad);
|
|
const ty1 = cy + (r + 6) * Math.sin(rad);
|
|
const tx2 = cx + (r + 12) * Math.cos(rad);
|
|
const ty2 = cy + (r + 12) * Math.sin(rad);
|
|
return (
|
|
<line
|
|
key={deg}
|
|
x1={tx1}
|
|
y1={ty1}
|
|
x2={tx2}
|
|
y2={ty2}
|
|
stroke="#94a3b8"
|
|
strokeWidth="1.5"
|
|
/>
|
|
);
|
|
})}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderBottleneck(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Wide top */}
|
|
<rect
|
|
x={cx - 50}
|
|
y={cy - 50}
|
|
width="100"
|
|
height="30"
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Narrow middle (bottleneck) */}
|
|
<path
|
|
d={`M ${cx - 50} ${cy - 20} L ${cx - 12} ${cy} L ${cx - 12} ${cy + 10} L ${cx - 50} ${cy + 30} Z`}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
<path
|
|
d={`M ${cx + 50} ${cy - 20} L ${cx + 12} ${cy} L ${cx + 12} ${cy + 10} L ${cx + 50} ${cy + 30} Z`}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Flow lines */}
|
|
{[-20, 0, 20].map((offset) => (
|
|
<circle
|
|
key={offset}
|
|
cx={cx + offset}
|
|
cy={cy - 38}
|
|
r="3"
|
|
fill={accent}
|
|
opacity="0.7"
|
|
/>
|
|
))}
|
|
<circle cx={cx} cy={cy + 5} r="3" fill={accent} />
|
|
{/* Wide bottom */}
|
|
<rect
|
|
x={cx - 50}
|
|
y={cy + 30}
|
|
width="100"
|
|
height="30"
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Arrow down through throttle */}
|
|
<line
|
|
x1={cx}
|
|
y1={cy - 30}
|
|
x2={cx}
|
|
y2={cy + 20}
|
|
stroke={accent}
|
|
strokeWidth="2"
|
|
strokeDasharray="4 3"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderPlugin(cx: number, cy: number, accent: string) {
|
|
const s = 28;
|
|
return (
|
|
<g>
|
|
{/* Connected piece 1 */}
|
|
<rect
|
|
x={cx - s - 8}
|
|
y={cy - s}
|
|
width={s}
|
|
height={s}
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Connected piece 2 */}
|
|
<rect
|
|
x={cx - 8}
|
|
y={cy - s}
|
|
width={s}
|
|
height={s}
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Connected piece 3 */}
|
|
<rect
|
|
x={cx - s - 8}
|
|
y={cy + 4}
|
|
width={s}
|
|
height={s}
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Disconnected piece (offset) */}
|
|
<rect
|
|
x={cx + 6}
|
|
y={cy + 12}
|
|
width={s}
|
|
height={s}
|
|
rx="4"
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2"
|
|
strokeDasharray="4 3"
|
|
transform={`rotate(8, ${cx + 6 + s / 2}, ${cy + 12 + s / 2})`}
|
|
/>
|
|
{/* Connector dots */}
|
|
<circle cx={cx - 8} cy={cy - s / 2} r="2.5" fill="#cbd5e1" />
|
|
<circle cx={cx - 8} cy={cy + 4 + s / 2} r="2.5" fill="#cbd5e1" />
|
|
{/* Warning */}
|
|
<text
|
|
x={cx + 20 + s / 2}
|
|
y={cy + 26 + s / 2}
|
|
fontSize="18"
|
|
fontWeight="bold"
|
|
fill={accent}
|
|
textAnchor="middle"
|
|
>
|
|
!
|
|
</text>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderShield(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
<path
|
|
d={`M ${cx} ${cy - 55} L ${cx + 45} ${cy - 30} L ${cx + 45} ${cy + 10} Q ${cx + 45} ${cy + 45} ${cx} ${cy + 55} Q ${cx - 45} ${cy + 45} ${cx - 45} ${cy + 10} L ${cx - 45} ${cy - 30} Z`}
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
<path
|
|
d={`M ${cx} ${cy - 55} L ${cx + 45} ${cy - 30} L ${cx + 45} ${cy + 10} Q ${cx + 45} ${cy + 45} ${cx} ${cy + 55} Q ${cx - 45} ${cy + 45} ${cx - 45} ${cy + 10} L ${cx - 45} ${cy - 30} Z`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
opacity="0.4"
|
|
/>
|
|
{/* Checkmark */}
|
|
<polyline
|
|
points={`${cx - 16},${cy} ${cx - 4},${cy + 14} ${cx + 20},${cy - 14}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="4"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderCookie(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Cookie circle */}
|
|
<circle
|
|
cx={cx}
|
|
cy={cy}
|
|
r="48"
|
|
fill="#fef3c7"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Chips */}
|
|
<circle cx={cx - 14} cy={cy - 16} r="5" fill="#92400e" opacity="0.5" />
|
|
<circle cx={cx + 18} cy={cy - 8} r="4" fill="#92400e" opacity="0.5" />
|
|
<circle cx={cx - 6} cy={cy + 18} r="5" fill="#92400e" opacity="0.5" />
|
|
<circle cx={cx + 12} cy={cy + 14} r="3.5" fill="#92400e" opacity="0.5" />
|
|
{/* Strikethrough */}
|
|
<line
|
|
x1={cx - 38}
|
|
y1={cx + 38}
|
|
x2={cx + 38}
|
|
y2={cy - 38}
|
|
stroke={accent}
|
|
strokeWidth="4"
|
|
strokeLinecap="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderCloud(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
<path
|
|
d={`M ${cx - 40} ${cy + 16} Q ${cx - 52} ${cy + 16} ${cx - 52} ${cy} Q ${cx - 52} ${cy - 20} ${cx - 32} ${cy - 24} Q ${cx - 28} ${cy - 48} ${cx} ${cy - 44} Q ${cx + 28} ${cy - 48} ${cx + 32} ${cy - 24} Q ${cx + 52} ${cy - 20} ${cx + 52} ${cy} Q ${cx + 52} ${cy + 16} ${cx + 40} ${cy + 16} Z`}
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Lock icon inside cloud */}
|
|
<rect
|
|
x={cx - 12}
|
|
y={cy - 6}
|
|
width="24"
|
|
height="18"
|
|
rx="3"
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
/>
|
|
<path
|
|
d={`M ${cx - 6} ${cy - 6} L ${cx - 6} ${cy - 14} Q ${cx - 6} ${cy - 22} ${cx} ${cy - 22} Q ${cx + 6} ${cy - 22} ${cx + 6} ${cy - 14} L ${cx + 6} ${cy - 6}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
/>
|
|
<circle cx={cx} cy={cy + 3} r="2.5" fill={accent} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderLock(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Lock body */}
|
|
<rect
|
|
x={cx - 24}
|
|
y={cy - 8}
|
|
width="48"
|
|
height="40"
|
|
rx="6"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Lock shackle */}
|
|
<path
|
|
d={`M ${cx - 14} ${cy - 8} L ${cx - 14} ${cy - 24} Q ${cx - 14} ${cy - 40} ${cx} ${cy - 40} Q ${cx + 14} ${cy - 40} ${cx + 14} ${cy - 24} L ${cx + 14} ${cy - 8}`}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="3"
|
|
/>
|
|
{/* Keyhole */}
|
|
<circle cx={cx} cy={cy + 6} r="5" fill={accent} />
|
|
<rect x={cx - 2} y={cy + 9} width="4" height="10" rx="1" fill={accent} />
|
|
{/* Chain links */}
|
|
{[-42, 42].map((offset) => (
|
|
<g key={offset}>
|
|
<ellipse
|
|
cx={cx + offset}
|
|
cy={cy + 12}
|
|
rx="10"
|
|
ry="6"
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
<ellipse
|
|
cx={cx + offset + (offset > 0 ? 14 : -14)}
|
|
cy={cy + 12}
|
|
rx="10"
|
|
ry="6"
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
</g>
|
|
))}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderChart(cx: number, cy: number, accent: string) {
|
|
const barWidth = 14;
|
|
const heights = [30, 50, 38, 55, 42];
|
|
const baseY = cy + 35;
|
|
return (
|
|
<g>
|
|
{/* Bars */}
|
|
{heights.map((h, i) => (
|
|
<rect
|
|
key={i}
|
|
x={cx - 48 + i * 22}
|
|
y={baseY - h}
|
|
width={barWidth}
|
|
height={h}
|
|
rx="2"
|
|
fill={i === 3 ? accent : "#e2e8f0"}
|
|
opacity={i === 3 ? 0.8 : 0.6}
|
|
/>
|
|
))}
|
|
{/* Base line */}
|
|
<line
|
|
x1={cx - 55}
|
|
y1={baseY}
|
|
x2={cx + 60}
|
|
y2={baseY}
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Eye with strikethrough */}
|
|
<ellipse
|
|
cx={cx}
|
|
cy={cy - 30}
|
|
rx="22"
|
|
ry="14"
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2"
|
|
/>
|
|
<circle cx={cx} cy={cy - 30} r="6" fill={accent} opacity="0.5" />
|
|
<line
|
|
x1={cx - 28}
|
|
y1={cy - 18}
|
|
x2={cx + 28}
|
|
y2={cy - 42}
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderLeaf(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Leaf shape */}
|
|
<path
|
|
d={`M ${cx} ${cy + 50} Q ${cx - 50} ${cy + 20} ${cx - 45} ${cy - 30} Q ${cx - 40} ${cy - 55} ${cx} ${cy - 50} Q ${cx + 40} ${cy - 55} ${cx + 45} ${cy - 30} Q ${cx + 50} ${cy + 20} ${cx} ${cy + 50} Z`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
opacity="0.7"
|
|
/>
|
|
<path
|
|
d={`M ${cx} ${cy + 50} Q ${cx - 50} ${cy + 20} ${cx - 45} ${cy - 30} Q ${cx - 40} ${cy - 55} ${cx} ${cy - 50} Q ${cx + 40} ${cy - 55} ${cx + 45} ${cy - 30} Q ${cx + 50} ${cy + 20} ${cx} ${cy + 50} Z`}
|
|
fill={accent}
|
|
opacity="0.08"
|
|
/>
|
|
{/* Stem/vein */}
|
|
<line
|
|
x1={cx}
|
|
y1={cy + 50}
|
|
x2={cx}
|
|
y2={cy - 40}
|
|
stroke={accent}
|
|
strokeWidth="1.5"
|
|
opacity="0.5"
|
|
/>
|
|
{/* Circuit nodes on leaf veins */}
|
|
{[-20, 0, 20].map((offset, i) => (
|
|
<g key={i}>
|
|
<line
|
|
x1={cx}
|
|
y1={cy + offset}
|
|
x2={cx + (i % 2 === 0 ? 20 : -20)}
|
|
y2={cy + offset - 10}
|
|
stroke={accent}
|
|
strokeWidth="1"
|
|
opacity="0.4"
|
|
/>
|
|
<circle
|
|
cx={cx + (i % 2 === 0 ? 20 : -20)}
|
|
cy={cy + offset - 10}
|
|
r="3"
|
|
fill={accent}
|
|
opacity="0.5"
|
|
/>
|
|
</g>
|
|
))}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderPrice(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Tag shape */}
|
|
<path
|
|
d={`M ${cx - 30} ${cy - 40} L ${cx + 30} ${cy - 40} L ${cx + 30} ${cy + 20} L ${cx} ${cy + 45} L ${cx - 30} ${cy + 20} Z`}
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Hole */}
|
|
<circle
|
|
cx={cx}
|
|
cy={cy - 26}
|
|
r="6"
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Equals sign */}
|
|
<line
|
|
x1={cx - 14}
|
|
y1={cy}
|
|
x2={cx + 14}
|
|
y2={cy}
|
|
stroke={accent}
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
<line
|
|
x1={cx - 14}
|
|
y1={cy + 12}
|
|
x2={cx + 14}
|
|
y2={cy + 12}
|
|
stroke={accent}
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderPrototype(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Browser frame */}
|
|
<rect
|
|
x={cx - 48}
|
|
y={cy - 42}
|
|
width="96"
|
|
height="76"
|
|
rx="6"
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Title bar */}
|
|
<line
|
|
x1={cx - 48}
|
|
y1={cy - 28}
|
|
x2={cx + 48}
|
|
y2={cy - 28}
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Dots */}
|
|
<circle cx={cx - 36} cy={cy - 35} r="2.5" fill="#fca5a5" />
|
|
<circle cx={cx - 28} cy={cy - 35} r="2.5" fill="#fde68a" />
|
|
<circle cx={cx - 20} cy={cy - 35} r="2.5" fill="#86efac" />
|
|
{/* Wireframe lines */}
|
|
<rect
|
|
x={cx - 38}
|
|
y={cy - 20}
|
|
width="76"
|
|
height="8"
|
|
rx="2"
|
|
fill="#e2e8f0"
|
|
/>
|
|
<rect
|
|
x={cx - 38}
|
|
y={cy - 6}
|
|
width="50"
|
|
height="5"
|
|
rx="1.5"
|
|
fill="#e2e8f0"
|
|
/>
|
|
<rect
|
|
x={cx - 38}
|
|
y={cy + 5}
|
|
width="60"
|
|
height="5"
|
|
rx="1.5"
|
|
fill="#e2e8f0"
|
|
/>
|
|
{/* Accent wireframe block */}
|
|
<rect
|
|
x={cx - 38}
|
|
y={cy + 16}
|
|
width="32"
|
|
height="14"
|
|
rx="3"
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="1.5"
|
|
strokeDasharray="3 2"
|
|
/>
|
|
<rect
|
|
x={cx + 2}
|
|
y={cy + 16}
|
|
width="36"
|
|
height="14"
|
|
rx="3"
|
|
fill={accent}
|
|
opacity="0.12"
|
|
stroke={accent}
|
|
strokeWidth="1"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderGear(cx: number, cy: number, accent: string) {
|
|
const teeth = 8;
|
|
const innerR = 24;
|
|
const outerR = 36;
|
|
const toothWidth = 0.2;
|
|
|
|
let d = "";
|
|
for (let i = 0; i < teeth; i++) {
|
|
const angle = (i * 2 * Math.PI) / teeth;
|
|
const a1 = angle - toothWidth;
|
|
const a2 = angle + toothWidth;
|
|
const _midAngle = (a1 + a2) / 2;
|
|
|
|
if (i === 0) {
|
|
d += `M ${cx + outerR * Math.cos(a1)} ${cy + outerR * Math.sin(a1)} `;
|
|
}
|
|
d += `L ${cx + outerR * Math.cos(a2)} ${cy + outerR * Math.sin(a2)} `;
|
|
|
|
const nextAngle = ((i + 1) * 2 * Math.PI) / teeth;
|
|
const na1 = nextAngle - toothWidth;
|
|
d += `L ${cx + innerR * Math.cos(a2)} ${cy + innerR * Math.sin(a2)} `;
|
|
d += `L ${cx + innerR * Math.cos(na1)} ${cy + innerR * Math.sin(na1)} `;
|
|
d += `L ${cx + outerR * Math.cos(na1)} ${cy + outerR * Math.sin(na1)} `;
|
|
}
|
|
d += "Z";
|
|
|
|
return (
|
|
<g>
|
|
<path d={d} fill="#f1f5f9" stroke="#cbd5e1" strokeWidth="1.5" />
|
|
<circle
|
|
cx={cx}
|
|
cy={cy}
|
|
r="10"
|
|
fill="white"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Infinity loop */}
|
|
<path
|
|
d={`M ${cx} ${cy} C ${cx - 20} ${cy - 18} ${cx - 28} ${cy + 18} ${cx} ${cy} C ${cx + 28} ${cy - 18} ${cx + 20} ${cy + 18} ${cx} ${cy}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderHourglass(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Top triangle */}
|
|
<path
|
|
d={`M ${cx - 32} ${cy - 50} L ${cx + 32} ${cy - 50} L ${cx + 4} ${cy} Z`}
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
<path
|
|
d={`M ${cx - 32} ${cy - 50} L ${cx + 32} ${cy - 50} L ${cx + 4} ${cy} Z`}
|
|
fill={accent}
|
|
opacity="0.08"
|
|
/>
|
|
{/* Bottom triangle */}
|
|
<path
|
|
d={`M ${cx - 4} ${cy} L ${cx + 32} ${cy + 50} L ${cx - 32} ${cy + 50} Z`}
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* "Sand" fill in bottom */}
|
|
<path
|
|
d={`M ${cx - 4} ${cy + 14} L ${cx + 18} ${cy + 50} L ${cx - 18} ${cy + 50} Z`}
|
|
fill={accent}
|
|
opacity="0.2"
|
|
/>
|
|
{/* Top and bottom lines */}
|
|
<line
|
|
x1={cx - 38}
|
|
y1={cy - 50}
|
|
x2={cx + 38}
|
|
y2={cy - 50}
|
|
stroke="#0f172a"
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
<line
|
|
x1={cx - 38}
|
|
y1={cy + 50}
|
|
x2={cx + 38}
|
|
y2={cy + 50}
|
|
stroke="#0f172a"
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
{/* Circuit nodes as "digital sand" */}
|
|
{[10, 22, 34].map((y, i) => (
|
|
<circle
|
|
key={i}
|
|
cx={cx + (i - 1) * 6}
|
|
cy={cy + y}
|
|
r="2"
|
|
fill={accent}
|
|
opacity={0.5 + i * 0.15}
|
|
/>
|
|
))}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderCode(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* < */}
|
|
<polyline
|
|
points={`${cx - 20},${cy - 24} ${cx - 46},${cy} ${cx - 20},${cy + 24}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
{/* / */}
|
|
<line
|
|
x1={cx + 8}
|
|
y1={cy - 30}
|
|
x2={cx - 8}
|
|
y2={cy + 30}
|
|
stroke="#cbd5e1"
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
{/* > */}
|
|
<polyline
|
|
points={`${cx + 20},${cy - 24} ${cx + 46},${cy} ${cx + 20},${cy + 24}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderResponsive(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Outer desktop */}
|
|
<rect
|
|
x={cx - 52}
|
|
y={cy - 38}
|
|
width="104"
|
|
height="68"
|
|
rx="5"
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Tablet */}
|
|
<rect
|
|
x={cx - 30}
|
|
y={cy - 24}
|
|
width="60"
|
|
height="48"
|
|
rx="4"
|
|
fill="none"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Phone */}
|
|
<rect
|
|
x={cx - 14}
|
|
y={cy - 14}
|
|
width="28"
|
|
height="42"
|
|
rx="4"
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="2"
|
|
/>
|
|
{/* Phone screen */}
|
|
<rect
|
|
x={cx - 10}
|
|
y={cy - 8}
|
|
width="20"
|
|
height="28"
|
|
rx="1"
|
|
fill={accent}
|
|
opacity="0.1"
|
|
/>
|
|
{/* Phone home button */}
|
|
<circle cx={cx} cy={cy + 22} r="2" fill={accent} opacity="0.5" />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderServer(cx: number, cy: number, accent: string) {
|
|
const unitH = 22;
|
|
return (
|
|
<g>
|
|
{/* Server units */}
|
|
{[0, 1, 2].map((i) => (
|
|
<g key={i}>
|
|
<rect
|
|
x={cx - 40}
|
|
y={cy - 36 + i * (unitH + 4)}
|
|
width="80"
|
|
height={unitH}
|
|
rx="3"
|
|
fill="#f8fafc"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Status LEDs */}
|
|
<circle
|
|
cx={cx - 28}
|
|
cy={cy - 36 + i * (unitH + 4) + unitH / 2}
|
|
r="3"
|
|
fill={i === 0 ? accent : "#cbd5e1"}
|
|
/>
|
|
<circle
|
|
cx={cx - 18}
|
|
cy={cy - 36 + i * (unitH + 4) + unitH / 2}
|
|
r="3"
|
|
fill={i <= 1 ? accent : "#cbd5e1"}
|
|
opacity={i <= 1 ? 0.6 : 1}
|
|
/>
|
|
{/* Drive bays */}
|
|
{[0, 1, 2, 3].map((j) => (
|
|
<rect
|
|
key={j}
|
|
x={cx + 4 + j * 10}
|
|
y={cy - 36 + i * (unitH + 4) + 5}
|
|
width="6"
|
|
height={unitH - 10}
|
|
rx="1"
|
|
fill="#e2e8f0"
|
|
/>
|
|
))}
|
|
</g>
|
|
))}
|
|
{/* Signal waves */}
|
|
{[10, 18, 26].map((r, i) => (
|
|
<path
|
|
key={i}
|
|
d={`M ${cx + 42 + r} ${cy - 12} Q ${cx + 42 + r + 6} ${cy} ${cx + 42 + r} ${cy + 12}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="1.5"
|
|
opacity={0.7 - i * 0.2}
|
|
/>
|
|
))}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderTemplate(cx: number, cy: number, accent: string) {
|
|
return (
|
|
<g>
|
|
{/* Grid layout */}
|
|
{[0, 1, 2, 3].map((i) => {
|
|
const row = Math.floor(i / 2);
|
|
const col = i % 2;
|
|
return (
|
|
<rect
|
|
key={i}
|
|
x={cx - 44 + col * 48}
|
|
y={cy - 36 + row * 40}
|
|
width="40"
|
|
height="32"
|
|
rx="4"
|
|
fill="#f1f5f9"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
);
|
|
})}
|
|
{/* Strikethrough diagonal */}
|
|
<line
|
|
x1={cx - 52}
|
|
y1={cy + 44}
|
|
x2={cx + 52}
|
|
y2={cy - 44}
|
|
stroke={accent}
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
/>
|
|
<line
|
|
x1={cx - 52}
|
|
y1={cy + 40}
|
|
x2={cx + 52}
|
|
y2={cy - 48}
|
|
stroke="white"
|
|
strokeWidth="1"
|
|
opacity="0.5"
|
|
/>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
function renderSync(cx: number, cy: number, accent: string) {
|
|
const r = 36;
|
|
return (
|
|
<g>
|
|
{/* Circular arrows */}
|
|
<path
|
|
d={`M ${cx + r} ${cy} A ${r} ${r} 0 0 1 ${cx - r} ${cy}`}
|
|
fill="none"
|
|
stroke={accent}
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
/>
|
|
<path
|
|
d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
|
|
fill="none"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
/>
|
|
{/* Arrowheads */}
|
|
<polygon
|
|
points={`${cx - r - 6},${cy - 6} ${cx - r + 6},${cy - 6} ${cx - r},${cy + 2}`}
|
|
fill={accent}
|
|
/>
|
|
<polygon
|
|
points={`${cx + r - 6},${cy + 6} ${cx + r + 6},${cy + 6} ${cx + r},${cy - 2}`}
|
|
fill="#cbd5e1"
|
|
/>
|
|
{/* Data dots flowing */}
|
|
<circle
|
|
cx={cx + 18}
|
|
cy={cy - r + 8}
|
|
r="3.5"
|
|
fill={accent}
|
|
opacity="0.7"
|
|
/>
|
|
<circle
|
|
cx={cx - 18}
|
|
cy={cy + r - 8}
|
|
r="3.5"
|
|
fill="#94a3b8"
|
|
opacity="0.7"
|
|
/>
|
|
{/* Center node */}
|
|
<circle
|
|
cx={cx}
|
|
cy={cy}
|
|
r="8"
|
|
fill="white"
|
|
stroke="#cbd5e1"
|
|
strokeWidth="1.5"
|
|
/>
|
|
<circle cx={cx} cy={cy} r="3" fill={accent} />
|
|
</g>
|
|
);
|
|
}
|
|
|
|
// ─── Icon dispatcher ──────────────────────────────────────────────
|
|
|
|
const iconRenderers: Record<
|
|
ThumbnailIcon,
|
|
(_cx: number, _cy: number, _accent: string) => React.ReactNode
|
|
> = {
|
|
gauge: renderGauge,
|
|
bottleneck: renderBottleneck,
|
|
plugin: renderPlugin,
|
|
shield: renderShield,
|
|
cookie: renderCookie,
|
|
cloud: renderCloud,
|
|
lock: renderLock,
|
|
chart: renderChart,
|
|
leaf: renderLeaf,
|
|
price: renderPrice,
|
|
prototype: renderPrototype,
|
|
gear: renderGear,
|
|
hourglass: renderHourglass,
|
|
code: renderCode,
|
|
responsive: renderResponsive,
|
|
server: renderServer,
|
|
template: renderTemplate,
|
|
sync: renderSync,
|
|
};
|
|
|
|
// ─── Main Component ──────────────────────────────────────────────
|
|
|
|
export const BlogThumbnailSVG: React.FC<BlogThumbnailSVGProps> = ({
|
|
slug,
|
|
variant = "square",
|
|
className,
|
|
}) => {
|
|
const config = blogThumbnails[slug];
|
|
if (!config) return null;
|
|
|
|
const isBanner = variant === "banner";
|
|
const vbWidth = isBanner ? 480 : 240;
|
|
const vbHeight = isBanner ? 160 : 240;
|
|
|
|
// For banner, we shift the icon more to the right
|
|
const iconCx = isBanner ? 340 : vbWidth / 2;
|
|
const iconCy = vbHeight / 2;
|
|
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${vbWidth} ${vbHeight}`}
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={className}
|
|
role="img"
|
|
aria-label={`Illustration: ${config.keyword}`}
|
|
preserveAspectRatio="xMidYMid slice"
|
|
>
|
|
{/* Background */}
|
|
<rect
|
|
width={vbWidth}
|
|
height={vbHeight}
|
|
fill="#f8fafc"
|
|
rx={isBanner ? 8 : 12}
|
|
/>
|
|
<GridPattern size={30} />
|
|
<rect
|
|
width={vbWidth}
|
|
height={vbHeight}
|
|
fill="url(#grid)"
|
|
rx={isBanner ? 8 : 12}
|
|
/>
|
|
|
|
{/* Accent strip at top */}
|
|
<rect
|
|
width={vbWidth}
|
|
height="3"
|
|
fill={config.accent}
|
|
opacity="0.6"
|
|
rx="1.5"
|
|
/>
|
|
|
|
{/* Decorative circuit lines */}
|
|
<line
|
|
x1="0"
|
|
y1={vbHeight - 1}
|
|
x2={vbWidth}
|
|
y2={vbHeight - 1}
|
|
stroke="#e2e8f0"
|
|
strokeWidth="1"
|
|
/>
|
|
|
|
{/* Corner markers */}
|
|
<g opacity="0.25">
|
|
<line
|
|
x1="12"
|
|
y1="16"
|
|
x2="28"
|
|
y2="16"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1"
|
|
/>
|
|
<line
|
|
x1="12"
|
|
y1="16"
|
|
x2="12"
|
|
y2="32"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1"
|
|
/>
|
|
<line
|
|
x1={vbWidth - 12}
|
|
y1="16"
|
|
x2={vbWidth - 28}
|
|
y2="16"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1"
|
|
/>
|
|
<line
|
|
x1={vbWidth - 12}
|
|
y1="16"
|
|
x2={vbWidth - 12}
|
|
y2="32"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1"
|
|
/>
|
|
</g>
|
|
|
|
{/* Icon - Scale down for banner to fit height better */}
|
|
<g
|
|
transform={
|
|
isBanner
|
|
? `translate(${iconCx}, ${iconCy}) scale(0.85) translate(-${iconCx}, -${iconCy})`
|
|
: ""
|
|
}
|
|
>
|
|
{iconRenderers[config.icon](iconCx, iconCy, config.accent)}
|
|
</g>
|
|
|
|
{/* Keyword label */}
|
|
<text
|
|
x="18"
|
|
y={vbHeight - 16}
|
|
fontSize="9"
|
|
fontFamily="ui-monospace, monospace"
|
|
fontWeight="700"
|
|
fill="#94a3b8"
|
|
letterSpacing="0.15em"
|
|
>
|
|
{config.keyword}
|
|
</text>
|
|
|
|
{/* Accent dot */}
|
|
<circle
|
|
cx={vbWidth - 18}
|
|
cy={vbHeight - 18}
|
|
r="3"
|
|
fill={config.accent}
|
|
opacity="0.6"
|
|
/>
|
|
</svg>
|
|
);
|
|
};
|