Client UX: toast system, confirm dialogs, load-error banners, nav fix, 404 page

- New ToastProvider/useToast (self-built, no dep): success/error toasts replace
  all five alert() calls, with server error messages surfaced
- New ConfirmProvider/useConfirm promise-based dialog replaces all six
  window.confirm sites, showing what is being deleted; Escape/overlay cancels
- Dashboard + BreedingCalendar loaders use Promise.allSettled with a
  danger banner + Retry instead of silently blanking on fetch failure
- Settings load failure now surfaces a toast instead of vanishing
- Fix BreedingCalendar females list: read paginated {data} shape (was .dogs,
  so 'Start Heat Cycle' never listed any females)
- Nav links highlight on detail routes (/dogs/123, /pedigree/5)
- Catch-all 404 route with link back to Dashboard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jason Stedwell
2026-07-06 14:55:32 -05:00
parent 31b1d903f4
commit 55d636b512
12 changed files with 338 additions and 56 deletions
+24 -5
View File
@@ -3,6 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Plus, X, ExternalLink, Dog, Weight, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
import axios from 'axios'
import LitterForm from '../components/LitterForm'
import { useToast } from '../components/Toast'
import { useConfirm } from '../components/ConfirmDialog'
// ─── Puppy Log Panel ────────────────────────────────────────────────────────────
function PuppyLogPanel({ litterId, puppy, whelpingDate }) {
@@ -18,6 +20,8 @@ function PuppyLogPanel({ litterId, puppy, whelpingDate }) {
record_type: 'weight_log'
})
const [saving, setSaving] = useState(false)
const toast = useToast()
const confirm = useConfirm()
useEffect(() => { if (open) fetchLogs() }, [open])
@@ -41,16 +45,22 @@ function PuppyLogPanel({ litterId, puppy, whelpingDate }) {
setShowAdd(false)
setForm(f => ({ ...f, weight_oz: '', weight_lbs: '', notes: '' }))
fetchLogs()
} catch (e) { console.error(e) }
} catch (e) {
console.error(e)
toast.error(e.response?.data?.error || 'Failed to save log entry')
}
finally { setSaving(false) }
}
const handleDelete = async (logId) => {
if (!window.confirm('Delete this log entry?')) return
if (!(await confirm({ title: 'Delete log entry?', message: `This log entry for ${puppy.name} will be permanently removed.` }))) return
try {
await axios.delete(`/api/litters/${litterId}/puppies/${puppy.id}/logs/${logId}`)
fetchLogs()
} catch (e) { console.error(e) }
} catch (e) {
console.error(e)
toast.error('Failed to delete log entry')
}
}
const TYPES = [
@@ -262,6 +272,8 @@ function LitterDetail() {
const [addMode, setAddMode] = useState('existing')
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
const toast = useToast()
const confirm = useConfirm()
useEffect(() => { fetchLitter(); fetchAllDogs() }, [id])
@@ -320,11 +332,18 @@ function LitterDetail() {
}
const handleUnlinkPuppy = async (puppyId) => {
if (!window.confirm('Remove this puppy from the litter? The dog record will not be deleted.')) return
if (!(await confirm({
title: 'Remove puppy from litter?',
message: 'The dog record itself will not be deleted.',
confirmLabel: 'Remove',
}))) return
try {
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
fetchLitter()
} catch (err) { console.error('Error unlinking puppy:', err) }
} catch (err) {
console.error('Error unlinking puppy:', err)
toast.error(err.response?.data?.error || 'Failed to remove puppy from litter')
}
}
if (loading) return <div className="container loading">Loading litter...</div>