import { useEffect, useRef, useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import PageHeader from '../components/PageHeader' import Card from '../components/Card' import Button from '../components/Button' import BreedSelect from '../components/BreedSelect' import { useAuth } from '../authContext' import { getSettings, updateSettings, exportDatabaseUrl, importData } from '../api' import { THEMES, applyTheme, DEFAULT_THEME } from '../lib/theme' const WEIGHT_UNIT_OPTIONS = [ { value: 'decimal_lbs', label: 'Decimal lbs' }, { value: 'lbs_oz', label: 'lbs + oz' }, ] function Toast({ message }) { if (!message) return null return (
{message}
) } function Settings() { const { mode, logout } = useAuth() const queryClient = useQueryClient() const { data: settings, isLoading } = useQuery({ queryKey: ['settings'], queryFn: getSettings }) const [form, setForm] = useState(null) const [saving, setSaving] = useState(false) const [toast, setToast] = useState(null) const importInputRef = useRef(null) const [pendingFile, setPendingFile] = useState(null) const [importing, setImporting] = useState(false) // Seed the form from settings exactly once (render-phase state adjustment): // form is null only until settings first load, so a later settings refetch // can't clobber the user's in-progress edits. if (settings && form === null) { setForm({ rabbitry_name: settings.rabbitry_name ?? '', weight_unit: settings.weight_unit ?? 'decimal_lbs', kit_prefix: settings.kit_prefix ?? '', default_breed: settings.default_breed ?? '', theme: settings.theme ?? DEFAULT_THEME, }) } useEffect(() => { if (!toast) return const timer = setTimeout(() => setToast(null), 2500) return () => clearTimeout(timer) }, [toast]) async function handleSave() { setSaving(true) try { await updateSettings(form) setToast('Settings saved') } catch (err) { setToast(err.message || 'Failed to save settings') } finally { setSaving(false) } } function handleThemeSelect(themeId) { setForm((prev) => ({ ...prev, theme: themeId })) // Apply immediately for live preview; handleSave persists it to the backend. applyTheme(themeId) } function handleExport() { window.location.href = exportDatabaseUrl() } function handleFilePicked(e) { const file = e.target.files?.[0] // Reset the input so picking the same file again still fires onChange. e.target.value = '' if (!file) return // Validate before uploading: an oversized file gets rejected by the server // mid-stream, which the browser surfaces as an opaque "network error" rather // than a readable message — so catch it (and obvious wrong file types) here. const name = file.name.toLowerCase() if (!name.endsWith('.csv') && !name.endsWith('.json')) { setToast('Please choose a .csv or .json file') return } if (file.size > 10 * 1024 * 1024) { setToast('File is too large (max 10 MB)') return } setPendingFile(file) } async function handleConfirmImport() { const file = pendingFile setImporting(true) try { const result = await importData(file) // Everything derived from rabbits/litters is now stale. queryClient.invalidateQueries() setToast(`Imported ${result.rabbits_imported} rabbits`) setPendingFile(null) } catch (err) { setToast(err.message || 'Import failed') } finally { setImporting(false) } } if (isLoading || !form) { return (
) } return (
setForm({ ...form, rabbitry_name: e.target.value })} className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent" />
{WEIGHT_UNIT_OPTIONS.map((opt) => ( ))}
setForm({ ...form, kit_prefix: e.target.value })} placeholder="e.g. RAG" className="mt-1 w-full rounded-md border border-border px-3 py-2 font-body text-sm focus:outline-none focus:ring-2 focus:ring-accent" />

Auto-generated kit names use this prefix (e.g. "{form.kit_prefix || 'Kit'}1").

setForm({ ...form, default_breed: breed })} />

Pre-fills the breed field when adding a new rabbit.

Selecting a theme previews it instantly. Save to keep it across your devices.

{THEMES.map((theme) => ( ))}

Export Database

Download a full backup of the SQLite database file.

Import Data

Load rabbits from a CSV or JSON file. This replaces your entire herd —{' '} all current rabbits and litters are deleted first.

{mode === 'authed' && (

Account

End this session on this device.

)}
{pendingFile && (

Replace all data?

Importing {pendingFile.name} will permanently delete all current rabbits and litters{' '} before loading the file. This cannot be undone.

)}
) } export default Settings