Add InfrastructureDiagram component missing from previous commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-09 01:52:45 +00:00
parent e4345a4da4
commit 5e204c278b
2 changed files with 354 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
'use client';
import { useState, useEffect, useRef } from 'react';
interface NodeConfig {
id: string;
label: string;
description: string;
icon: string;
color: string;
tech: string[];
x: number;
y: number;
connections: string[];
}
const nodes: NodeConfig[] = [
{
id: 'whymyrating',
label: 'whymyrating',
description: 'Web + Mobile Apps',
icon: '📱',
color: '#8b5cf6',
tech: ['React', 'Next.js', 'React Native', 'TypeScript'],
x: 50,
y: 5,
connections: ['whymyrating-engine', 'whymyrating-brand', 'whymyrating-templates'],
},
{
id: 'whymyrating-engine',
label: 'whymyrating-engine',
description: 'AI Pipelines & Backend API',
icon: '⚙️',
color: '#ef4444',
tech: ['Python', 'FastAPI', 'PostgreSQL', 'Docker'],
x: 10,
y: 40,
connections: ['nuc-server'],
},
{
id: 'whymyrating-brand',
label: 'whymyrating-brand',
description: 'Brand Assets & Guidelines',
icon: '🎨',
color: '#f59e0b',
tech: ['SVG', 'Figma', 'CSS'],
x: 50,
y: 40,
connections: [],
},
{
id: 'whymyrating-templates',
label: 'whymyrating-templates',
description: 'Email & Doc Templates',
icon: '📄',
color: '#10b981',
tech: ['Next.js', 'React Email', 'TypeScript'],
x: 90,
y: 40,
connections: [],
},
{
id: 'whyrating-hub',
label: 'whyrating-hub',
description: 'Internal Dashboard (You are here)',
icon: '🏠',
color: '#3b82f6',
tech: ['Next.js', 'Tailwind', 'TypeScript'],
x: 50,
y: 75,
connections: [],
},
{
id: 'nuc-server',
label: 'NUC Server',
description: 'Self-hosted Infrastructure',
icon: '🖥️',
color: '#6366f1',
tech: ['Docker', 'PostgreSQL', 'Nginx', 'Gitea'],
x: 10,
y: 75,
connections: ['whyrating-hub'],
},
];
const DiagramNode = ({
node,
index,
onHover,
isHighlighted,
}: {
node: NodeConfig;
index: number;
onHover: (id: string | null) => void;
isHighlighted: boolean;
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), index * 100);
return () => clearTimeout(timer);
}, [index]);
return (
<div
className="absolute transform -translate-x-1/2 transition-all duration-500 cursor-pointer"
style={{
left: `${node.x}%`,
top: `${node.y}%`,
opacity: isVisible ? 1 : 0,
transform: `translate(-50%, ${isVisible ? '0' : '20px'}) scale(${isHovered ? 1.05 : 1})`,
zIndex: isHovered ? 10 : 1,
filter: isHighlighted || isHovered ? 'none' : isHighlighted === false ? 'opacity(0.4)' : 'none',
}}
onMouseEnter={() => {
setIsHovered(true);
onHover(node.id);
}}
onMouseLeave={() => {
setIsHovered(false);
onHover(null);
}}
>
<div
className="relative rounded-2xl p-4 min-w-[180px] max-w-[200px]"
style={{
background: `linear-gradient(135deg, ${node.color}, ${node.color}dd)`,
boxShadow: isHovered
? `0 20px 40px -10px ${node.color}66, 0 0 0 2px ${node.color}`
: '0 10px 30px -10px rgba(0,0,0,0.3)',
}}
>
{/* Pulse animation for hovered node */}
{isHovered && (
<div
className="absolute inset-0 rounded-2xl animate-ping"
style={{
background: node.color,
opacity: 0.3,
animationDuration: '1.5s',
}}
/>
)}
{/* Icon */}
<div className="text-2xl mb-2 drop-shadow-md">{node.icon}</div>
{/* Label */}
<div className="text-white font-bold text-sm mb-1 drop-shadow-sm">
{node.label}
</div>
{/* Description */}
<div className="text-white/80 text-xs leading-relaxed">
{node.description}
</div>
{/* Tech stack - shown on hover */}
<div
className={`flex flex-wrap gap-1 mt-3 transition-all duration-300 overflow-hidden ${
isHovered ? 'max-h-20 opacity-100' : 'max-h-0 opacity-0'
}`}
>
{node.tech.map((t, i) => (
<span
key={i}
className="bg-white/20 text-white text-[10px] px-2 py-0.5 rounded-full backdrop-blur-sm"
>
{t}
</span>
))}
</div>
</div>
</div>
);
};
const ConnectionLine = ({
from,
to,
isHighlighted,
delay,
}: {
from: NodeConfig;
to: NodeConfig;
isHighlighted: boolean;
delay: number;
}) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), delay);
return () => clearTimeout(timer);
}, [delay]);
// Calculate line positions (center of nodes)
const x1 = from.x;
const y1 = from.y + 8; // Offset for node height
const x2 = to.x;
const y2 = to.y;
// Calculate control points for curved line
const midY = (y1 + y2) / 2;
return (
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
style={{ zIndex: 0 }}
>
<defs>
<linearGradient id={`gradient-${from.id}-${to.id}`} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={from.color} stopOpacity="0.6" />
<stop offset="100%" stopColor={to.color} stopOpacity="0.6" />
</linearGradient>
</defs>
<path
d={`M ${x1}% ${y1}% Q ${x1}% ${midY}% ${(x1 + x2) / 2}% ${midY}% T ${x2}% ${y2}%`}
fill="none"
stroke={`url(#gradient-${from.id}-${to.id})`}
strokeWidth={isHighlighted ? 3 : 2}
strokeDasharray="8 4"
strokeLinecap="round"
className="transition-all duration-300"
style={{
opacity: isVisible ? (isHighlighted ? 1 : 0.4) : 0,
strokeDashoffset: isVisible ? 0 : 100,
animation: isVisible ? 'dash 20s linear infinite' : 'none',
}}
/>
{/* Animated dot along the path */}
{isHighlighted && isVisible && (
<circle r="4" fill={from.color}>
<animateMotion
dur="3s"
repeatCount="indefinite"
path={`M ${x1}% ${y1}% Q ${x1}% ${midY}% ${(x1 + x2) / 2}% ${midY}% T ${x2}% ${y2}%`}
/>
</circle>
)}
</svg>
);
};
export default function InfrastructureDiagram() {
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="flex items-center justify-center h-[500px]">
<div className="animate-pulse text-stone-500">Loading diagram...</div>
</div>
);
}
// Get all connections
const connections: { from: NodeConfig; to: NodeConfig }[] = [];
nodes.forEach((node) => {
node.connections.forEach((targetId) => {
const target = nodes.find((n) => n.id === targetId);
if (target) {
connections.push({ from: node, to: target });
}
});
});
// Determine which nodes/connections are highlighted
const getHighlightedNodes = () => {
if (!hoveredNode) return null;
const node = nodes.find((n) => n.id === hoveredNode);
if (!node) return null;
return new Set([hoveredNode, ...node.connections]);
};
const highlightedSet = getHighlightedNodes();
return (
<div className="relative">
{/* Background grid */}
<div
className="absolute inset-0 opacity-10 rounded-xl"
style={{
backgroundImage: `
radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0)
`,
backgroundSize: '24px 24px',
}}
/>
{/* Diagram container */}
<div className="relative h-[500px] w-full">
{/* Connection lines */}
{connections.map(({ from, to }, i) => (
<ConnectionLine
key={`${from.id}-${to.id}`}
from={from}
to={to}
isHighlighted={
highlightedSet === null ||
(highlightedSet.has(from.id) && highlightedSet.has(to.id))
}
delay={600 + i * 100}
/>
))}
{/* Nodes */}
{nodes.map((node, i) => (
<DiagramNode
key={node.id}
node={node}
index={i}
onHover={setHoveredNode}
isHighlighted={highlightedSet === null || highlightedSet.has(node.id)}
/>
))}
</div>
{/* Legend */}
<div className="mt-8 flex flex-wrap gap-4 justify-center">
{[
{ color: '#8b5cf6', label: 'Main Apps' },
{ color: '#ef4444', label: 'Backend/AI' },
{ color: '#f59e0b', label: 'Brand' },
{ color: '#10b981', label: 'Templates' },
{ color: '#3b82f6', label: 'Dashboard' },
{ color: '#6366f1', label: 'Infrastructure' },
].map((item) => (
<div key={item.label} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ background: item.color }}
/>
<span className="text-xs text-stone-500">{item.label}</span>
</div>
))}
</div>
{/* Global styles for animations */}
<style jsx global>{`
@keyframes dash {
to {
stroke-dashoffset: -100;
}
}
`}</style>
</div>
);
}

View File

@@ -1,2 +1,3 @@
export { Icon, icons } from './Icons';
export { WhyMyRatingLogo } from './WhyMyRatingLogo';
export { default as InfrastructureDiagram } from './InfrastructureDiagram';