import { useState, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { useSearchParams } from 'react-router-dom' import api from '../lib/api' import { MagnifyingGlassIcon, MapIcon as MapIconOutline, ListBulletIcon, AdjustmentsHorizontalIcon, XMarkIcon } from '@heroicons/react/24/outline' import USMap from '../components/USMap' import MultiSelect from '../components/MultiSelect' interface Bill { bill_id: string bill_number: string title: string classification: string[] session: string session_name: string first_action_date: string latest_action_date: string latest_action: string latest_action_description: string jurisdiction: string jurisdiction_name: string } interface Session { session: string session_name: string start_date: string end_date: string bill_count: number } interface StateData { state: string total_bills: number type_counts: { ban: number restriction: number protection: number other: number } status_counts: { enacted: number failed: number pending: number } primary_type: string primary_status: string map_category: string } export default function PolicyMap() { const [searchParams, setSearchParams] = useSearchParams() const [viewMode, setViewMode] = useState<'map' | 'list'>('map') const [selectedState, setSelectedState] = useState('AL') const [selectedSession, setSelectedSession] = useState('') const [searchQuery, setSearchQuery] = useState('') const [sortBy, setSortBy] = useState<'date' | 'name'>('date') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') const [expandedBill, setExpandedBill] = useState(null) // Multi-select filter states const [selectedSessions, setSelectedSessions] = useState([]) const [selectedChambers, setSelectedChambers] = useState([]) const [selectedBillTypes, setSelectedBillTypes] = useState([]) const [selectedStatuses, setSelectedStatuses] = useState([]) // Read topic from URL - use state that syncs with URL const [selectedTopic, setSelectedTopic] = useState('') const [showTopicSelector, setShowTopicSelector] = useState(true) // Advanced filters sidebar state const [showAdvancedFilters, setShowAdvancedFilters] = useState(false) // Sync topic FROM URL to state (on mount and URL changes) useEffect(() => { const topicFromUrl = searchParams.get('topic') || '' if (topicFromUrl) { setSelectedTopic(topicFromUrl) setShowTopicSelector(false) console.log('πŸ”— Initialized topic from URL:', topicFromUrl) } else { setSelectedTopic('') setShowTopicSelector(true) } }, [searchParams]) // Re-run when URL changes // Sync topic changes TO URL (when user selects a topic) // IMPORTANT: Don't include searchParams in deps to avoid circular updates useEffect(() => { const currentTopicInUrl = searchParams.get('topic') || '' if (selectedTopic && !showTopicSelector) { // Only update URL if topic is different if (currentTopicInUrl !== selectedTopic) { setSearchParams({ topic: selectedTopic }, { replace: true }) console.log('πŸ“ Updated URL with topic:', selectedTopic) } } else if (showTopicSelector && currentTopicInUrl) { // Clear topic from URL if selector is shown setSearchParams({}, { replace: true }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedTopic, showTopicSelector, setSearchParams]) const [page, setPage] = useState(1) const limit = 20 // Fetch map aggregation data const { data: mapData, isLoading: mapLoading, error: mapError } = useQuery<{ states: Record topic: string | null session: string | null legend: { types: Record statuses: Record } }>({ queryKey: ['billsMap', selectedTopic, selectedSession], queryFn: async () => { const params = new URLSearchParams() if (selectedTopic) params.append('topic', selectedTopic) if (selectedSession) params.append('session', selectedSession) const response = await api.get(`/bills/map?${params}`) return response.data }, enabled: viewMode === 'map' && !showTopicSelector && selectedTopic !== '', staleTime: 5 * 60 * 1000, // 5 minutes - prevent refetch jitters refetchOnWindowFocus: false, retry: 2, retryDelay: 1000, }) // Fetch sessions const { data: sessionsData } = useQuery({ queryKey: ['sessions', selectedState, selectedTopic, selectedChambers, selectedBillTypes, selectedStatuses, searchQuery], queryFn: async () => { const params = new URLSearchParams({ state: selectedState }) if (selectedTopic) params.append('topic', selectedTopic) if (selectedChambers.length > 0) params.append('chambers', selectedChambers.join(',')) if (selectedBillTypes.length > 0) params.append('bill_types', selectedBillTypes.join(',')) if (selectedStatuses.length > 0) params.append('statuses', selectedStatuses.join(',')) if (searchQuery) params.append('q', searchQuery) // Debug logging console.log('πŸ” Fetching sessions with params:', { state: selectedState, topic: selectedTopic, chambers: selectedChambers, bill_types: selectedBillTypes, statuses: selectedStatuses, q: searchQuery, url: `/bills/sessions?${params}` }) const response = await api.get(`/bills/sessions?${params}`) console.log('βœ… Sessions response:', { total_sessions: response.data.total_sessions, sessions: response.data.sessions?.length }) return response.data }, enabled: viewMode === 'list' && !showTopicSelector && selectedTopic !== '', // Only fetch when actually needed staleTime: 5 * 60 * 1000, // 5 minutes - prevent refetch jitters refetchOnWindowFocus: false, retry: 2, retryDelay: 1000, }) // Fetch bills const { data: billsData, isLoading, error: billsError } = useQuery<{ total: number bills: Bill[] pagination: { limit: number; offset: number; has_more: boolean } }>({ queryKey: ['bills', selectedState, selectedSessions, searchQuery, selectedTopic, selectedChambers, selectedBillTypes, selectedStatuses, page], queryFn: async () => { const params = new URLSearchParams({ state: selectedState, limit: limit.toString(), offset: ((page - 1) * limit).toString(), }) if (selectedSessions.length > 0) params.append('sessions', selectedSessions.join(',')) if (searchQuery) params.append('q', searchQuery) if (selectedTopic) params.append('topic', selectedTopic) if (selectedChambers.length > 0) params.append('chambers', selectedChambers.join(',')) if (selectedBillTypes.length > 0) params.append('bill_types', selectedBillTypes.join(',')) if (selectedStatuses.length > 0) params.append('statuses', selectedStatuses.join(',')) const response = await api.get(`/bills?${params}`) return response.data }, enabled: viewMode === 'list' && !showTopicSelector && selectedTopic !== '', // Only fetch when actually needed staleTime: 5 * 60 * 1000, // 5 minutes - prevent refetch jitters refetchOnWindowFocus: false, retry: 2, retryDelay: 1000, }) const totalPages = Math.ceil((billsData?.total || 0) / limit) const handleStateClick = (stateCode: string) => { console.log('πŸ—ΊοΈ State clicked:', stateCode, 'Current topic:', selectedTopic) setSelectedState(stateCode) setViewMode('list') } const handleTopicSelect = (topic: string) => { setSelectedTopic(topic) setShowTopicSelector(false) setViewMode('map') } const handleBackToTopics = () => { setShowTopicSelector(true) setSelectedTopic('') } // Sort bills client-side const sortedBills = billsData?.bills ? [...billsData.bills].sort((a, b) => { if (sortBy === 'date') { const dateA = a.latest_action_date ? new Date(a.latest_action_date).getTime() : 0 const dateB = b.latest_action_date ? new Date(b.latest_action_date).getTime() : 0 return sortOrder === 'asc' ? dateA - dateB : dateB - dateA } else { // Sort by bill number const numA = a.bill_number const numB = b.bill_number return sortOrder === 'asc' ? numA.localeCompare(numB) : numB.localeCompare(numA) } }) : [] const totalStatesWithLegislation = mapData ? Object.values(mapData.states).filter(s => s.total_bills > 0).length : 0 const totalBillsAcrossStates = mapData ? Object.values(mapData.states).reduce((sum, s) => sum + s.total_bills, 0) : 0 return (
{/* Header */}

πŸ“œ Legislative Policy Map

{showTopicSelector ? 'Choose a topic to explore state-by-state legislation' : 'Track state legislation initiatives compared across the country' }

{/* Back to Map button - show when in list view */} {!showTopicSelector && viewMode === 'list' && ( )} {/* Back to Topics button */} {!showTopicSelector && ( )} {/* View Mode Toggle - only show when topic is selected */} {!showTopicSelector && (
)}
{/* Topic Selection View */} {showTopicSelector && (

Select a Policy Topic

Choose a topic below to see how states across the country are addressing it through legislation

{/* Fluoridation Card */} {/* Dental Health Card */} {/* Oral Health Card */} {/* Medicaid Card */} {/* Education Card */} {/* General Health Card */}
)} {/* Map and List View - only show when topic is selected */} {!showTopicSelector && ( <> {/* Selected Topic Badge */}
{selectedTopic === 'fluoride' && 'πŸ’§'} {selectedTopic === 'dental' && '🦷'} {selectedTopic === 'oral health' && '😁'} {selectedTopic === 'medicaid' && 'πŸ₯'} {selectedTopic === 'education' && 'πŸŽ“'} {selectedTopic === 'health' && '🏨'}
Viewing legislation for:
{selectedTopic === 'fluoride' ? 'Water Fluoridation' : selectedTopic === 'dental' ? 'Dental Health' : selectedTopic === 'oral health' ? 'Oral Health' : selectedTopic === 'medicaid' ? 'Medicaid' : selectedTopic === 'education' ? 'Education' : 'Health'}
{/* Basic Filters - Clean and Spacious */}
{/* State Filter - list view only */} {viewMode === 'list' && (
)} {/* Search - Prominent */}
setSearchQuery(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && setPage(1)} />
{/* Advanced Filters Button & Active Filter Indicator */} {viewMode === 'list' && (
{/* Active Filters Summary */} {(selectedSessions.length > 0 || selectedChambers.length > 0 || selectedBillTypes.length > 0 || selectedStatuses.length > 0 || searchQuery) && ( )}
)}
{/* Advanced Filters Sidebar */} {viewMode === 'list' && showAdvancedFilters && ( <> {/* Backdrop */}
setShowAdvancedFilters(false)} /> {/* Sidebar */}
{/* Header */}

Advanced Filters

{/* Filters */}
{/* Session Filter */}
{ const dateA = a.end_date ? new Date(a.end_date).getTime() : 0 const dateB = b.end_date ? new Date(b.end_date).getTime() : 0 return dateB - dateA }) .map((session: Session) => ({ value: session.session, label: session.session_name, count: session.bill_count })) || [] } selected={selectedSessions} onChange={(values) => { setSelectedSessions(values) setPage(1) }} placeholder="All Sessions" />
{/* Chamber Filter */}
{ setSelectedChambers(values) setPage(1) }} placeholder="All Chambers" />
{/* Bill Type Filter */}
{ setSelectedBillTypes(values) setPage(1) }} placeholder="All Types" />
{/* Status Filter */}
{ setSelectedStatuses(values) setPage(1) }} placeholder="All Statuses" />
{/* Sort Controls */}
{/* Footer Actions */}
)} {/* Map Visualization */} {viewMode === 'map' && (
{/* Clear Explanatory Title */}

{selectedTopic ? ( <> {selectedTopic.charAt(0).toUpperCase() + selectedTopic.slice(1)} Legislation Across the US ) : ( <>State-by-State Legislative Policy Overview )}

{selectedTopic === 'fluoride' && ( <>See which states mandate water fluoridation, which have removed it, and where funding or studies are underway. Each state's color shows the primary type of legislation, while darker/lighter shades indicate whether bills have been enacted, are pending, or have failed. )} {selectedTopic === 'dental' && ( <>Track dental health policies including coverage expansion, screening programs, provider access initiatives, and funding. Colors show the main focus of legislation in each state, with shading indicating current status. )} {selectedTopic === 'medicaid' && ( <>Monitor Medicaid program changes across states, including expansions, coverage modifications, reimbursement adjustments, and eligibility requirements. The map shows what type of Medicaid legislation is most active in each state. )} {selectedTopic === 'health' && ( <>View health-related legislation including protections, restrictions, funding initiatives, and healthcare reforms. Each state's color indicates the dominant type of health policy being considered or enacted. )} {selectedTopic === 'education' && ( <>Explore educational policy across states, from new requirements and curriculum changes to funding initiatives and system reforms. Colors represent the primary focus of education legislation in each state. )} {!selectedTopic && ( <>This interactive map shows legislative activity across all 50 states. Click any state to drill down into specific bills, or use the topic filter above to focus on a particular policy area. Colors indicate the primary type of legislation, while shading shows whether bills have been enacted (darker), are pending (normal), or failed (lighter). )}

{mapError ? (
⚠️

Failed to load map data

{String(mapError)}

) : mapLoading ? (

Loading map...

) : ( )}
)} {/* Stats Summary - Below Map */} {viewMode === 'map' && mapData && (
States with Legislation
{totalStatesWithLegislation}
{selectedTopic ? `matching "${selectedTopic}"` : 'all topics'}
Total Bills
{totalBillsAcrossStates.toLocaleString()}
across all states
Filter Topic
{selectedTopic || 'All Topics'}
Click map to drill down
)} {/* List View */} {viewMode === 'list' && billsData && (
Total Bills
{billsData.total.toLocaleString()}
{selectedSession ? `in ${selectedSession}` : 'all sessions'}
Sessions Available
{sessionsData?.total_sessions || 0}
{sessionsData?.sessions?.[0]?.session} - {sessionsData?.sessions?.[sessionsData.sessions.length - 1]?.session}
Showing
{billsData.bills.length}
Page {page} of {totalPages}
)} {/* Bills List */} {viewMode === 'list' && ( <> {billsError ? (
⚠️

Unable to Load Bills

{billsError instanceof Error ? billsError.message : 'There was an error fetching bills data. The API server may be unavailable.'}

) : isLoading ? (

Loading bills...

) : billsData && billsData.total === 0 ? (
πŸ“­

No Bills Found

{selectedState === 'LA' || selectedState === 'Louisiana' ? ( <>Louisiana data is not yet available in our database. We currently have data for Alabama, Georgia, Massachusetts, Washington, and Wisconsin. ) : ( <>No bills found for the selected filters. Try adjusting your search criteria or clearing filters. )}

{(selectedSessions.length > 0 || selectedChambers.length > 0 || selectedBillTypes.length > 0 || selectedStatuses.length > 0 || searchQuery) ? ( ) : null}
) : ( <>
{sortedBills.map((bill) => { const isExpanded = expandedBill === bill.bill_id return (
{/* Bill Header - Always Visible */}
setExpandedBill(isExpanded ? null : bill.bill_id)} >
{bill.bill_number} {bill.classification && bill.classification.length > 0 && ( {bill.classification.join(', ')} )} {bill.session_name}

{bill.title}

Latest Action: {bill.latest_action_description || bill.latest_action || 'N/A'} {bill.latest_action_date && ( Date:{' '} {new Date(bill.latest_action_date).toLocaleDateString()} )}
{/* Expanded Details */} {isExpanded && (

Jurisdiction

{bill.jurisdiction_name}

Session

{bill.session_name} ({bill.session})

{bill.first_action_date && (

First Action

{new Date(bill.first_action_date).toLocaleDateString()}

)}

Bill ID

{bill.bill_id}

)}
)})}
{/* Pagination */} {totalPages > 1 && (
Showing {(page - 1) * limit + 1} to{' '} {Math.min(page * limit, billsData?.total || 0)} of{' '} {billsData?.total.toLocaleString()} bills
Page {page} of {totalPages}
)} {/* No Results */} {!isLoading && !billsError && billsData && billsData.bills.length === 0 && (

No bills found matching your filters.

)} )} )} )}
) }