feat: DogList — render ChampionBadge and ChampionBloodlineBadge on dog cards

This commit is contained in:
2026-03-09 22:18:28 -05:00
parent ec249c7865
commit 9e699e308f

View File

@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
import { Dog, Plus, Search, Calendar, Hash, ArrowRight } from 'lucide-react' import { Dog, Plus, Search, Calendar, Hash, ArrowRight } from 'lucide-react'
import axios from 'axios' import axios from 'axios'
import DogForm from '../components/DogForm' import DogForm from '../components/DogForm'
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
function DogList() { function DogList() {
const [dogs, setDogs] = useState([]) const [dogs, setDogs] = useState([])
@@ -12,13 +13,8 @@ function DogList() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false)
useEffect(() => { useEffect(() => { fetchDogs() }, [])
fetchDogs() useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
}, [])
useEffect(() => {
filterDogs()
}, [dogs, search, sexFilter])
const fetchDogs = async () => { const fetchDogs = async () => {
try { try {
@@ -33,24 +29,19 @@ function DogList() {
const filterDogs = () => { const filterDogs = () => {
let filtered = dogs let filtered = dogs
if (search) { if (search) {
filtered = filtered.filter(dog => filtered = filtered.filter(dog =>
dog.name.toLowerCase().includes(search.toLowerCase()) || dog.name.toLowerCase().includes(search.toLowerCase()) ||
(dog.registration_number && dog.registration_number.toLowerCase().includes(search.toLowerCase())) (dog.registration_number && dog.registration_number.toLowerCase().includes(search.toLowerCase()))
) )
} }
if (sexFilter !== 'all') { if (sexFilter !== 'all') {
filtered = filtered.filter(dog => dog.sex === sexFilter) filtered = filtered.filter(dog => dog.sex === sexFilter)
} }
setFilteredDogs(filtered) setFilteredDogs(filtered)
} }
const handleSave = () => { const handleSave = () => { fetchDogs() }
fetchDogs()
}
const calculateAge = (birthDate) => { const calculateAge = (birthDate) => {
if (!birthDate) return null if (!birthDate) return null
@@ -58,17 +49,16 @@ function DogList() {
const birth = new Date(birthDate) const birth = new Date(birthDate)
let years = today.getFullYear() - birth.getFullYear() let years = today.getFullYear() - birth.getFullYear()
let months = today.getMonth() - birth.getMonth() let months = today.getMonth() - birth.getMonth()
if (months < 0) { years--; months += 12 }
if (months < 0) {
years--
months += 12
}
if (years === 0) return `${months}mo` if (years === 0) return `${months}mo`
if (months === 0) return `${years}y` if (months === 0) return `${years}y`
return `${years}y ${months}mo` return `${years}y ${months}mo`
} }
// A dog has champion blood if sire or dam is a champion
const hasChampionBlood = (dog) =>
(dog.sire && dog.sire.is_champion) || (dog.dam && dog.dam.is_champion)
if (loading) { if (loading) {
return <div className="container loading">Loading dogs...</div> return <div className="container loading">Loading dogs...</div>
} }
@@ -109,12 +99,9 @@ function DogList() {
<option value="female">Females </option> <option value="female">Females </option>
</select> </select>
{(search || sexFilter !== 'all') && ( {(search || sexFilter !== 'all') && (
<button <button
className="btn btn-ghost" className="btn btn-ghost"
onClick={() => { onClick={() => { setSearch(''); setSexFilter('all') }}
setSearch('')
setSexFilter('all')
}}
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }} style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
> >
Clear Clear
@@ -131,8 +118,8 @@ function DogList() {
{search || sexFilter !== 'all' ? 'No dogs found' : 'No dogs yet'} {search || sexFilter !== 'all' ? 'No dogs found' : 'No dogs yet'}
</h3> </h3>
<p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}> <p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>
{search || sexFilter !== 'all' {search || sexFilter !== 'all'
? 'Try adjusting your search or filters' ? 'Try adjusting your search or filters'
: 'Add your first dog to get started'} : 'Add your first dog to get started'}
</p> </p>
{!search && sexFilter === 'all' && ( {!search && sexFilter === 'all' && (
@@ -145,13 +132,13 @@ function DogList() {
) : ( ) : (
<div style={{ display: 'grid', gap: '1rem' }}> <div style={{ display: 'grid', gap: '1rem' }}>
{filteredDogs.map(dog => ( {filteredDogs.map(dog => (
<Link <Link
key={dog.id} key={dog.id}
to={`/dogs/${dog.id}`} to={`/dogs/${dog.id}`}
className="card" className="card"
style={{ style={{
padding: '1rem', padding: '1rem',
textDecoration: 'none', textDecoration: 'none',
display: 'flex', display: 'flex',
gap: '1rem', gap: '1rem',
alignItems: 'center', alignItems: 'center',
@@ -169,65 +156,60 @@ function DogList() {
e.currentTarget.style.boxShadow = 'var(--shadow-sm)' e.currentTarget.style.boxShadow = 'var(--shadow-sm)'
}} }}
> >
{/* Avatar Photo */} {/* Avatar */}
<div style={{ <div style={{
width: '80px', width: '80px', height: '80px', flexShrink: 0,
height: '80px',
flexShrink: 0,
borderRadius: 'var(--radius)', borderRadius: 'var(--radius)',
background: 'var(--bg-primary)', background: 'var(--bg-primary)',
border: '2px solid var(--border)', border: dog.is_champion
display: 'flex', ? '2px solid var(--champion-gold)'
alignItems: 'center', : hasChampionBlood(dog)
justifyContent: 'center', ? '2px solid var(--bloodline-amber)'
overflow: 'hidden' : '2px solid var(--border)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
overflow: 'hidden',
boxShadow: dog.is_champion
? '0 0 8px var(--champion-glow)'
: hasChampionBlood(dog)
? '0 0 8px var(--bloodline-glow)'
: 'none'
}}> }}>
{dog.photo_urls && dog.photo_urls.length > 0 ? ( {dog.photo_urls && dog.photo_urls.length > 0 ? (
<img <img
src={dog.photo_urls[0]} src={dog.photo_urls[0]}
alt={dog.name} alt={dog.name}
style={{ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/> />
) : ( ) : (
<Dog size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Dog size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
)} )}
</div> </div>
{/* Info Section */} {/* Info */}
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<h3 style={{ <h3 style={{
fontSize: '1.125rem', fontSize: '1.125rem',
marginBottom: '0.375rem', marginBottom: '0.25rem',
overflow: 'hidden', display: 'flex', alignItems: 'center', gap: '0.5rem',
textOverflow: 'ellipsis', flexWrap: 'wrap'
whiteSpace: 'nowrap'
}}> }}>
{dog.name} <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<span style={{ {dog.name}
marginLeft: '0.5rem', </span>
fontSize: '1rem', <span style={{ color: dog.sex === 'male' ? 'var(--primary)' : '#ec4899', fontSize: '1rem' }}>
color: dog.sex === 'male' ? 'var(--primary)' : '#ec4899'
}}>
{dog.sex === 'male' ? '♂' : '♀'} {dog.sex === 'male' ? '♂' : '♀'}
</span> </span>
{dog.is_champion ? <ChampionBadge /> : hasChampionBlood(dog) ? <ChampionBloodlineBadge /> : null}
</h3> </h3>
<div style={{ <div style={{
display: 'flex', display: 'flex', flexWrap: 'wrap', gap: '0.75rem',
flexWrap: 'wrap', fontSize: '0.8125rem', color: 'var(--text-secondary)', marginBottom: '0.5rem'
gap: '0.75rem',
fontSize: '0.8125rem',
color: 'var(--text-secondary)',
marginBottom: '0.5rem'
}}> }}>
<span>{dog.breed}</span> <span>{dog.breed}</span>
{dog.birth_date && ( {dog.birth_date && (
<> <>
<span></span> <span>·</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Calendar size={12} /> <Calendar size={12} />
{calculateAge(dog.birth_date)} {calculateAge(dog.birth_date)}
@@ -236,23 +218,20 @@ function DogList() {
)} )}
{dog.color && ( {dog.color && (
<> <>
<span></span> <span>·</span>
<span>{dog.color}</span> <span>{dog.color}</span>
</> </>
)} )}
</div> </div>
{dog.registration_number && ( {dog.registration_number && (
<div style={{ <div style={{
display: 'inline-flex', display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
alignItems: 'center',
gap: '0.25rem',
padding: '0.25rem 0.5rem', padding: '0.25rem 0.5rem',
background: 'var(--bg-primary)', background: 'var(--bg-primary)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
fontSize: '0.75rem', fontSize: '0.75rem', fontFamily: 'monospace',
fontFamily: 'monospace',
color: 'var(--text-muted)' color: 'var(--text-muted)'
}}> }}>
<Hash size={10} /> <Hash size={10} />
@@ -261,11 +240,7 @@ function DogList() {
)} )}
</div> </div>
{/* Arrow Indicator */} <div style={{ opacity: 0.5, transition: 'var(--transition)' }}>
<div style={{
opacity: 0.5,
transition: 'var(--transition)'
}}>
<ArrowRight size={20} color="var(--text-muted)" /> <ArrowRight size={20} color="var(--text-muted)" />
</div> </div>
</Link> </Link>
@@ -283,4 +258,4 @@ function DogList() {
) )
} }
export default DogList export default DogList