diff --git a/.zenflow/tasks/6e6e64eb-cb72-459e-b943-27554a749459/investigation.md b/.zenflow/tasks/6e6e64eb-cb72-459e-b943-27554a749459/investigation.md index 66d6a8e..4c8ab6d 100644 --- a/.zenflow/tasks/6e6e64eb-cb72-459e-b943-27554a749459/investigation.md +++ b/.zenflow/tasks/6e6e64eb-cb72-459e-b943-27554a749459/investigation.md @@ -13,12 +13,17 @@ The "External Dogs" interface does not match the layout and style of the main "D ## Affected Components - `client/src/pages/ExternalDogs.jsx` -## Proposed Solution -Refactor `ExternalDogs.jsx` to mirror the structure and style of `DogList.jsx`: -1. **Standardize Imports**: Use `axios` instead of `fetch`. Import `ChampionBadge`, `ChampionBloodlineBadge`, and necessary `lucide-react` icons. -2. **Match Layout**: Update the main container and header to match `DogList.jsx` using the `container` class and consistent inline styles. -3. **Sync Filter Bar**: Use the same search and filter bar implementation as `DogList.jsx`. -4. **Implement List View**: Replace the `dog-grid` with a vertical stack of cards matching the Dogs page style. -5. **Add Delete Functionality**: Implement the `handleDelete` logic and add the `Delete Confirmation Modal`. -6. **Use Badges & Helpers**: Replace emoji badges with the standardized badge components and use `calculateAge` for birth dates. -7. **Consistent Navigation**: Use `react-router-dom`'s `Link` for navigation to dog details. +## 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`). diff --git a/.zenflow/tasks/new-task-6e6e/plan.md b/.zenflow/tasks/new-task-6e6e/plan.md index 7d9ae26..66fec80 100644 --- a/.zenflow/tasks/new-task-6e6e/plan.md +++ b/.zenflow/tasks/new-task-6e6e/plan.md @@ -31,7 +31,8 @@ Save findings to `{@artifacts_path}/investigation.md` with: - Affected components - Proposed solution -### [ ] Step: Implementation +### [x] Step: Implementation + Read `{@artifacts_path}/investigation.md` Implement the bug fix. diff --git a/client/src/pages/ExternalDogs.jsx b/client/src/pages/ExternalDogs.jsx index 9baa308..df9b8c8 100644 --- a/client/src/pages/ExternalDogs.jsx +++ b/client/src/pages/ExternalDogs.jsx @@ -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
Loading external dogs...
; + 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
Loading external dogs...
+ } return ( -
- {/* Header */} -
-
- -
-

External Dogs

-

External sires, dams, and ancestors used in your breeding program

+
+
+
+
+ +

External Dogs

+

+ {filteredDogs.length} {filteredDogs.length === 1 ? 'dog' : 'dogs'} + {search || sexFilter !== 'all' ? ' matching filters' : ' total'} +

-
- {/* Filters */} -
-
- - setSearch(e.target.value)} - className="search-input" - /> -
-
- - setSearch(e.target.value)} + style={{ paddingLeft: '2.75rem' }} + /> +
+ + {(search || sexFilter !== 'all') && ( + + )}
- {filtered.length} dog{filtered.length !== 1 ? 's' : ''}
- {filtered.length === 0 ? ( -
- -

No external dogs yet

-

Add sires, dams, or ancestors that aren't part of your kennel roster.

- + {/* Dogs List */} + {filteredDogs.length === 0 ? ( +
+ +

+ {search || sexFilter !== 'all' ? 'No dogs found' : 'No external dogs yet'} +

+

+ {search || sexFilter !== 'all' + ? 'Try adjusting your search or filters' + : 'Add sires, dams, or ancestors that aren\'t part of your kennel roster.'} +

+ {!search && sexFilter === 'all' && ( + + )}
) : ( -
- {(sexFilter === 'all' || sexFilter === 'male') && sires.length > 0 && ( -
-

♂ Sires ({sires.length})

-
- {sires.map(dog => )} +
+ {filteredDogs.map(dog => ( +
{ + 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 */} + +
+ {dog.photo_urls && dog.photo_urls.length > 0 ? ( + {dog.name} + ) : ( + + )} +
+ EXT +
+
+ + + {/* Info — clicking navigates to detail */} + +

+ + {dog.name} + + + {dog.sex === 'male' ? '♂' : '♀'} + + {dog.is_champion ? : hasChampionBlood(dog) ? : null} +

+ +
+ {dog.breed} + {dog.birth_date && ( + <> + · + + + {calculateAge(dog.birth_date)} + + + )} + {dog.color && ( + <> + · + {dog.color} + + )} +
+ + {dog.registration_number && ( +
+ + {dog.registration_number} +
+ )} + + + {/* Actions */} +
+ + + +
-
- )} - {(sexFilter === 'all' || sexFilter === 'female') && dams.length > 0 && ( -
-

♀ Dams ({dams.length})

-
- {dams.map(dog => )} -
-
- )} +
+ ))}
)} - {/* Add External Dog Modal */} + {/* Add Dog Modal */} {showAddModal && ( { fetchDogs(); setShowAddModal(false); }} /> )} + + {/* Delete Confirmation Modal */} + {deleteTarget && ( +
+
+
+ +
+

Delete External Dog?

+

+ {deleteTarget.name} will be + permanently removed. This cannot be undone. +

+
+ + +
+
+
+ )}
- ); + ) } -function DogCard({ dog }) { - const photo = dog.photo_urls?.[0]; - return ( -
window.location.href = `/dogs/${dog.id}`} - role="button" - tabIndex={0} - onKeyDown={e => e.key === 'Enter' && (window.location.href = `/dogs/${dog.id}`)} - > -
- {photo - ? {dog.name} - :
- } - {dog.is_champion === 1 && 🏆} - Ext -
-
-
- {dog.is_champion === 1 && } - {dog.name} -
-
{dog.breed}
-
- {dog.sex === 'male' ? '\u2642 Sire' : '\u2640 Dam'} - {dog.birth_date && <>· {dog.birth_date}} -
- {(dog.sire || dog.dam) && ( -
- {dog.sire && S: {dog.sire.name}} - {dog.dam && D: {dog.dam.name}} -
- )} -
-
- ); -} +export default ExternalDogs