Implementation
Task complete.
This commit is contained in:
@@ -13,12 +13,17 @@ The "External Dogs" interface does not match the layout and style of the main "D
|
|||||||
## Affected Components
|
## Affected Components
|
||||||
- `client/src/pages/ExternalDogs.jsx`
|
- `client/src/pages/ExternalDogs.jsx`
|
||||||
|
|
||||||
## Proposed Solution
|
## Implementation Notes
|
||||||
Refactor `ExternalDogs.jsx` to mirror the structure and style of `DogList.jsx`:
|
Refactored `ExternalDogs.jsx` to match `DogList.jsx` in layout, style, and functionality. Key changes:
|
||||||
1. **Standardize Imports**: Use `axios` instead of `fetch`. Import `ChampionBadge`, `ChampionBloodlineBadge`, and necessary `lucide-react` icons.
|
- Switched to `axios` for API calls.
|
||||||
2. **Match Layout**: Update the main container and header to match `DogList.jsx` using the `container` class and consistent inline styles.
|
- Adopted the vertical list layout instead of the grid.
|
||||||
3. **Sync Filter Bar**: Use the same search and filter bar implementation as `DogList.jsx`.
|
- Used standardized `ChampionBadge` and `ChampionBloodlineBadge` components.
|
||||||
4. **Implement List View**: Replace the `dog-grid` with a vertical stack of cards matching the Dogs page style.
|
- Added a search/filter bar consistent with the main Dogs page.
|
||||||
5. **Add Delete Functionality**: Implement the `handleDelete` logic and add the `Delete Confirmation Modal`.
|
- Implemented delete functionality with a confirmation modal.
|
||||||
6. **Use Badges & Helpers**: Replace emoji badges with the standardized badge components and use `calculateAge` for birth dates.
|
- Standardized age calculation using the `calculateAge` helper logic.
|
||||||
7. **Consistent Navigation**: Use `react-router-dom`'s `Link` for navigation to dog details.
|
- 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`).
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ Save findings to `{@artifacts_path}/investigation.md` with:
|
|||||||
- Affected components
|
- Affected components
|
||||||
- Proposed solution
|
- Proposed solution
|
||||||
|
|
||||||
### [ ] Step: Implementation
|
### [x] Step: Implementation
|
||||||
|
<!-- chat-id: a16cb98d-27d8-4461-b8cd-bd5f1ba8ab8e -->
|
||||||
Read `{@artifacts_path}/investigation.md`
|
Read `{@artifacts_path}/investigation.md`
|
||||||
Implement the bug fix.
|
Implement the bug fix.
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +1,327 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react'
|
||||||
import { Users, Plus, Search, ExternalLink, Award, Filter } from 'lucide-react';
|
import { Link } from 'react-router-dom'
|
||||||
import DogForm from '../components/DogForm';
|
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() {
|
function ExternalDogs() {
|
||||||
const [dogs, setDogs] = useState([]);
|
const [dogs, setDogs] = useState([])
|
||||||
const [loading, setLoading] = useState(true);
|
const [filteredDogs, setFilteredDogs] = useState([])
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('')
|
||||||
const [sexFilter, setSexFilter] = useState('all');
|
const [sexFilter, setSexFilter] = useState('all')
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { fetchDogs() }, [])
|
||||||
fetchDogs();
|
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchDogs = () => {
|
const fetchDogs = async () => {
|
||||||
fetch('/api/dogs/external')
|
try {
|
||||||
.then(r => r.json())
|
const res = await axios.get('/api/dogs/external')
|
||||||
.then(data => { setDogs(data); setLoading(false); })
|
setDogs(res.data)
|
||||||
.catch(() => setLoading(false));
|
setLoading(false)
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('Error fetching external dogs:', error)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = dogs.filter(d => {
|
const filterDogs = () => {
|
||||||
const matchSearch = d.name.toLowerCase().includes(search.toLowerCase()) ||
|
let filtered = dogs
|
||||||
(d.breed || '').toLowerCase().includes(search.toLowerCase());
|
if (search) {
|
||||||
const matchSex = sexFilter === 'all' || d.sex === sexFilter;
|
filtered = filtered.filter(dog =>
|
||||||
return matchSearch && matchSex;
|
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 handleDelete = async () => {
|
||||||
const dams = filtered.filter(d => d.sex === 'female');
|
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 (
|
return (
|
||||||
<div className="page-container">
|
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||||
{/* Header */}
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||||
<div className="page-header">
|
|
||||||
<div className="page-header-left">
|
|
||||||
<ExternalLink size={28} className="page-icon" />
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">External Dogs</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
|
||||||
<p className="page-subtitle">External sires, dams, and ancestors used in your breeding program</p>
|
<ExternalLink size={28} style={{ color: 'var(--primary)' }} />
|
||||||
|
<h1 style={{ margin: 0 }}>External Dogs</h1>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<button
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
className="btn btn-primary"
|
<Plus size={18} />
|
||||||
onClick={() => setShowAddModal(true)}
|
Add External Dog
|
||||||
>
|
|
||||||
<Plus size={16} /> Add External Dog
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Search and Filter Bar */}
|
||||||
<div className="filter-bar">
|
<div className="card" style={{ marginBottom: '1.5rem', padding: '1rem' }}>
|
||||||
<div className="search-wrapper">
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto auto', gap: '1rem', alignItems: 'center' }}>
|
||||||
<Search size={16} className="search-icon" />
|
<div style={{ position: 'relative' }}>
|
||||||
|
<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"
|
||||||
placeholder="Search by name or breed..."
|
placeholder="Search by name or breed..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="search-input"
|
style={{ paddingLeft: '2.75rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="filter-group">
|
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '160px' }}>
|
||||||
<Filter size={14} />
|
<option value="all">All Genders</option>
|
||||||
<select value={sexFilter} onChange={e => setSexFilter(e.target.value)} className="filter-select">
|
<option value="male">Sires (Male) ♂</option>
|
||||||
<option value="all">All</option>
|
<option value="female">Dams (Female) ♀</option>
|
||||||
<option value="male">Sires (Male)</option>
|
|
||||||
<option value="female">Dams (Female)</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>
|
||||||
<span className="result-count">{filtered.length} dog{filtered.length !== 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{/* Dogs List */}
|
||||||
<div className="empty-state">
|
{filteredDogs.length === 0 ? (
|
||||||
<ExternalLink size={48} className="empty-icon" />
|
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||||
<h3>No external dogs yet</h3>
|
<ExternalLink size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
||||||
<p>Add sires, dams, or ancestors that aren't part of your kennel roster.</p>
|
<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)}>
|
<button className="btn btn-primary" onClick={() => setShowAddModal(true)}>
|
||||||
<Plus size={16} /> Add First External Dog
|
<Plus size={18} />
|
||||||
|
Add Your First External Dog
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="external-sections">
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
{(sexFilter === 'all' || sexFilter === 'male') && sires.length > 0 && (
|
{filteredDogs.map(dog => (
|
||||||
<section className="external-section">
|
<div
|
||||||
<h2 className="section-heading">♂ Sires ({sires.length})</h2>
|
key={dog.id}
|
||||||
<div className="dog-grid">
|
className="card"
|
||||||
{sires.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
style={{
|
||||||
</div>
|
padding: '1rem',
|
||||||
</section>
|
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 }} />
|
||||||
)}
|
)}
|
||||||
{(sexFilter === 'all' || sexFilter === 'female') && dams.length > 0 && (
|
<div style={{
|
||||||
<section className="external-section">
|
position: 'absolute',
|
||||||
<h2 className="section-heading">♀ Dams ({dams.length})</h2>
|
top: 0,
|
||||||
<div className="dog-grid">
|
right: 0,
|
||||||
{dams.map(dog => <DogCard key={dog.id} dog={dog} />)}
|
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>
|
||||||
</section>
|
</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>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add External Dog Modal */}
|
{/* Add Dog Modal */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<DogForm
|
<DogForm
|
||||||
isExternal={true}
|
isExternal={true}
|
||||||
@@ -112,45 +329,61 @@ export default function ExternalDogs() {
|
|||||||
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
onSave={() => { fetchDogs(); setShowAddModal(false); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DogCard({ dog }) {
|
{/* Delete Confirmation Modal */}
|
||||||
const photo = dog.photo_urls?.[0];
|
{deleteTarget && (
|
||||||
return (
|
<div style={{
|
||||||
<div
|
position: 'fixed', inset: 0,
|
||||||
className="dog-card dog-card--external"
|
background: 'rgba(0,0,0,0.65)',
|
||||||
onClick={() => window.location.href = `/dogs/${dog.id}`}
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
role="button"
|
zIndex: 1000,
|
||||||
tabIndex={0}
|
backdropFilter: 'blur(4px)'
|
||||||
onKeyDown={e => e.key === 'Enter' && (window.location.href = `/dogs/${dog.id}`)}
|
}}>
|
||||||
|
<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 }}
|
||||||
>
|
>
|
||||||
<div className="dog-card-photo">
|
Cancel
|
||||||
{photo
|
</button>
|
||||||
? <img src={photo} alt={dog.name} />
|
<button
|
||||||
: <div className="dog-card-photo-placeholder"><Users size={32} /></div>
|
className="btn"
|
||||||
}
|
onClick={handleDelete}
|
||||||
{dog.is_champion === 1 && <span className="champion-badge" title="Champion">🏆</span>}
|
disabled={deleting}
|
||||||
<span className="external-badge"><ExternalLink size={11} /> Ext</span>
|
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 className="dog-card-body">
|
|
||||||
<div className="dog-card-name">
|
|
||||||
{dog.is_champion === 1 && <Award size={13} className="champion-icon" />}
|
|
||||||
{dog.name}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ExternalDogs
|
||||||
|
|||||||
Reference in New Issue
Block a user