diff --git a/client/src/App.css b/client/src/App.css index 8b2b5ac..227c5b4 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -27,16 +27,16 @@ gap: 0.75rem; color: var(--text-primary); font-weight: 700; - font-size: 2.25rem; /* +30% from 1.5rem */ + font-size: 2.25rem; text-decoration: none; transition: var(--transition); } .nav-brand:hover { - color: var(--primary-light); + opacity: 0.9; } -/* Square logo: doubled from 2.5rem to 5rem */ +/* Square logo */ .brand-logo { width: 5rem; height: 5rem; @@ -45,7 +45,6 @@ display: block; border-radius: 4px; flex-shrink: 0; - /* Subtle diffuse black drop shadow for depth */ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.30)); } @@ -58,7 +57,7 @@ height: 5rem; background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%); border-radius: var(--radius); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + box-shadow: 0 4px 12px rgba(194, 134, 42, 0.3); } /* Title gradient: medium-dark gold → rusty dark red-gold */ @@ -68,7 +67,6 @@ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; - /* text-shadow doesn't work with background-clip:text — use filter instead */ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.50)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.30)); } @@ -76,6 +74,7 @@ .nav-links { display: flex; gap: 0.5rem; + align-items: center; } .nav-link { @@ -99,9 +98,22 @@ } .nav-link.active { - background: var(--primary); - color: white; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + background: linear-gradient(135deg, rgba(201,148,10,0.2) 0%, rgba(139,37,0,0.2) 100%); + color: var(--primary-light); + border-color: rgba(194, 134, 42, 0.4); + box-shadow: 0 2px 8px rgba(194, 134, 42, 0.15); +} + +/* Settings link — slightly different treatment, sits at end */ +.nav-link-settings { + margin-left: 0.5rem; + padding: 0.5rem; + border-radius: var(--radius-sm); + color: var(--text-muted); +} + +.nav-link-settings:hover { + color: var(--primary-light); } .main-content { @@ -114,10 +126,9 @@ } .nav-brand { - font-size: 1.625rem; /* +30% from 1.25rem */ + font-size: 1.625rem; } - /* Scale square logo down on mobile (doubled from 2rem) */ .brand-logo { width: 4rem; height: 4rem; diff --git a/client/src/App.jsx b/client/src/App.jsx index bddf6bc..729ff39 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,5 @@ -import { BrowserRouter as Router, Routes, Route, Link} from 'react-router-dom' -import { Home, Users, Activity, Heart, FlaskConical } from 'lucide-react' +import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom' +import { Home, Users, Activity, Heart, FlaskConical, Settings } from 'lucide-react' import Dashboard from './pages/Dashboard' import DogList from './pages/DogList' import DogDetail from './pages/DogDetail' @@ -8,60 +8,69 @@ import LitterList from './pages/LitterList' import LitterDetail from './pages/LitterDetail' import BreedingCalendar from './pages/BreedingCalendar' import PairingSimulator from './pages/PairingSimulator' +import SettingsPage from './pages/SettingsPage' +import { useSettings } from './hooks/useSettings' import './App.css' +function NavLink({ to, icon: Icon, label }) { + const location = useLocation() + const isActive = location.pathname === to + return ( + + + {label} + + ) +} + +function AppInner() { + const { settings } = useSettings() + const kennelName = settings?.kennel_name || 'BREEDR' + + return ( +
+ + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ) +} + function App() { return ( -
- - -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
+
) } diff --git a/client/src/components/ChampionBadge.jsx b/client/src/components/ChampionBadge.jsx new file mode 100644 index 0000000..81a43c0 --- /dev/null +++ b/client/src/components/ChampionBadge.jsx @@ -0,0 +1,52 @@ +/** + * ChampionBadge — shown on dogs with is_champion = 1 + * ChampionBloodlineBadge — shown on dogs whose sire OR dam is a champion + * + * Usage: + * + * + */ + +export function ChampionBadge({ size = 'sm' }) { + return ( + + {/* Crown SVG inline — no extra dep */} + + CH + + ) +} + +export function ChampionBloodlineBadge({ size = 'sm' }) { + return ( + + {/* Droplet / bloodline SVG */} + + BL + + ) +} diff --git a/client/src/components/DogForm.jsx b/client/src/components/DogForm.jsx index 750821a..24b6f24 100644 --- a/client/src/components/DogForm.jsx +++ b/client/src/components/DogForm.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { X } from 'lucide-react' +import { X, Award } from 'lucide-react' import axios from 'axios' function DogForm({ dog, onClose, onSave }) { @@ -12,9 +12,10 @@ function DogForm({ dog, onClose, onSave }) { color: '', microchip: '', notes: '', - sire_id: null, // Changed from '' to null - dam_id: null, // Changed from '' to null - litter_id: null // Changed from '' to null + sire_id: null, + dam_id: null, + litter_id: null, + is_champion: false, }) const [dogs, setDogs] = useState([]) const [litters, setLitters] = useState([]) @@ -36,9 +37,10 @@ function DogForm({ dog, onClose, onSave }) { color: dog.color || '', microchip: dog.microchip || '', notes: dog.notes || '', - sire_id: dog.sire?.id || null, // Ensure null, not '' - dam_id: dog.dam?.id || null, // Ensure null, not '' - litter_id: dog.litter_id || null // Ensure null, not '' + sire_id: dog.sire?.id || null, + dam_id: dog.dam?.id || null, + litter_id: dog.litter_id || null, + is_champion: !!dog.is_champion, }) setUseManualParents(!dog.litter_id) } @@ -48,8 +50,7 @@ function DogForm({ dog, onClose, onSave }) { try { const res = await axios.get('/api/dogs') setDogs(res.data || []) - } catch (error) { - console.error('Error fetching dogs:', error) + } catch (e) { setDogs([]) } } @@ -57,16 +58,11 @@ function DogForm({ dog, onClose, onSave }) { const fetchLitters = async () => { try { const res = await axios.get('/api/litters') - const litterData = res.data || [] - setLitters(litterData) - setLittersAvailable(litterData.length > 0) - // Only default to manual if no litters exist - if (litterData.length === 0) { - setUseManualParents(true) - } - } catch (error) { - console.error('Error fetching litters:', error) - // If endpoint fails, gracefully fallback to manual mode + const data = res.data || [] + setLitters(data) + setLittersAvailable(data.length > 0) + if (data.length === 0) setUseManualParents(true) + } catch (e) { setLitters([]) setLittersAvailable(false) setUseManualParents(true) @@ -74,25 +70,27 @@ function DogForm({ dog, onClose, onSave }) { } const handleChange = (e) => { - const { name, value } = e.target - - // Convert empty strings to null for ID fields - let processedValue = value - if (name === 'sire_id' || name === 'dam_id' || name === 'litter_id') { - processedValue = value === '' ? null : parseInt(value) + const { name, value, type, checked } = e.target + + if (type === 'checkbox') { + setFormData(prev => ({ ...prev, [name]: checked })) + return } - - setFormData(prev => ({ ...prev, [name]: processedValue })) - - // If litter is selected, auto-populate parents + + let processed = value + if (name === 'sire_id' || name === 'dam_id' || name === 'litter_id') { + processed = value === '' ? null : parseInt(value) + } + setFormData(prev => ({ ...prev, [name]: processed })) + if (name === 'litter_id' && value) { - const selectedLitter = litters.find(l => l.id === parseInt(value)) - if (selectedLitter) { + const sel = litters.find(l => l.id === parseInt(value)) + if (sel) { setFormData(prev => ({ ...prev, - sire_id: selectedLitter.sire_id, - dam_id: selectedLitter.dam_id, - breed: prev.breed || selectedLitter.sire_name?.split(' ')[0] || '' + sire_id: sel.sire_id, + dam_id: sel.dam_id, + breed: prev.breed || sel.sire_name?.split(' ')[0] || '' })) } } @@ -102,11 +100,10 @@ function DogForm({ dog, onClose, onSave }) { e.preventDefault() setError('') setLoading(true) - try { - const submitData = { + const submitData = { ...formData, - // Ensure null values are sent, not empty strings + is_champion: formData.is_champion ? 1 : 0, sire_id: formData.sire_id || null, dam_id: formData.dam_id || null, litter_id: useManualParents ? null : (formData.litter_id || null), @@ -114,25 +111,22 @@ function DogForm({ dog, onClose, onSave }) { birth_date: formData.birth_date || null, color: formData.color || null, microchip: formData.microchip || null, - notes: formData.notes || null + notes: formData.notes || null, } - if (dog) { - // Update existing dog await axios.put(`/api/dogs/${dog.id}`, submitData) } else { - // Create new dog await axios.post('/api/dogs', submitData) } onSave() onClose() - } catch (error) { - setError(error.response?.data?.error || 'Failed to save dog') + } catch (err) { + setError(err.response?.data?.error || 'Failed to save dog') setLoading(false) } } - const males = dogs.filter(d => d.sex === 'male' && d.id !== dog?.id) + 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 ( @@ -140,9 +134,7 @@ function DogForm({ dog, onClose, onSave }) {
e.stopPropagation()}>

{dog ? 'Edit Dog' : 'Add New Dog'}

- +
@@ -151,48 +143,25 @@ function DogForm({ dog, onClose, onSave }) {
- +
- +
- +
- @@ -200,62 +169,77 @@ function DogForm({ dog, onClose, onSave }) {
- +
- +
- +
- {/* Litter or Manual Parent Selection */} -
+ {/* Champion Toggle */} +
setFormData(prev => ({ ...prev, is_champion: !prev.is_champion }))} + > + e.stopPropagation()} + /> + +
+
+ Champion +
+
+ Mark this dog as a titled champion — offspring will display a Champion Bloodline badge +
+
+
+ + {/* Parent Section */} +
- + {littersAvailable && (
@@ -264,12 +248,8 @@ function DogForm({ dog, onClose, onSave }) { {!useManualParents && littersAvailable ? (
- {litters.map(l => (