import { useEffect, useMemo, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getArbaShows, refreshArbaShows } from '../api' import Card from '../components/Card' import Button from '../components/Button' import PageHeader from '../components/PageHeader' import EmptyState from '../components/EmptyState' import AddToEventModal from '../components/AddToEventModal' import { Input } from '../components/FormControls' const PAGE_SIZE = 50 // The five buckets lib/arba.ts normalizes to. 'OTHER' is intentionally omitted // from the filter — it's a small catch-all for rows whose show-information // string didn't match the expected pattern, and they still show when no // show-type filter is active. const SHOW_TYPE_OPTIONS = [ { value: 'ALL BREED OPEN', label: 'All Breed — Open' }, { value: 'ALL BREED YOUTH', label: 'All Breed — Youth' }, { value: 'SPECIALTY OPEN', label: 'Specialty — Open' }, { value: 'SPECIALTY YOUTH', label: 'Specialty — Youth' }, { value: 'FAIR SHOW', label: 'Fair Show' }, ] function todayIso() { return new Date().toISOString().slice(0, 10) } function formatTimestamp(iso) { if (!iso) return 'never' const d = new Date(iso) if (Number.isNaN(d.getTime())) return 'unknown' return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }) } function ChevronIcon() { return ( ) } // Compact checkbox dropdown used for State / District / Show Type. Closes on // outside click. `options` is [{ value, label }]; `selected` is an array of // values. function MultiSelect({ label, options, selected, onChange }) { const [open, setOpen] = useState(false) const ref = useRef(null) useEffect(() => { if (!open) return undefined function onClick(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) } document.addEventListener('mousedown', onClick) return () => document.removeEventListener('mousedown', onClick) }, [open]) function toggle(value) { if (selected.includes(value)) onChange(selected.filter((v) => v !== value)) else onChange([...selected, value]) } return (
{open && (
{selected.length > 0 && ( )} {options.map((option) => ( ))}
)}
) } function MapPinIcon() { return ( ) } // Phone/tablet layout for one show: the same fields as a table row, stacked, // so the list never scrolls sideways. function ShowListCard({ show: s, onAdd }) { const place = [s.city, s.state].filter(Boolean).join(', ') return (

{s.club_name}

{s.show_type_full || s.show_type} {s.special_breed ? ` · ${s.special_breed}` : ''}

{s.raw_date || s.date || '—'}

{place || '—'} {s.district ? ` · District ${s.district}` : ''}

{s.secretary || s.email ? (

{s.secretary} {s.secretary && s.email ? ' · ' : ''} {s.email ? ( {s.email} ) : null}

) : null}
{s.maps_url ? ( Map ) : null} {s.catalog_url ? ( Catalog ) : null}
) } function Shows() { const queryClient = useQueryClient() const { data, isLoading, isError, error } = useQuery({ queryKey: ['arba-shows'], queryFn: getArbaShows, }) const refresh = useMutation({ mutationFn: refreshArbaShows, onSuccess: (result) => queryClient.setQueryData(['arba-shows'], result), }) const shows = useMemo(() => data?.shows ?? [], [data]) // Filter state const [states, setStates] = useState([]) const [districts, setDistricts] = useState([]) const [showTypes, setShowTypes] = useState([]) const [dateFrom, setDateFrom] = useState(todayIso) const [dateTo, setDateTo] = useState('') const [keyword, setKeyword] = useState('') const [page, setPage] = useState(1) const [addingShow, setAddingShow] = useState(null) // Options derived from the loaded data. const stateOptions = useMemo(() => { const set = [...new Set(shows.map((s) => s.state).filter(Boolean))].sort() return set.map((v) => ({ value: v, label: v })) }, [shows]) const districtOptions = useMemo(() => { const set = [...new Set(shows.map((s) => s.district ?? ''))].sort((a, b) => { if (a === '') return 1 if (b === '') return -1 return Number(a) - Number(b) }) return set.map((v) => ({ value: v, label: v === '' ? '— (none)' : v })) }, [shows]) const filtered = useMemo(() => { const kw = keyword.trim().toLowerCase() return shows.filter((s) => { if (states.length && !states.includes(s.state)) return false if (districts.length && !districts.includes(s.district ?? '')) return false if (showTypes.length && !showTypes.includes(s.show_type)) return false if (dateFrom && (!s.date || s.date < dateFrom)) return false if (dateTo && (!s.date || s.date > dateTo)) return false if (kw) { const haystack = `${s.club_name} ${s.city} ${s.secretary}`.toLowerCase() if (!haystack.includes(kw)) return false } return true }) }, [shows, states, districts, showTypes, dateFrom, dateTo, keyword]) // Snap back to page 1 whenever the filtered set changes size/content. const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) const currentPage = Math.min(page, totalPages) const pageRows = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE) function resetFilters() { setStates([]) setDistricts([]) setShowTypes([]) setDateFrom(todayIso()) setDateTo('') setKeyword('') setPage(1) } const hasActiveFilters = states.length || districts.length || showTypes.length || dateTo || keyword || dateFrom !== todayIso() return (
Updated {formatTimestamp(data?.fetched_at)} {data?.stale ? ' (stale)' : ''}
} /> {/* Filter bar */}
{ setStates(v); setPage(1) }} /> { setDistricts(v); setPage(1) }} /> { setShowTypes(v); setPage(1) }} /> { setKeyword(e.target.value); setPage(1) }} placeholder="Club, city, or secretary" aria-label="Search by club, city, or secretary" className="w-56" /> {hasActiveFilters ? ( ) : null}
{refresh.isError ? (

Refresh failed: {refresh.error.message}

) : null} {isError ? ( refresh.mutate()}>Try Again} /> ) : isLoading ? (

Loading shows…

) : filtered.length === 0 ? ( Reset Filters : null} /> ) : ( <>

{filtered.length} show{filtered.length === 1 ? '' : 's'}

{pageRows.map((s, i) => ( setAddingShow(s)} /> ))}
{/* The full table needs ~900px, which only fits beside the sidebar on wide screens. */} {pageRows.map((s, i) => ( ))}
Date State Dist. City Club / Show Specialty Secretary Links
{s.raw_date || s.date || '—'} {s.state} {s.district || '—'} {s.city}
{s.club_name}
{s.show_type_full || s.show_type}
{s.special_breed || '—'}
{s.secretary || '—'}
{s.email ? ( {s.email} ) : null}
{s.maps_url ? ( ) : null} {s.catalog_url ? ( Catalog ) : null}
{totalPages > 1 ? (
Page {currentPage} of {totalPages}
) : null} )} {addingShow ? ( setAddingShow(null)} /> ) : null} ) } export default Shows