Redesign: Horizontal info cards with avatars for Dogs list
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Dog, Plus, Search } 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'
|
||||||
|
|
||||||
@@ -52,68 +52,224 @@ function DogList() {
|
|||||||
fetchDogs()
|
fetchDogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}mo`
|
||||||
|
if (months === 0) return `${years}y`
|
||||||
|
return `${years}y ${months}mo`
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="container loading">Loading dogs...</div>
|
return <div className="container loading">Loading dogs...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||||
<h1>Dogs</h1>
|
<div>
|
||||||
|
<h1 style={{ marginBottom: '0.25rem' }}>Dogs</h1>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
|
||||||
|
{filteredDogs.length} {filteredDogs.length === 1 ? 'dog' : 'dogs'}
|
||||||
|
{search || sexFilter !== 'all' ? ' matching filters' : ' total'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
<Plus size={20} />
|
<Plus size={18} />
|
||||||
Add Dog
|
Add Dog
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ marginBottom: '2rem' }}>
|
{/* Search and Filter Bar */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem' }}>
|
<div className="card" style={{ marginBottom: '1.5rem', padding: '1rem' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto auto', gap: '1rem', alignItems: 'center' }}>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<Search size={20} style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }} />
|
<Search size={18} style={{ position: 'absolute', left: '0.875rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input"
|
||||||
placeholder="Search by name or registration number..."
|
placeholder="Search by name or registration..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
style={{ paddingLeft: '2.5rem' }}
|
style={{ paddingLeft: '2.75rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: 'auto' }}>
|
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '140px' }}>
|
||||||
<option value="all">All</option>
|
<option value="all">All Dogs</option>
|
||||||
<option value="male">Males</option>
|
<option value="male">Males ♂</option>
|
||||||
<option value="female">Females</option>
|
<option value="female">Females ♀</option>
|
||||||
</select>
|
</select>
|
||||||
|
{(search || sexFilter !== 'all') && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setSearch('')
|
||||||
|
setSexFilter('all')
|
||||||
|
}}
|
||||||
|
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-3">
|
{/* Dogs List */}
|
||||||
{filteredDogs.map(dog => (
|
{filteredDogs.length === 0 ? (
|
||||||
<Link key={dog.id} to={`/dogs/${dog.id}`} className="card" style={{ textDecoration: 'none', color: 'inherit' }}>
|
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||||
<div style={{ aspectRatio: '1', background: 'var(--bg-secondary)', borderRadius: '0.375rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Dog size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
||||||
{dog.photo_urls && dog.photo_urls.length > 0 ? (
|
<h3 style={{ marginBottom: '0.5rem' }}>
|
||||||
<img src={dog.photo_urls[0]} alt={dog.name} style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '0.375rem' }} />
|
{search || sexFilter !== 'all' ? 'No dogs found' : 'No dogs yet'}
|
||||||
) : (
|
</h3>
|
||||||
<Dog size={48} style={{ color: 'var(--text-secondary)' }} />
|
<p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>
|
||||||
)}
|
{search || sexFilter !== 'all'
|
||||||
</div>
|
? 'Try adjusting your search or filters'
|
||||||
<h3>{dog.name}</h3>
|
: 'Add your first dog to get started'}
|
||||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
|
</p>
|
||||||
{dog.breed} • {dog.sex === 'male' ? '♂' : '♀'}
|
{!search && sexFilter === 'all' && (
|
||||||
</p>
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
{dog.registration_number && (
|
<Plus size={18} />
|
||||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.75rem', marginTop: '0.25rem' }}>{dog.registration_number}</p>
|
Add Your First Dog
|
||||||
)}
|
</button>
|
||||||
{dog.birth_date && (
|
)}
|
||||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.75rem' }}>Born: {new Date(dog.birth_date).toLocaleDateString()}</p>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</Link>
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
))}
|
{filteredDogs.map(dog => (
|
||||||
</div>
|
<Link
|
||||||
|
key={dog.id}
|
||||||
|
to={`/dogs/${dog.id}`}
|
||||||
|
className="card"
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
transition: 'var(--transition)',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--primary)'
|
||||||
|
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)'
|
||||||
|
e.currentTarget.style.boxShadow = 'var(--shadow-sm)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Avatar Photo */}
|
||||||
|
<div style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '2px solid var(--border)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{dog.photo_urls && dog.photo_urls.length > 0 ? (
|
||||||
|
<img
|
||||||
|
src={dog.photo_urls[0]}
|
||||||
|
alt={dog.name}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Dog size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{filteredDogs.length === 0 && (
|
{/* Info Section */}
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>No dogs found matching your search criteria.</p>
|
<h3 style={{
|
||||||
|
fontSize: '1.125rem',
|
||||||
|
marginBottom: '0.375rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}>
|
||||||
|
{dog.name}
|
||||||
|
<span style={{
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: dog.sex === 'male' ? 'var(--primary)' : '#ec4899'
|
||||||
|
}}>
|
||||||
|
{dog.sex === 'male' ? '♂' : '♀'}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.75rem',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
marginBottom: '0.5rem'
|
||||||
|
}}>
|
||||||
|
<span>{dog.breed}</span>
|
||||||
|
{dog.birth_date && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||||
|
<Calendar size={12} />
|
||||||
|
{calculateAge(dog.birth_date)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{dog.color && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{dog.color}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dog.registration_number && (
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: 'var(--text-muted)'
|
||||||
|
}}>
|
||||||
|
<Hash size={10} />
|
||||||
|
{dog.registration_number}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrow Indicator */}
|
||||||
|
<div style={{
|
||||||
|
opacity: 0.5,
|
||||||
|
transition: 'var(--transition)'
|
||||||
|
}}>
|
||||||
|
<ArrowRight size={20} color="var(--text-muted)" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user