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 { 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