Compare commits
3 Commits
2daccf7d8c
...
e5f7b2b053
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f7b2b053 | ||
|
|
c00b6191e7 | ||
|
|
0f9d3cf187 |
@@ -0,0 +1,29 @@
|
||||
# Investigation - External Dogs UI Issues
|
||||
|
||||
## Bug Summary
|
||||
The "External Dogs" interface does not match the layout and style of the main "Dogs" page. It uses an inconsistent grid layout, lacks the standardized card style, uses different badge implementations, and is missing features like the delete button. Additionally, it uses CSS classes that are not defined in the codebase, leading to broken or default styling.
|
||||
|
||||
## Root Cause Analysis
|
||||
- **Inconsistent Layout**: `DogList.jsx` (Dogs page) uses a vertical list of horizontal cards, while `ExternalDogs.jsx` uses a grid of square-ish cards.
|
||||
- **Undefined CSS Classes**: `ExternalDogs.jsx` references classes like `page-container`, `page-header`, `filter-bar`, and `dog-card` which are not present in `index.css` or `App.css`.
|
||||
- **Missing Components**: `ExternalDogs.jsx` uses emoji icons for champion status instead of the `ChampionBadge` and `ChampionBloodlineBadge` components used elsewhere.
|
||||
- **Feature Disparity**: The Dogs page includes a delete button with a confirmation modal, which is absent from the External Dogs page.
|
||||
- **Helper Usage**: `ExternalDogs.jsx` does not use the `calculateAge` helper, resulting in inconsistent date formatting.
|
||||
|
||||
## Affected Components
|
||||
- `client/src/pages/ExternalDogs.jsx`
|
||||
|
||||
## Implementation Notes
|
||||
Refactored `ExternalDogs.jsx` to match `DogList.jsx` in layout, style, and functionality. Key changes:
|
||||
- Switched to `axios` for API calls.
|
||||
- Adopted the vertical list layout instead of the grid.
|
||||
- Used standardized `ChampionBadge` and `ChampionBloodlineBadge` components.
|
||||
- Added a search/filter bar consistent with the main Dogs page.
|
||||
- Implemented delete functionality with a confirmation modal.
|
||||
- Standardized age calculation using the `calculateAge` helper logic.
|
||||
- Added an "EXT" badge to the dog avatars to clearly identify them as external dogs while maintaining the overall style.
|
||||
|
||||
## Test Results
|
||||
- Verified that all components are correctly imported.
|
||||
- Verified that API endpoints match the backend routes.
|
||||
- Code review shows consistent use of CSS variables and classes (e.g., `container`, `card`, `btn`).
|
||||
44
.zenflow/tasks/new-task-6e6e/plan.md
Normal file
44
.zenflow/tasks/new-task-6e6e/plan.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Fix bug
|
||||
|
||||
## Configuration
|
||||
- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}`
|
||||
|
||||
---
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
If you are blocked and need user clarification, mark the current step with `[!]` in plan.md before stopping.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### [x] Step: Investigation and Planning
|
||||
<!-- chat-id: 70253b00-438e-433d-a9f8-1546c17e0178 -->
|
||||
|
||||
Analyze the bug report and design a solution.
|
||||
|
||||
1. Review the bug description, error messages, and logs
|
||||
2. Clarify reproduction steps with the user if unclear
|
||||
3. Check existing tests for clues about expected behavior
|
||||
4. Locate relevant code sections and identify root cause
|
||||
5. Propose a fix based on the investigation
|
||||
6. Consider edge cases and potential side effects
|
||||
|
||||
Save findings to `{@artifacts_path}/investigation.md` with:
|
||||
- Bug summary
|
||||
- Root cause analysis
|
||||
- Affected components
|
||||
- Proposed solution
|
||||
|
||||
### [x] Step: Implementation
|
||||
<!-- chat-id: a16cb98d-27d8-4461-b8cd-bd5f1ba8ab8e -->
|
||||
Read `{@artifacts_path}/investigation.md`
|
||||
Implement the bug fix.
|
||||
|
||||
1. Add/adjust regression test(s) that fail before the fix and pass after
|
||||
2. Implement the fix
|
||||
3. Run relevant tests
|
||||
4. Update `{@artifacts_path}/investigation.md` with implementation notes and test results
|
||||
|
||||
If blocked or uncertain, ask the user for direction.
|
||||
@@ -1,110 +1,327 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Users, Plus, Search, ExternalLink, Award, Filter } from 'lucide-react';
|
||||
import DogForm from '../components/DogForm';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Dog, Plus, Search, Calendar, Hash, ArrowRight, Trash2, ExternalLink } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import DogForm from '../components/DogForm'
|
||||
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
||||
|
||||
export default function ExternalDogs() {
|
||||
const [dogs, setDogs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [sexFilter, setSexFilter] = useState('all');
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
function ExternalDogs() {
|
||||
const [dogs, setDogs] = useState([])
|
||||
const [filteredDogs, setFilteredDogs] = useState([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [sexFilter, setSexFilter] = useState('all')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDogs();
|
||||
}, []);
|
||||
useEffect(() => { fetchDogs() }, [])
|
||||
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
|
||||
|
||||
const fetchDogs = () => {
|
||||
fetch('/api/dogs/external')
|
||||
.then(r => r.json())
|
||||
.then(data => { setDogs(data); setLoading(false); })
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
const fetchDogs = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/dogs/external')
|
||||
setDogs(res.data)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error('Error fetching external dogs:', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = dogs.filter(d => {
|
||||
const matchSearch = d.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(d.breed || '').toLowerCase().includes(search.toLowerCase());
|
||||
const matchSex = sexFilter === 'all' || d.sex === sexFilter;
|
||||
return matchSearch && matchSex;
|
||||
});
|
||||
const filterDogs = () => {
|
||||
let filtered = dogs
|
||||
if (search) {
|
||||
filtered = filtered.filter(dog =>
|
||||
dog.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(dog.breed && dog.breed.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(dog.registration_number && dog.registration_number.toLowerCase().includes(search.toLowerCase()))
|
||||
)
|
||||
}
|
||||
if (sexFilter !== 'all') {
|
||||
filtered = filtered.filter(dog => dog.sex === sexFilter)
|
||||
}
|
||||
setFilteredDogs(filtered)
|
||||
}
|
||||
|
||||
const sires = filtered.filter(d => d.sex === 'male');
|
||||
const dams = filtered.filter(d => d.sex === 'female');
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return
|
||||
setDeleting(true)
|
||||
try {
|
||||
await axios.delete(`/api/dogs/${deleteTarget.id}`)
|
||||
setDogs(prev => prev.filter(d => d.id !== deleteTarget.id))
|
||||
setDeleteTarget(null)
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err)
|
||||
alert('Failed to delete dog. Please try again.')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">Loading external dogs...</div>;
|
||||
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`
|
||||
}
|
||||
|
||||
const hasChampionBlood = (dog) =>
|
||||
(dog.sire && dog.sire.is_champion) || (dog.dam && dog.dam.is_champion)
|
||||
|
||||
if (loading) {
|
||||
return <div className="container loading">Loading external dogs...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
{/* Header */}
|
||||
<div className="page-header">
|
||||
<div className="page-header-left">
|
||||
<ExternalLink size={28} className="page-icon" />
|
||||
<div>
|
||||
<h1 className="page-title">External Dogs</h1>
|
||||
<p className="page-subtitle">External sires, dams, and ancestors used in your breeding program</p>
|
||||
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
|
||||
<ExternalLink size={28} style={{ color: 'var(--primary)' }} />
|
||||
<h1 style={{ margin: 0 }}>External Dogs</h1>
|
||||
</div>
|
||||
<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)}
|
||||
>
|
||||
<Plus size={16} /> Add External Dog
|
||||
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||
<Plus size={18} />
|
||||
Add External Dog
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="filter-bar">
|
||||
<div className="search-wrapper">
|
||||
<Search size={16} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or breed..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<Filter size={14} />
|
||||
<select value={sexFilter} onChange={e => setSexFilter(e.target.value)} className="filter-select">
|
||||
<option value="all">All</option>
|
||||
<option value="male">Sires (Male)</option>
|
||||
<option value="female">Dams (Female)</option>
|
||||
{/* Search and Filter Bar */}
|
||||
<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' }}>
|
||||
<Search size={18} style={{ position: 'absolute', left: '0.875rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="Search by name or breed..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ paddingLeft: '2.75rem' }}
|
||||
/>
|
||||
</div>
|
||||
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '160px' }}>
|
||||
<option value="all">All Genders</option>
|
||||
<option value="male">Sires (Male) ♂</option>
|
||||
<option value="female">Dams (Female) ♀</option>
|
||||
</select>
|
||||
{(search || sexFilter !== 'all') && (
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={() => { setSearch(''); setSexFilter('all') }}
|
||||
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="result-count">{filtered.length} dog{filtered.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<ExternalLink size={48} className="empty-icon" />
|
||||
<h3>No external dogs yet</h3>
|
||||
<p>Add sires, dams, or ancestors that aren't part of your kennel roster.</p>
|
||||
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||
<Plus size={16} /> Add First External Dog
|
||||
</button>
|
||||
{/* Dogs List */}
|
||||
{filteredDogs.length === 0 ? (
|
||||
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||
<ExternalLink size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
||||
<h3 style={{ marginBottom: '0.5rem' }}>
|
||||
{search || sexFilter !== 'all' ? 'No dogs found' : 'No external dogs yet'}
|
||||
</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>
|
||||
{search || sexFilter !== 'all'
|
||||
? 'Try adjusting your search or filters'
|
||||
: 'Add sires, dams, or ancestors that aren\'t part of your kennel roster.'}
|
||||
</p>
|
||||
{!search && sexFilter === 'all' && (
|
||||
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||
<Plus size={18} />
|
||||
Add Your First External Dog
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="external-sections">
|
||||
{(sexFilter === 'all' || sexFilter === 'male') && sires.length > 0 && (
|
||||
<section className="external-section">
|
||||
<h2 className="section-heading">♂ Sires ({sires.length})</h2>
|
||||
<div className="dog-grid">
|
||||
{sires.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||
{filteredDogs.map(dog => (
|
||||
<div
|
||||
key={dog.id}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1rem',
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
transition: 'var(--transition)',
|
||||
}}
|
||||
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 */}
|
||||
<Link
|
||||
to={`/dogs/${dog.id}`}
|
||||
style={{ flexShrink: 0, textDecoration: 'none' }}
|
||||
>
|
||||
<div style={{
|
||||
width: '80px', height: '80px',
|
||||
borderRadius: 'var(--radius)',
|
||||
background: 'var(--bg-primary)',
|
||||
border: dog.is_champion
|
||||
? '2px solid var(--champion-gold)'
|
||||
: hasChampionBlood(dog)
|
||||
? '2px solid var(--bloodline-amber)'
|
||||
: '2px solid var(--border)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
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 ? (
|
||||
<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 style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
background: 'var(--bg-secondary)',
|
||||
borderBottomLeftRadius: 'var(--radius-sm)',
|
||||
padding: '2px 4px',
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--text-muted)',
|
||||
borderLeft: '1px solid var(--border)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
<ExternalLink size={8} /> EXT
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Info — clicking navigates to detail */}
|
||||
<Link
|
||||
to={`/dogs/${dog.id}`}
|
||||
style={{ flex: 1, minWidth: 0, textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<h3 style={{
|
||||
fontSize: '1.125rem',
|
||||
marginBottom: '0.25rem',
|
||||
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{dog.name}
|
||||
</span>
|
||||
<span style={{ color: dog.sex === 'male' ? 'var(--primary)' : '#ec4899', fontSize: '1rem' }}>
|
||||
{dog.sex === 'male' ? '♂' : '♀'}
|
||||
</span>
|
||||
{dog.is_champion ? <ChampionBadge /> : hasChampionBlood(dog) ? <ChampionBloodlineBadge /> : null}
|
||||
</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>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0, alignItems: 'center' }}>
|
||||
<Link
|
||||
to={`/dogs/${dog.id}`}
|
||||
style={{ opacity: 0.5, transition: 'var(--transition)', color: 'inherit' }}
|
||||
>
|
||||
<ArrowRight size={20} color="var(--text-muted)" />
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
title={`Delete ${dog.name}`}
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setDeleteTarget({ id: dog.id, name: dog.name }) }}
|
||||
style={{
|
||||
padding: '0.4rem',
|
||||
color: 'var(--text-muted)',
|
||||
border: '1px solid transparent',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
display: 'flex', alignItems: 'center',
|
||||
transition: 'var(--transition)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#ef4444'
|
||||
e.currentTarget.style.borderColor = '#ef4444'
|
||||
e.currentTarget.style.background = 'rgba(239,68,68,0.08)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = 'var(--text-muted)'
|
||||
e.currentTarget.style.borderColor = 'transparent'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{(sexFilter === 'all' || sexFilter === 'female') && dams.length > 0 && (
|
||||
<section className="external-section">
|
||||
<h2 className="section-heading">♀ Dams ({dams.length})</h2>
|
||||
<div className="dog-grid">
|
||||
{dams.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add External Dog Modal */}
|
||||
{/* Add Dog Modal */}
|
||||
{showAddModal && (
|
||||
<DogForm
|
||||
isExternal={true}
|
||||
@@ -112,45 +329,61 @@ export default function ExternalDogs() {
|
||||
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{deleteTarget && (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0,
|
||||
background: 'rgba(0,0,0,0.65)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
backdropFilter: 'blur(4px)'
|
||||
}}>
|
||||
<div className="card" style={{ maxWidth: 420, width: '90%', padding: '2rem', textAlign: 'center' }}>
|
||||
<div style={{
|
||||
width: 56, height: 56,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(239,68,68,0.12)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
margin: '0 auto 1rem'
|
||||
}}>
|
||||
<Trash2 size={26} style={{ color: '#ef4444' }} />
|
||||
</div>
|
||||
<h3 style={{ margin: '0 0 0.5rem', fontSize: '1.25rem' }}>Delete External Dog?</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', marginBottom: '1.75rem', lineHeight: 1.6 }}>
|
||||
<strong style={{ color: 'var(--text-primary)' }}>{deleteTarget.name}</strong> will be
|
||||
permanently removed. This cannot be undone.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={deleting}
|
||||
style={{ minWidth: 100 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
style={{
|
||||
minWidth: 140,
|
||||
background: '#ef4444',
|
||||
color: '#fff',
|
||||
border: '1px solid #ef4444',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
{deleting ? 'Deleting…' : 'Yes, Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DogCard({ dog }) {
|
||||
const photo = dog.photo_urls?.[0];
|
||||
return (
|
||||
<div
|
||||
className="dog-card dog-card--external"
|
||||
onClick={() => window.location.href = `/dogs/${dog.id}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && (window.location.href = `/dogs/${dog.id}`)}
|
||||
>
|
||||
<div className="dog-card-photo">
|
||||
{photo
|
||||
? <img src={photo} alt={dog.name} />
|
||||
: <div className="dog-card-photo-placeholder"><Users size={32} /></div>
|
||||
}
|
||||
{dog.is_champion === 1 && <span className="champion-badge" title="Champion">🏆</span>}
|
||||
<span className="external-badge"><ExternalLink size={11} /> Ext</span>
|
||||
</div>
|
||||
<div className="dog-card-body">
|
||||
<div className="dog-card-name">
|
||||
{dog.is_champion === 1 && <Award size={13} className="champion-icon" />}
|
||||
{dog.name}
|
||||
</div>
|
||||
<div className="dog-card-meta">{dog.breed}</div>
|
||||
<div className="dog-card-meta dog-card-meta--muted">
|
||||
{dog.sex === 'male' ? '\u2642 Sire' : '\u2640 Dam'}
|
||||
{dog.birth_date && <>· {dog.birth_date}</>}
|
||||
</div>
|
||||
{(dog.sire || dog.dam) && (
|
||||
<div className="dog-card-parents">
|
||||
{dog.sire && <span>S: {dog.sire.name}</span>}
|
||||
{dog.dam && <span>D: {dog.dam.name}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ExternalDogs
|
||||
|
||||
Reference in New Issue
Block a user