Files
breedr/client/src/components/PedigreeView.jsx

235 lines
6.5 KiB
React
Raw Normal View History

import { useState, useEffect, useCallback } from 'react'
import { X, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react'
import Tree from 'react-d3-tree'
import axios from 'axios'
import './PedigreeView.css'
function PedigreeView({ dogId, onClose }) {
const [treeData, setTreeData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [translate, setTranslate] = useState({ x: 0, y: 0 })
const [zoom, setZoom] = useState(0.8)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
fetchPedigree()
}, [dogId])
useEffect(() => {
const updateDimensions = () => {
const container = document.querySelector('.pedigree-container')
if (container) {
const width = container.offsetWidth
const height = container.offsetHeight
setDimensions({ width, height })
setTranslate({ x: width / 4, y: height / 2 })
}
}
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [])
const fetchPedigree = async () => {
try {
setLoading(true)
const response = await axios.get(`/api/pedigree/${dogId}?generations=5`)
const formatted = formatTreeData(response.data)
setTreeData(formatted)
} catch (err) {
setError(err.response?.data?.error || 'Failed to load pedigree')
} finally {
setLoading(false)
}
}
const formatTreeData = (dog) => {
if (!dog) return null
const children = []
if (dog.sire) children.push(formatTreeData(dog.sire))
if (dog.dam) children.push(formatTreeData(dog.dam))
return {
name: dog.name,
attributes: {
sex: dog.sex,
birth_date: dog.birth_date,
registration: dog.registration_number,
breed: dog.breed,
color: dog.color,
generation: dog.generation
},
children: children.length > 0 ? children : undefined
}
}
const handleNodeClick = useCallback((nodeData) => {
console.log('Node clicked:', nodeData)
}, [])
const handleZoomIn = () => {
setZoom(prev => Math.min(prev + 0.2, 2))
}
const handleZoomOut = () => {
setZoom(prev => Math.max(prev - 0.2, 0.4))
}
const handleReset = () => {
setZoom(0.8)
setTranslate({ x: dimensions.width / 4, y: dimensions.height / 2 })
}
const renderCustomNode = ({ nodeDatum, toggleNode }) => (
<g>
<circle
r="20"
fill={nodeDatum.attributes.sex === 'male' ? '#3b82f6' : '#ec4899'}
stroke="#fff"
strokeWidth="2"
onClick={toggleNode}
style={{ cursor: 'pointer' }}
/>
<text
fill="#fff"
strokeWidth="0"
x="0"
y="5"
textAnchor="middle"
fontSize="12"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.attributes.sex === 'male' ? '♂' : '♀'}
</text>
<text
fill="#1f2937"
x="30"
y="-10"
fontSize="14"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.name}
</text>
{nodeDatum.attributes.registration && (
<text
fill="#6b7280"
x="30"
y="8"
fontSize="11"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.attributes.registration}
</text>
)}
{nodeDatum.attributes.birth_date && (
<text
fill="#6b7280"
x="30"
y="22"
fontSize="10"
style={{ pointerEvents: 'none' }}
>
Born: {new Date(nodeDatum.attributes.birth_date).getFullYear()}
</text>
)}
</g>
)
if (loading) {
return (
<div className="modal-overlay">
<div className="pedigree-modal">
<div className="loading">Loading pedigree...</div>
</div>
</div>
)
}
if (error) {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="pedigree-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Pedigree Tree</h2>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
</div>
<div className="error">{error}</div>
</div>
</div>
)
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="pedigree-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Pedigree Tree - {treeData?.name}</h2>
<div className="pedigree-controls">
<button className="btn-icon" onClick={handleZoomOut} title="Zoom Out">
<ZoomOut size={20} />
</button>
<button className="btn-icon" onClick={handleZoomIn} title="Zoom In">
<ZoomIn size={20} />
</button>
<button className="btn-icon" onClick={handleReset} title="Reset View">
<Maximize2 size={20} />
</button>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
</div>
</div>
<div className="pedigree-legend">
<div className="legend-item">
<span className="legend-color male"></span>
<span>Male</span>
</div>
<div className="legend-item">
<span className="legend-color female"></span>
<span>Female</span>
</div>
</div>
<div className="pedigree-container">
{treeData && dimensions.width > 0 && (
<Tree
data={treeData}
translate={translate}
zoom={zoom}
onNodeClick={handleNodeClick}
renderCustomNodeElement={renderCustomNode}
orientation="horizontal"
pathFunc="step"
separation={{ siblings: 2, nonSiblings: 2.5 }}
nodeSize={{ x: 200, y: 100 }}
enableLegacyTransitions
transitionDuration={300}
collapsible={false}
zoomable={true}
draggable={true}
dimensions={dimensions}
/>
)}
</div>
<div className="pedigree-info">
<p>
<strong>Tip:</strong> Use mouse wheel to zoom, click and drag to pan.
Click on nodes to view details.
</p>
</div>
</div>
</div>
)
}
export default PedigreeView