import React, { useEffect, useState, useRef, useCallback } from 'react'; import ForceGraph3D from 'react-force-graph-3d'; import { useNavigate } from 'react-router-dom'; import { fetchGraphData } from '../utils/graphDataManager'; import Loading from '../components/Loading'; import { ArrowLeftIcon, InfoIcon } from '@phosphor-icons/react'; import { Link } from 'react-router-dom'; import Seo from '../components/Seo'; const KnowledgeGraphPage = () => { const [graphData, setGraphData] = useState({ nodes: [], links: [] }); const [loading, setLoading] = useState(true); const [hoverNode, setHoverNode] = useState(null); const fgRef = useRef(); const navigate = useNavigate(); useEffect(() => { const loadData = async () => { const data = await fetchGraphData(); setGraphData(data); setLoading(false); }; loadData(); }, []); const handleNodeClick = useCallback( (node) => { if (!node) return; // Aim at node from outside it const distance = 40; const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z); if (fgRef.current) { fgRef.current.cameraPosition( { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio, }, // new position node, // lookAt ({ x, y, z }) 3000, // ms transition duration ); } // Navigate logic setTimeout(() => { if (node.group === 'post') { navigate(`/blog/${node.slug}`); } else if (node.group === 'app') { navigate(node.to || `/apps/${node.slug}`); } else if (node.group === 'project') { navigate(`/projects/${node.slug}`); } }, 1500); // Wait for half the zoom }, [navigate], ); if (loading) return ; return (
{/* UI Overlay */}
Exit_Sim
Double-click to center. Click to navigate.
Post App Project Tag
{hoverNode && (
{hoverNode.group}
{hoverNode.name}
{hoverNode.desc && (
{hoverNode.desc.substring(0, 100)}...
)}
)} '#ffffff20'} linkWidth={0.5} linkOpacity={0.2} enableNodeDrag={false} />
); }; export default KnowledgeGraphPage;