Add InfrastructureDiagram component missing from previous commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
353
src/components/InfrastructureDiagram.tsx
Normal file
353
src/components/InfrastructureDiagram.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { Icon, icons } from './Icons';
|
||||
export { WhyMyRatingLogo } from './WhyMyRatingLogo';
|
||||
export { default as InfrastructureDiagram } from './InfrastructureDiagram';
|
||||
|
||||
Reference in New Issue
Block a user