feat: DogList — render ChampionBadge and ChampionBloodlineBadge on dog cards
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user