Redesign: Compact photo gallery with modern info layout
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { Dog, GitBranch, Edit, Upload, Trash2 } from 'lucide-react'
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom'
|
||||
import { Dog, GitBranch, Edit, Upload, Trash2, ArrowLeft, Calendar, Hash, Award } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import DogForm from '../components/DogForm'
|
||||
|
||||
function DogDetail() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [dog, setDog] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [selectedPhoto, setSelectedPhoto] = useState(0)
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,12 +57,32 @@ function DogDetail() {
|
||||
try {
|
||||
await axios.delete(`/api/dogs/${id}/photos/${photoIndex}`)
|
||||
fetchDog()
|
||||
if (selectedPhoto >= photoIndex && selectedPhoto > 0) {
|
||||
setSelectedPhoto(selectedPhoto - 1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo:', error)
|
||||
alert('Failed to delete photo')
|
||||
}
|
||||
}
|
||||
|
||||
const calculateAge = (birthDate) => {
|
||||
if (!birthDate) return null
|
||||
const today = new Date()
|
||||
const birth = new Date(birthDate)
|
||||
let years = today.getFullYear() - birth.getFullYear()
|
||||
let months = today.getMonth() - birth.getMonth()
|
||||
|
||||
if (months < 0) {
|
||||
years--
|
||||
months += 12
|
||||
}
|
||||
|
||||
if (years === 0) return `${months} month${months !== 1 ? 's' : ''}`
|
||||
if (months === 0) return `${years} year${years !== 1 ? 's' : ''}`
|
||||
return `${years}y ${months}m`
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="container loading">Loading...</div>
|
||||
}
|
||||
@@ -70,64 +92,51 @@ function DogDetail() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
<h1>{dog.name}</h1>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Link to={`/pedigree/${dog.id}`} className="btn btn-primary">
|
||||
<GitBranch size={20} />
|
||||
View Pedigree
|
||||
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
|
||||
<button className="btn-icon" onClick={() => navigate('/dogs')} style={{ marginRight: '0.5rem' }}>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h1 style={{ marginBottom: '0.25rem' }}>{dog.name}</h1>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', color: 'var(--text-secondary)' }}>
|
||||
<span>{dog.breed}</span>
|
||||
<span>•</span>
|
||||
<span>{dog.sex === 'male' ? 'Male ♂' : 'Female ♀'}</span>
|
||||
{dog.birth_date && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{calculateAge(dog.birth_date)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||
<Link to={`/pedigree/${dog.id}`} className="btn btn-ghost">
|
||||
<GitBranch size={18} />
|
||||
Pedigree
|
||||
</Link>
|
||||
<button className="btn btn-secondary" onClick={() => setShowEditModal(true)}>
|
||||
<Edit size={20} />
|
||||
<button className="btn btn-primary" onClick={() => setShowEditModal(true)}>
|
||||
<Edit size={18} />
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-2">
|
||||
<div className="card">
|
||||
<h2 style={{ marginBottom: '1rem' }}>Basic Information</h2>
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
<div>
|
||||
<strong>Breed:</strong> {dog.breed}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Sex:</strong> {dog.sex === 'male' ? 'Male ♂' : 'Female ♀'}
|
||||
</div>
|
||||
{dog.birth_date && (
|
||||
<div>
|
||||
<strong>Birth Date:</strong> {new Date(dog.birth_date).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
{dog.color && (
|
||||
<div>
|
||||
<strong>Color:</strong> {dog.color}
|
||||
</div>
|
||||
)}
|
||||
{dog.registration_number && (
|
||||
<div>
|
||||
<strong>Registration:</strong> {dog.registration_number}
|
||||
</div>
|
||||
)}
|
||||
{dog.microchip && (
|
||||
<div>
|
||||
<strong>Microchip:</strong> {dog.microchip}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2>Photos</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '1.5rem', marginBottom: '1.5rem' }}>
|
||||
{/* Photo Section - Compact */}
|
||||
<div className="card" style={{ padding: '1rem' }}>
|
||||
<div style={{ marginBottom: '0.75rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3 style={{ fontSize: '0.875rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)' }}>Photos</h3>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
style={{ padding: '0.375rem 0.75rem', fontSize: '0.75rem' }}
|
||||
>
|
||||
<Upload size={18} />
|
||||
{uploading ? 'Uploading...' : 'Upload'}
|
||||
<Upload size={14} />
|
||||
{uploading ? 'Uploading...' : 'Add'}
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -137,75 +146,186 @@ function DogDetail() {
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{dog.photo_urls && dog.photo_urls.length > 0 ? (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '0.5rem' }}>
|
||||
{dog.photo_urls.map((url, index) => (
|
||||
<div key={index} style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={url}
|
||||
alt={`${dog.name} ${index + 1}`}
|
||||
style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: '0.375rem' }}
|
||||
/>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => handleDeletePhoto(index)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0.25rem',
|
||||
right: '0.25rem',
|
||||
background: 'rgba(255,255,255,0.9)'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} color="var(--danger)" />
|
||||
</button>
|
||||
<>
|
||||
{/* Main Photo */}
|
||||
<div style={{ position: 'relative', marginBottom: '0.75rem' }}>
|
||||
<img
|
||||
src={dog.photo_urls[selectedPhoto]}
|
||||
alt={dog.name}
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 'var(--radius)',
|
||||
border: '1px solid var(--border)'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => handleDeletePhoto(selectedPhoto)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0.5rem',
|
||||
right: '0.5rem',
|
||||
background: 'rgba(15, 23, 42, 0.8)',
|
||||
backdropFilter: 'blur(8px)'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} color="var(--danger)" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Strip */}
|
||||
{dog.photo_urls.length > 1 && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', overflowX: 'auto' }}>
|
||||
{dog.photo_urls.map((url, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={url}
|
||||
alt={`${dog.name} ${index + 1}`}
|
||||
onClick={() => setSelectedPhoto(index)}
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
border: selectedPhoto === index ? '2px solid var(--primary)' : '1px solid var(--border)',
|
||||
opacity: selectedPhoto === index ? 1 : 0.6,
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '2rem', background: 'var(--bg-secondary)', borderRadius: '0.375rem' }}>
|
||||
<Dog size={48} style={{ color: 'var(--text-secondary)', margin: '0 auto 0.5rem' }} />
|
||||
<p style={{ color: 'var(--text-secondary)' }}>No photos uploaded</p>
|
||||
<div style={{ textAlign: 'center', padding: '3rem 1rem', background: 'var(--bg-primary)', borderRadius: 'var(--radius)', border: '1px dashed var(--border)' }}>
|
||||
<Dog size={48} style={{ color: 'var(--text-muted)', margin: '0 auto 0.5rem', opacity: 0.5 }} />
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}>No photos</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div>
|
||||
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Details</h2>
|
||||
|
||||
<div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">Breed</span>
|
||||
<span className="info-value">{dog.breed}</span>
|
||||
</div>
|
||||
|
||||
<div className="info-row">
|
||||
<span className="info-label">Sex</span>
|
||||
<span className="info-value">{dog.sex === 'male' ? 'Male ♂' : 'Female ♀'}</span>
|
||||
</div>
|
||||
|
||||
{dog.birth_date && (
|
||||
<div className="info-row">
|
||||
<span className="info-label"><Calendar size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Birth Date</span>
|
||||
<span className="info-value">
|
||||
{new Date(dog.birth_date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||||
<span style={{ marginLeft: '0.5rem', color: 'var(--text-muted)', fontSize: '0.875rem' }}>({calculateAge(dog.birth_date)})</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dog.color && (
|
||||
<div className="info-row">
|
||||
<span className="info-label">Color</span>
|
||||
<span className="info-value">{dog.color}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dog.registration_number && (
|
||||
<div className="info-row">
|
||||
<span className="info-label"><Award size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Registration</span>
|
||||
<span className="info-value" style={{ fontFamily: 'monospace' }}>{dog.registration_number}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dog.microchip && (
|
||||
<div className="info-row">
|
||||
<span className="info-label"><Hash size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Microchip</span>
|
||||
<span className="info-value" style={{ fontFamily: 'monospace' }}>{dog.microchip}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parents */}
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Pedigree</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sire</div>
|
||||
{dog.sire ? (
|
||||
<Link to={`/dogs/${dog.sire.id}`} style={{ color: 'var(--primary)', fontWeight: 500, textDecoration: 'none' }}>
|
||||
{dog.sire.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>Unknown</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Dam</div>
|
||||
{dog.dam ? (
|
||||
<Link to={`/dogs/${dog.dam.id}`} style={{ color: 'var(--primary)', fontWeight: 500, textDecoration: 'none' }}>
|
||||
{dog.dam.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>Unknown</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{dog.notes && (
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<h2 style={{ marginBottom: '1rem' }}>Notes</h2>
|
||||
<p style={{ whiteSpace: 'pre-wrap' }}>{dog.notes}</p>
|
||||
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Notes</h2>
|
||||
<p style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6', color: 'var(--text-secondary)' }}>{dog.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<h2 style={{ marginBottom: '1rem' }}>Parents</h2>
|
||||
<div className="grid grid-2">
|
||||
<div>
|
||||
<h3>Sire (Father)</h3>
|
||||
{dog.sire ? (
|
||||
<Link to={`/dogs/${dog.sire.id}`} style={{ color: 'var(--primary)' }}>{dog.sire.name}</Link>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-secondary)' }}>Unknown</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Dam (Mother)</h3>
|
||||
{dog.dam ? (
|
||||
<Link to={`/dogs/${dog.dam.id}`} style={{ color: 'var(--primary)' }}>{dog.dam.name}</Link>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-secondary)' }}>Unknown</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offspring */}
|
||||
{dog.offspring && dog.offspring.length > 0 && (
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<h2 style={{ marginBottom: '1rem' }}>Offspring ({dog.offspring.length})</h2>
|
||||
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Offspring ({dog.offspring.length})</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '0.75rem' }}>
|
||||
{dog.offspring.map(child => (
|
||||
<Link key={child.id} to={`/dogs/${child.id}`} style={{ color: 'var(--primary)' }}>
|
||||
{child.name} - {child.sex === 'male' ? '♂' : '♀'}
|
||||
<Link
|
||||
key={child.id}
|
||||
to={`/dogs/${child.id}`}
|
||||
style={{
|
||||
padding: '0.75rem 1rem',
|
||||
background: 'var(--bg-primary)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
textDecoration: 'none',
|
||||
transition: 'var(--transition)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--primary)'
|
||||
e.currentTarget.style.background = 'var(--bg-tertiary)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--border)'
|
||||
e.currentTarget.style.background = 'var(--bg-primary)'
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{child.name}</span>
|
||||
<span style={{ fontSize: '1.125rem' }}>{child.sex === 'male' ? '♂' : '♀'}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user