Polish: live OFA age warning, puppy sorting, responsive fixes, form hints
- HealthRecordForm shows a live amber warning under Test Date when the dog is under the OFA minimum certification age (submit-time block unchanged) - LitterDetail puppies sortable by name / sex / birth date - Responsive at 375px: DogDetail photos+info grid stacks, header wraps; puppy grid uses min(220px,100%); navbar wraps with scrollable icon row; calendar uses minmax(0,1fr) columns so cells shrink instead of overflowing - LitterForm explains why sire/dam selects are locked when editing - Add .claude/launch.json for the vite dev server Verified in-browser at 375px and desktop: no horizontal overflow on any page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+11
-3
@@ -139,14 +139,22 @@
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
.navbar .container {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.nav-links {
|
||||
gap: 0.25rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.nav-link span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.nav-link {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,14 @@ export default function HealthRecordForm({ dogId, dogBirthDate, record, onClose,
|
||||
test_type: v === 'ofa_clearance' ? (f.test_type || 'hip_ofa') : f.test_type,
|
||||
}))
|
||||
|
||||
// Live warning while typing — validate() still hard-blocks on submit.
|
||||
const ageWarning = (() => {
|
||||
if (!isOFA || !ofaTest?.minAgeMonths || !dogBirthDate || !form.test_date) return null
|
||||
const months = (new Date(form.test_date) - new Date(dogBirthDate)) / (1000 * 60 * 60 * 24 * 30.44)
|
||||
if (months < 0 || months >= ofaTest.minAgeMonths) return null
|
||||
return `Dog was ~${Math.floor(months)} months old on this date — final ${ofaTest.label} certification requires ${ofaTest.minAgeMonths} months.`
|
||||
})()
|
||||
|
||||
const validate = () => {
|
||||
if (!form.test_date) return 'A date is required.'
|
||||
if (form.test_date > today()) return 'Date cannot be in the future.'
|
||||
@@ -250,6 +258,9 @@ export default function HealthRecordForm({ dogId, dogBirthDate, record, onClose,
|
||||
<label style={labelStyle}>Test Date *</label>
|
||||
<input style={inputStyle} type="date" required max={today()} value={form.test_date}
|
||||
onChange={e => set('test_date', e.target.value)} />
|
||||
{ageWarning && (
|
||||
<span style={{ ...hintStyle, color: 'var(--warning)' }}>{ageWarning}</span>
|
||||
)}
|
||||
</div>
|
||||
{ofaTest?.recurs ? (
|
||||
<div style={fw}>
|
||||
|
||||
@@ -116,6 +116,11 @@ function LitterForm({ litter, prefill, onClose, onSave }) {
|
||||
<option key={d.id} value={d.id}>{d.name} {d.registration_number ? `(${d.registration_number})` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
{!!litter && (
|
||||
<p style={{ fontSize: '0.78rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
|
||||
Parents can't be changed after the litter is created.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
|
||||
@@ -534,7 +534,7 @@ export default function BreedingCalendar() {
|
||||
</div>
|
||||
|
||||
{/* Day headers */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', borderBottom: '1px solid var(--border)' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', borderBottom: '1px solid var(--border)' }}>
|
||||
{DAY_NAMES.map(d => (
|
||||
<div key={d} style={{ padding: '0.5rem', textAlign: 'center', fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{d}</div>
|
||||
))}
|
||||
@@ -544,7 +544,7 @@ export default function BreedingCalendar() {
|
||||
{loading ? (
|
||||
<div className="loading" style={{ minHeight: 280 }}>Loading calendar…</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))' }}>
|
||||
{Array.from({ length: rows * 7 }).map((_, idx) => {
|
||||
const dayNum = idx - startPad + 1
|
||||
const isValid = dayNum >= 1 && dayNum <= lastDay.getDate()
|
||||
|
||||
@@ -126,7 +126,7 @@ function DogDetail() {
|
||||
return (
|
||||
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem', flexWrap: 'wrap' }}>
|
||||
<button className="btn-icon" onClick={() => navigate('/dogs')} style={{ marginRight: '0.5rem' }}>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
@@ -160,7 +160,7 @@ function DogDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '1.5rem', marginBottom: '1.5rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(320px, 100%), 1fr))', gap: '1.5rem', marginBottom: '1.5rem' }}>
|
||||
{/* Photo Section */}
|
||||
<div className="card" style={{ padding: '1rem' }}>
|
||||
<div style={{ marginBottom: '0.75rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
|
||||
@@ -272,6 +272,7 @@ function LitterDetail() {
|
||||
const [addMode, setAddMode] = useState('existing')
|
||||
const [error, setError] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [puppySort, setPuppySort] = useState('name')
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
|
||||
@@ -351,6 +352,12 @@ function LitterDetail() {
|
||||
|
||||
const puppyCount = litter.puppies?.length ?? 0
|
||||
|
||||
const sortedPuppies = [...(litter.puppies || [])].sort((a, b) => {
|
||||
if (puppySort === 'sex') return (a.sex || '').localeCompare(b.sex || '') || (a.name || '').localeCompare(b.name || '')
|
||||
if (puppySort === 'dob') return (a.date_of_birth || '').localeCompare(b.date_of_birth || '') || (a.name || '').localeCompare(b.name || '')
|
||||
return (a.name || '').localeCompare(b.name || '')
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{/* Header */}
|
||||
@@ -405,12 +412,27 @@ function LitterDetail() {
|
||||
)}
|
||||
|
||||
{/* Puppies section */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', flexWrap: 'wrap', gap: '0.75rem' }}>
|
||||
<h2 style={{ margin: 0 }}>Puppies</h2>
|
||||
<button className="btn btn-primary" onClick={() => { setShowAddPuppy(true); setError('') }}>
|
||||
<Plus size={16} style={{ marginRight: '0.4rem' }} />
|
||||
Add Puppy
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
{puppyCount > 1 && (
|
||||
<select
|
||||
value={puppySort}
|
||||
onChange={e => setPuppySort(e.target.value)}
|
||||
className="input"
|
||||
style={{ width: 'auto', fontSize: '0.85rem', padding: '0.4rem 0.6rem' }}
|
||||
aria-label="Sort puppies"
|
||||
>
|
||||
<option value="name">Sort: Name</option>
|
||||
<option value="sex">Sort: Sex</option>
|
||||
<option value="dob">Sort: Birth Date</option>
|
||||
</select>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={() => { setShowAddPuppy(true); setError('') }}>
|
||||
<Plus size={16} style={{ marginRight: '0.4rem' }} />
|
||||
Add Puppy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{puppyCount === 0 ? (
|
||||
@@ -419,8 +441,8 @@ function LitterDetail() {
|
||||
<p style={{ color: 'var(--text-secondary)' }}>No puppies linked yet. Add puppies to this litter.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: '1rem' }}>
|
||||
{litter.puppies.map(puppy => (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(220px, 100%), 1fr))', gap: '1rem' }}>
|
||||
{sortedPuppies.map(puppy => (
|
||||
<div key={puppy.id} className="card" style={{ position: 'relative' }}>
|
||||
<button
|
||||
className="btn-icon"
|
||||
|
||||
Reference in New Issue
Block a user