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

228 lines
8.0 KiB
JavaScript

import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
import axios from 'axios'
import PedigreeTree from '../components/PedigreeTree'
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
function PedigreeView() {
const { id } = useParams()
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [dog, setDog] = useState(null)
const [pedigreeData, setPedigreeData] = useState(null)
const [coiData, setCoiData] = useState(null)
const [generations, setGenerations] = useState(5)
useEffect(() => {
fetchPedigreeData()
}, [id, generations])
const fetchPedigreeData = async () => {
setLoading(true)
setError('')
try {
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
const dogData = pedigreeRes.data
setDog(dogData)
const treeData = transformPedigreeData(dogData, generations)
setPedigreeData(treeData)
try {
const coiRes = await axios.get(`/api/pedigree/${id}/coi`)
setCoiData(coiRes.data)
} catch (coiError) {
console.warn('COI calculation unavailable:', coiError)
setCoiData(null)
}
setLoading(false)
} catch (err) {
console.error('Error fetching pedigree:', err)
setError(err.response?.data?.error || 'Failed to load pedigree data')
setLoading(false)
}
}
const completeness = pedigreeData ? getPedigreeCompleteness(pedigreeData, generations) : 0
const coiInfo = formatCOI(coiData?.coi)
if (loading) {
return (
<div className="container">
<div className="loading" style={{ textAlign: 'center', padding: '4rem' }}>
<Loader size={48} style={{ animation: 'spin 1s linear infinite', margin: '0 auto 1rem' }} />
<p>Loading pedigree data...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="container">
<div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
<AlertCircle size={64} style={{ color: 'var(--danger)', margin: '0 auto 1rem' }} />
<h2>Error Loading Pedigree</h2>
<p style={{ color: 'var(--text-secondary)', marginTop: '0.5rem' }}>{error}</p>
<button
className="btn btn-primary"
onClick={() => navigate('/dogs')}
style={{ marginTop: '1.5rem' }}
>
Back to Dogs
</button>
</div>
</div>
)
}
// Completeness bar colour — uses theme tokens
const barColor = completeness === 100 ? 'var(--success)' : 'var(--primary)'
return (
<div className="container">
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>
<button
className="btn btn-secondary"
onClick={() => navigate(`/dogs/${id}`)}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}
>
<ArrowLeft size={20} />
Back to Profile
</button>
<div style={{ flex: 1 }}>
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
{dog?.name}'s Pedigree
</h1>
{dog?.registration_number && (
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
Registration: {dog.registration_number}
</p>
)}
</div>
</div>
{/* Stats Bar */}
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
{/* COI */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Coefficient of Inbreeding
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: coiInfo.color }}>
{coiInfo.value}
</span>
<span style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: coiInfo.color + '20',
color: coiInfo.color,
textTransform: 'uppercase',
fontWeight: '600'
}}>
{coiInfo.level}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
{coiInfo.description}
</div>
</div>
{/* Completeness */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Pedigree Completeness
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--text-primary)' }}>
{completeness}%
</div>
<div style={{ marginTop: '0.5rem' }}>
<div style={{
height: '6px',
background: 'var(--bg-tertiary)',
borderRadius: '3px',
overflow: 'hidden',
border: '1px solid var(--border)'
}}>
<div style={{
height: '100%',
width: `${completeness}%`,
background: barColor,
borderRadius: '3px',
transition: 'width 0.4s ease',
boxShadow: `0 0 6px ${barColor}`
}} />
</div>
</div>
</div>
{/* Generations */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Generations Displayed
</div>
<select
className="input"
value={generations}
onChange={(e) => setGenerations(Number(e.target.value))}
style={{ marginTop: '0.25rem' }}
>
<option value={3}>3 Generations</option>
<option value={4}>4 Generations</option>
<option value={5}>5 Generations</option>
</select>
</div>
</div>
</div>
{/* Pedigree Tree */}
<div className="card" style={{ padding: 0 }}>
{pedigreeData ? (
<PedigreeTree
dogId={id}
pedigreeData={pedigreeData}
coi={coiData?.coi}
/>
) : (
<div style={{ textAlign: 'center', padding: '4rem' }}>
<GitBranch size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem' }} />
<h3>No Pedigree Data Available</h3>
<p style={{ color: 'var(--text-secondary)' }}>
Add parent information to this dog to build the pedigree tree.
</p>
</div>
)}
</div>
{/* Tip */}
<div className="card" style={{
marginTop: '1rem',
background: 'var(--bg-elevated)',
border: '1px solid var(--border-light)'
}}>
<div style={{ fontSize: '0.875rem', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ color: 'var(--primary)' }}>&#128161;</span>
<span>
<strong style={{ color: 'var(--text-primary)' }}>Tip:</strong>{' '}
Click any ancestor node to navigate to their profile.
Use the zoom controls or scroll to explore the tree, and drag to pan.
</span>
</div>
</div>
</div>
)
}
export default PedigreeView