feature/dog-forms #1
234
ROADMAP.md
Normal file
234
ROADMAP.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# BREEDR Development Roadmap
|
||||
|
||||
## ✅ Phase 1: Foundation (COMPLETE)
|
||||
|
||||
### Infrastructure
|
||||
- [x] Docker multi-stage build configuration
|
||||
- [x] SQLite database with automatic initialization
|
||||
- [x] Express.js API server
|
||||
- [x] React 18 frontend with Vite
|
||||
- [x] Git repository structure
|
||||
|
||||
### Database Schema
|
||||
- [x] Dogs table with core fields
|
||||
- [x] Parents relationship table
|
||||
- [x] Litters breeding records
|
||||
- [x] Health records tracking
|
||||
- [x] Heat cycles management
|
||||
- [x] Traits genetic mapping
|
||||
- [x] Indexes and triggers
|
||||
|
||||
### API Endpoints
|
||||
- [x] `/api/dogs` - Full CRUD operations
|
||||
- [x] `/api/pedigree` - Tree generation and COI calculator
|
||||
- [x] `/api/litters` - Breeding records
|
||||
- [x] `/api/health` - Health tracking
|
||||
- [x] `/api/breeding` - Heat cycles and whelping calculator
|
||||
- [x] Photo upload with Multer
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 2: Core Functionality (COMPLETE)
|
||||
|
||||
### Dog Management
|
||||
- [x] Add new dogs with full form
|
||||
- [x] Edit existing dogs
|
||||
- [x] View dog details
|
||||
- [x] List all dogs with search/filter
|
||||
- [x] Upload multiple photos per dog
|
||||
- [x] Delete photos
|
||||
- [x] Parent selection (sire/dam)
|
||||
|
||||
### User Interface
|
||||
- [x] Dashboard with statistics
|
||||
- [x] Dog list with grid view
|
||||
- [x] Dog detail pages
|
||||
- [x] Modal forms for add/edit
|
||||
- [x] Photo management UI
|
||||
- [x] Search and sex filtering
|
||||
- [x] Responsive navigation
|
||||
|
||||
### Features Implemented
|
||||
- [x] Photo upload and storage
|
||||
- [x] Parent-child relationships
|
||||
- [x] Basic information tracking
|
||||
- [x] Registration numbers
|
||||
- [x] Microchip tracking
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Phase 3: Breeding Tools (IN PROGRESS)
|
||||
|
||||
### Priority Features
|
||||
- [ ] Interactive pedigree tree visualization
|
||||
- [ ] Integrate React-D3-Tree
|
||||
- [ ] Show 3-5 generations
|
||||
- [ ] Click to navigate
|
||||
- [ ] Zoom and pan controls
|
||||
|
||||
- [ ] Trial Pairing Simulator
|
||||
- [ ] Select sire and dam
|
||||
- [ ] Display COI calculation
|
||||
- [ ] Show common ancestors
|
||||
- [ ] Risk assessment display
|
||||
|
||||
- [ ] Heat Cycle Management
|
||||
- [ ] Add/edit heat cycles
|
||||
- [ ] Track progesterone levels
|
||||
- [ ] Calendar view
|
||||
- [ ] Breeding date suggestions
|
||||
|
||||
- [ ] Litter Management
|
||||
- [ ] Create litter records
|
||||
- [ ] Link puppies to litter
|
||||
- [ ] Track whelping details
|
||||
- [ ] Auto-link parent relationships
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 4: Health & Genetics (PLANNED)
|
||||
|
||||
### Health Records
|
||||
- [ ] Add health test results
|
||||
- [ ] Vaccination tracking
|
||||
- [ ] Medical history timeline
|
||||
- [ ] Document uploads (PDFs, images)
|
||||
- [ ] Alert for expiring vaccinations
|
||||
|
||||
### Genetic Tracking
|
||||
- [ ] Track inherited traits
|
||||
- [ ] Color genetics calculator
|
||||
- [ ] Health clearance status
|
||||
- [ ] Link traits to ancestors
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 5: Advanced Features (PLANNED)
|
||||
|
||||
### Pedigree Tools
|
||||
- [ ] Reverse pedigree (descendants view)
|
||||
- [ ] PDF pedigree generation
|
||||
- [ ] Export to standard formats
|
||||
- [ ] Print-friendly layouts
|
||||
- [ ] Multi-generation COI analysis
|
||||
|
||||
### Breeding Planning
|
||||
- [ ] Breeding calendar
|
||||
- [ ] Heat cycle predictions
|
||||
- [ ] Expected whelping alerts
|
||||
- [ ] Breeding history reports
|
||||
|
||||
### Search & Analytics
|
||||
- [ ] Advanced search filters
|
||||
- [ ] By breed, color, age
|
||||
- [ ] By health clearances
|
||||
- [ ] By registration status
|
||||
- [ ] Statistics dashboard
|
||||
- [ ] Breeding success rates
|
||||
- [ ] Average litter sizes
|
||||
- [ ] Popular pairings
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 6: Polish & Optimization (PLANNED)
|
||||
|
||||
### User Experience
|
||||
- [ ] Loading states for all operations
|
||||
- [ ] Better error messages
|
||||
- [ ] Confirmation dialogs
|
||||
- [ ] Undo functionality
|
||||
- [ ] Keyboard shortcuts
|
||||
|
||||
### Performance
|
||||
- [ ] Image optimization
|
||||
- [ ] Lazy loading
|
||||
- [ ] API caching
|
||||
- [ ] Database query optimization
|
||||
|
||||
### Mobile
|
||||
- [ ] Touch-friendly interface
|
||||
- [ ] Mobile photo capture
|
||||
- [ ] Responsive tables
|
||||
- [ ] Offline mode
|
||||
|
||||
### Documentation
|
||||
- [ ] User manual
|
||||
- [ ] API documentation
|
||||
- [ ] Video tutorials
|
||||
- [ ] FAQ section
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements (BACKLOG)
|
||||
|
||||
### Multi-User Support
|
||||
- [ ] User authentication
|
||||
- [ ] Role-based permissions
|
||||
- [ ] Activity logs
|
||||
- [ ] Shared access
|
||||
|
||||
### Integration
|
||||
- [ ] Import from other systems
|
||||
- [ ] Export to Excel/CSV
|
||||
- [ ] Integration with kennel clubs
|
||||
- [ ] Backup to cloud storage
|
||||
|
||||
### Advanced Genetics
|
||||
- [ ] DNA test result tracking
|
||||
- [ ] Genetic diversity analysis
|
||||
- [ ] Breed-specific calculators
|
||||
- [ ] Health risk predictions
|
||||
|
||||
### Kennel Management
|
||||
- [ ] Breeding contracts
|
||||
- [ ] Buyer tracking
|
||||
- [ ] Financial records
|
||||
- [ ] Stud service management
|
||||
|
||||
---
|
||||
|
||||
## Current Sprint Focus
|
||||
|
||||
### Next Up (Priority)
|
||||
1. **Interactive Pedigree Visualization**
|
||||
- Implement React-D3-Tree integration
|
||||
- Connect to `/api/pedigree/:id` endpoint
|
||||
- Add zoom/pan controls
|
||||
- Enable click navigation
|
||||
|
||||
2. **Trial Pairing Tool**
|
||||
- Create pairing form
|
||||
- Display COI calculation
|
||||
- Show common ancestors
|
||||
- Add recommendation system
|
||||
|
||||
3. **Litter Management**
|
||||
- Add litter creation form
|
||||
- Link puppies to litters
|
||||
- Display breeding history
|
||||
- Track whelping outcomes
|
||||
|
||||
### Testing Needed
|
||||
- [ ] Add/edit dog forms
|
||||
- [ ] Photo upload functionality
|
||||
- [ ] Search and filtering
|
||||
- [ ] Parent relationship linking
|
||||
- [ ] API error handling
|
||||
|
||||
### Known Issues
|
||||
- None currently
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Pick a feature from "Priority Features"
|
||||
2. Create a feature branch: `feature/feature-name`
|
||||
3. Implement with tests
|
||||
4. Update this roadmap
|
||||
5. Submit for review
|
||||
|
||||
## Version History
|
||||
|
||||
- **v0.2.0** (Current) - Dog CRUD operations complete
|
||||
- **v0.1.0** - Initial foundation with API and database
|
||||
230
client/src/components/DogForm.jsx
Normal file
230
client/src/components/DogForm.jsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
|
||||
function DogForm({ dog, onClose, onSave }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
registration_number: '',
|
||||
breed: '',
|
||||
sex: 'male',
|
||||
birth_date: '',
|
||||
color: '',
|
||||
microchip: '',
|
||||
notes: '',
|
||||
sire_id: '',
|
||||
dam_id: ''
|
||||
})
|
||||
const [dogs, setDogs] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchDogs()
|
||||
if (dog) {
|
||||
setFormData({
|
||||
name: dog.name || '',
|
||||
registration_number: dog.registration_number || '',
|
||||
breed: dog.breed || '',
|
||||
sex: dog.sex || 'male',
|
||||
birth_date: dog.birth_date || '',
|
||||
color: dog.color || '',
|
||||
microchip: dog.microchip || '',
|
||||
notes: dog.notes || '',
|
||||
sire_id: dog.sire?.id || '',
|
||||
dam_id: dog.dam?.id || ''
|
||||
})
|
||||
}
|
||||
}, [dog])
|
||||
|
||||
const fetchDogs = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/dogs')
|
||||
setDogs(res.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching dogs:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
if (dog) {
|
||||
// Update existing dog
|
||||
await axios.put(`/api/dogs/${dog.id}`, formData)
|
||||
} else {
|
||||
// Create new dog
|
||||
await axios.post('/api/dogs', formData)
|
||||
}
|
||||
onSave()
|
||||
onClose()
|
||||
} catch (error) {
|
||||
setError(error.response?.data?.error || 'Failed to save dog')
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const males = dogs.filter(d => d.sex === 'male' && d.id !== dog?.id)
|
||||
const females = dogs.filter(d => d.sex === 'female' && d.id !== dog?.id)
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>{dog ? 'Edit Dog' : 'Add New Dog'}</h2>
|
||||
<button className="btn-icon" onClick={onClose}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="modal-body">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label className="label">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className="input"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Registration Number</label>
|
||||
<input
|
||||
type="text"
|
||||
name="registration_number"
|
||||
className="input"
|
||||
value={formData.registration_number}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Breed *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="breed"
|
||||
className="input"
|
||||
value={formData.breed}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Sex *</label>
|
||||
<select
|
||||
name="sex"
|
||||
className="input"
|
||||
value={formData.sex}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Birth Date</label>
|
||||
<input
|
||||
type="date"
|
||||
name="birth_date"
|
||||
className="input"
|
||||
value={formData.birth_date}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Color</label>
|
||||
<input
|
||||
type="text"
|
||||
name="color"
|
||||
className="input"
|
||||
value={formData.color}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Microchip Number</label>
|
||||
<input
|
||||
type="text"
|
||||
name="microchip"
|
||||
className="input"
|
||||
value={formData.microchip}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Sire (Father)</label>
|
||||
<select
|
||||
name="sire_id"
|
||||
className="input"
|
||||
value={formData.sire_id}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Unknown</option>
|
||||
{males.map(d => (
|
||||
<option key={d.id} value={d.id}>{d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">Dam (Mother)</label>
|
||||
<select
|
||||
name="dam_id"
|
||||
className="input"
|
||||
value={formData.dam_id}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Unknown</option>
|
||||
{females.map(d => (
|
||||
<option key={d.id} value={d.id}>{d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginTop: '1rem' }}>
|
||||
<label className="label">Notes</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
className="input"
|
||||
rows="4"
|
||||
value={formData.notes}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? 'Saving...' : dog ? 'Update Dog' : 'Add Dog'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DogForm
|
||||
@@ -78,17 +78,22 @@ code {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
@@ -107,6 +112,24 @@ code {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg);
|
||||
border-radius: 0.5rem;
|
||||
@@ -114,20 +137,27 @@ code {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.input {
|
||||
.input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
.input:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -163,3 +193,70 @@ code {
|
||||
border-radius: 0.375rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
select.input {
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { Dog, GitBranch, Edit, Trash2 } from 'lucide-react'
|
||||
import { Dog, GitBranch, Edit, Upload, Trash2 } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import DogForm from '../components/DogForm'
|
||||
|
||||
function DogDetail() {
|
||||
const { id } = useParams()
|
||||
const [dog, setDog] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDog()
|
||||
@@ -23,6 +27,40 @@ function DogDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePhotoUpload = async (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
setUploading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('photo', file)
|
||||
|
||||
try {
|
||||
await axios.post(`/api/dogs/${id}/photos`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
fetchDog()
|
||||
} catch (error) {
|
||||
console.error('Error uploading photo:', error)
|
||||
alert('Failed to upload photo')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePhoto = async (photoIndex) => {
|
||||
if (!confirm('Delete this photo?')) return
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/dogs/${id}/photos/${photoIndex}`)
|
||||
fetchDog()
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo:', error)
|
||||
alert('Failed to delete photo')
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="container loading">Loading...</div>
|
||||
}
|
||||
@@ -40,7 +78,7 @@ function DogDetail() {
|
||||
<GitBranch size={20} />
|
||||
View Pedigree
|
||||
</Link>
|
||||
<button className="btn btn-secondary">
|
||||
<button className="btn btn-secondary" onClick={() => setShowEditModal(true)}>
|
||||
<Edit size={20} />
|
||||
Edit
|
||||
</button>
|
||||
@@ -81,11 +119,46 @@ function DogDetail() {
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2 style={{ marginBottom: '1rem' }}>Photos</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2>Photos</h2>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<Upload size={18} />
|
||||
{uploading ? 'Uploading...' : 'Upload'}
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoUpload}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
{dog.photo_urls && dog.photo_urls.length > 0 ? (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '0.5rem' }}>
|
||||
{dog.photo_urls.map((url, index) => (
|
||||
<img key={index} src={url} alt={`${dog.name} ${index + 1}`} style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: '0.375rem' }} />
|
||||
<div key={index} style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={url}
|
||||
alt={`${dog.name} ${index + 1}`}
|
||||
style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: '0.375rem' }}
|
||||
/>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => handleDeletePhoto(index)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0.25rem',
|
||||
right: '0.25rem',
|
||||
background: 'rgba(255,255,255,0.9)'
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} color="var(--danger)" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -138,6 +211,17 @@ function DogDetail() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditModal && (
|
||||
<DogForm
|
||||
dog={dog}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
onSave={() => {
|
||||
fetchDog()
|
||||
setShowEditModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Dog, Plus, Search } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import DogForm from '../components/DogForm'
|
||||
|
||||
function DogList() {
|
||||
const [dogs, setDogs] = useState([])
|
||||
@@ -47,6 +48,10 @@ function DogList() {
|
||||
setFilteredDogs(filtered)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
fetchDogs()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="container loading">Loading dogs...</div>
|
||||
}
|
||||
@@ -111,6 +116,13 @@ function DogList() {
|
||||
<p style={{ color: 'var(--text-secondary)' }}>No dogs found matching your search criteria.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddModal && (
|
||||
<DogForm
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user