- {/* Mobile Search */}
-
+ {/* Mobile Navigation Links */}
+
{/* Mobile State Selection */}
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 97933ba..8a227f5 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -15,14 +15,15 @@ const Hero = () => {
// Available filter options
const stateOptions = [
- 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut',
+ 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut',
+ 'District of Columbia',
'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa',
'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan',
'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota',
'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia',
- 'Wisconsin', 'Wyoming', 'District of Columbia'
+ 'Wisconsin', 'Wyoming'
]
const commenterTypeOptions = [
diff --git a/src/components/StateDirectory.tsx b/src/components/StateDirectory.tsx
index 264f424..be8a53e 100644
--- a/src/components/StateDirectory.tsx
+++ b/src/components/StateDirectory.tsx
@@ -62,8 +62,8 @@ const StateDirectory = () => {
const handleStateSelect = (stateCode: string) => {
setSelectedState(stateCode);
setShowDropdown(false);
- // Navigate to state subdomain
- window.location.href = `https://${stateCode.toLowerCase()}.opencomments.us`;
+ // Navigate to state page
+ window.location.href = `/state/${stateCode.toLowerCase()}`;
};
// Flag-style coloring: Blue field (top-left) + Red/White stripes
diff --git a/src/hooks/useCommentSearch.ts b/src/hooks/useCommentSearch.ts
index 2890760..f6896fb 100644
--- a/src/hooks/useCommentSearch.ts
+++ b/src/hooks/useCommentSearch.ts
@@ -5,6 +5,10 @@ export interface CommentSearchFilters {
query?: string
agency_name?: string
state?: string
+ comment_filter?: string
+ filing_company?: string
+ comment_id?: string
+ docket_id?: string
tags?: string[]
date_from?: string
date_to?: string
@@ -33,6 +37,7 @@ export interface CommentSearchResult {
tags: string[]
attachment_count: number
rank: number
+ total_count?: number
}
export interface CommentDetail {
@@ -83,8 +88,12 @@ export const useCommentSearch = () => {
p_date_to: filters.date_to ? new Date(filters.date_to).toISOString() : null,
p_commenter_type: filters.commenter_type || null,
p_position: filters.position || null,
+ p_comment_filter: filters.comment_filter || null,
+ p_filing_company: filters.filing_company || null,
+ p_comment_id: filters.comment_id || null,
+ p_docket_id: filters.docket_id || null,
p_sort_by: filters.sort_by || 'newest',
- p_limit: filters.limit || 20,
+ p_limit: filters.limit || 10,
p_offset: filters.offset || 0
})
@@ -102,8 +111,13 @@ export const useCommentSearch = () => {
setResults(prev => [...prev, ...searchResults])
}
- setHasMore(searchResults.length === (filters.limit || 20))
- setTotal(prev => filters.offset === 0 ? searchResults.length : prev + searchResults.length)
+ setHasMore(searchResults.length === (filters.limit || 10))
+ // Use the total_count from the first result if available, otherwise fall back to previous logic
+ if (searchResults.length > 0 && searchResults[0].total_count !== undefined) {
+ setTotal(searchResults[0].total_count)
+ } else {
+ setTotal(prev => filters.offset === 0 ? searchResults.length : prev + searchResults.length)
+ }
} catch (err) {
console.error('Search error:', err)
diff --git a/src/hooks/usePublicBrowse.ts b/src/hooks/usePublicBrowse.ts
index f97e88f..9dd4720 100644
--- a/src/hooks/usePublicBrowse.ts
+++ b/src/hooks/usePublicBrowse.ts
@@ -17,6 +17,7 @@ export interface PublicDocket {
comment_count: number
created_at: string
rank?: number
+ total_count?: number
}
export interface DocketSearchFilters {
@@ -27,7 +28,7 @@ export interface DocketSearchFilters {
tags?: string[]
date_from?: string
date_to?: string
- sort_by?: 'newest' | 'closing' | 'title' | 'agency'
+ sort_by?: 'newest' | 'oldest' | 'title_asc' | 'title_desc' | 'agency_asc' | 'agency_desc' | 'closing_soon'
limit?: number
offset?: number
}
@@ -107,7 +108,7 @@ export const usePublicBrowse = () => {
p_date_from: filters.date_from ? new Date(filters.date_from).toISOString() : null,
p_date_to: filters.date_to ? new Date(filters.date_to).toISOString() : null,
p_sort_by: filters.sort_by || 'newest',
- p_limit: filters.limit || 20,
+ p_limit: filters.limit || 10,
p_offset: filters.offset || 0
})
@@ -119,15 +120,17 @@ export const usePublicBrowse = () => {
const results = data || []
- if (filters.offset === 0) {
- setDockets(results)
+ // Always replace dockets for pagination, don't append
+ setDockets(results)
+
+ setHasMore(results.length === (filters.limit || 10))
+ // Use the total_count from the first result if available, otherwise fall back to previous logic
+ if (results.length > 0 && results[0].total_count !== undefined) {
+ setTotal(results[0].total_count)
} else {
- setDockets(prev => [...prev, ...results])
+ setTotal(prev => filters.offset === 0 ? results.length : prev + results.length)
}
- setHasMore(results.length === (filters.limit || 20))
- setTotal(prev => filters.offset === 0 ? results.length : prev + results.length)
-
} catch (err) {
console.error('Browse error:', err)
setError('An unexpected error occurred')
@@ -173,45 +176,21 @@ export const useAgencyProfile = () => {
setError(null)
try {
- // Fetch agency data directly
- const { data: agencyData, error: agencyError } = await supabase
- .from('agencies')
- .select('*')
- .eq('slug', agencySlug)
- .single()
+ const { data, error: fetchError } = await supabase.rpc('get_agency_public_profile', {
+ p_agency_slug: agencySlug
+ })
- if (agencyError || !agencyData) {
- console.error('Agency fetch error:', agencyError)
+ if (fetchError || !data || data.length === 0) {
+ console.error('Agency fetch error:', fetchError)
setError('Agency not found')
return
}
- // Fetch dockets for this agency
- const { data: docketsData, error: docketsError } = await supabase
- .from('dockets')
- .select(`
- id,
- title,
- slug,
- status,
- open_at,
- close_at,
- tags,
- created_at,
- comments!inner(id)
- `)
- .eq('agency_id', agencyData.id)
- .in('status', ['open', 'closed'])
- .order('created_at', { ascending: false })
-
- if (docketsError) {
- console.error('Dockets fetch error:', docketsError)
- setError('Failed to load agency dockets')
- return
- }
-
- // Transform the data to match the expected format
- const transformedDockets = docketsData?.map(docket => ({
+ const agencyData = data[0]
+
+ // Transform the dockets data
+ const dockets = agencyData.dockets || []
+ const transformedDockets = dockets.map((docket: any) => ({
id: docket.id,
title: docket.title,
slug: docket.slug,
@@ -219,9 +198,9 @@ export const useAgencyProfile = () => {
open_at: docket.open_at,
close_at: docket.close_at,
tags: docket.tags || [],
- comment_count: docket.comments?.length || 0,
+ comment_count: docket.comment_count || 0,
created_at: docket.created_at
- })) || []
+ }))
// Create the agency profile object
const agencyProfile: AgencyProfile = {
diff --git a/src/pages/SearchResults.tsx b/src/pages/SearchResults.tsx
index 8d70168..305e281 100644
--- a/src/pages/SearchResults.tsx
+++ b/src/pages/SearchResults.tsx
@@ -386,28 +386,17 @@ const SearchResults = () => {
{docket.agency_name}
- {docket.tags.length > 0 && (
-
- {docket.tags.slice(0, 3).map(tag => (
-
- {tag}
-
- ))}
- {docket.tags.length > 3 && (
-
- +{docket.tags.length - 3} more
-
- )}
-
- )}
+
- {docket.comment_count} comments
+
+ {docket.comment_count} comments
+
{docket.close_at && (
diff --git a/src/pages/agency/DocketDetail.tsx b/src/pages/agency/DocketDetail.tsx
index 2598d90..35abe34 100644
--- a/src/pages/agency/DocketDetail.tsx
+++ b/src/pages/agency/DocketDetail.tsx
@@ -493,21 +493,7 @@ const DocketDetail = () => {
- {docket.tags.length > 0 && (
-
-
Tags
-
- {docket.tags.map(tag => (
-
- {tag}
-
- ))}
-
-
- )}
+
{docket.supportingDocs.length > 0 && (
diff --git a/src/pages/public/AgencyProfile.tsx b/src/pages/public/AgencyProfile.tsx
index bdd033b..c6cc509 100644
--- a/src/pages/public/AgencyProfile.tsx
+++ b/src/pages/public/AgencyProfile.tsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
import { useParams, Link } from 'react-router-dom'
import { useAgencyProfile } from '../../hooks/usePublicBrowse'
import PublicLayout from '../../components/PublicLayout'
+import Breadcrumb from '../../components/Breadcrumb'
import {
Building2,
MapPin,
@@ -34,6 +35,63 @@ const AgencyProfile = () => {
})
}
+ const getStateAbbreviation = (stateName: string) => {
+ const stateAbbreviations: Record
= {
+ 'Alabama': 'al',
+ 'Alaska': 'ak',
+ 'Arizona': 'az',
+ 'Arkansas': 'ar',
+ 'California': 'ca',
+ 'Colorado': 'co',
+ 'Connecticut': 'ct',
+ 'Delaware': 'de',
+ 'Florida': 'fl',
+ 'Georgia': 'ga',
+ 'Hawaii': 'hi',
+ 'Idaho': 'id',
+ 'Illinois': 'il',
+ 'Indiana': 'in',
+ 'Iowa': 'ia',
+ 'Kansas': 'ks',
+ 'Kentucky': 'ky',
+ 'Louisiana': 'la',
+ 'Maine': 'me',
+ 'Maryland': 'md',
+ 'Massachusetts': 'ma',
+ 'Michigan': 'mi',
+ 'Minnesota': 'mn',
+ 'Mississippi': 'ms',
+ 'Missouri': 'mo',
+ 'Montana': 'mt',
+ 'Nebraska': 'ne',
+ 'Nevada': 'nv',
+ 'New Hampshire': 'nh',
+ 'New Jersey': 'nj',
+ 'New Mexico': 'nm',
+ 'New York': 'ny',
+ 'North Carolina': 'nc',
+ 'North Dakota': 'nd',
+ 'Ohio': 'oh',
+ 'Oklahoma': 'ok',
+ 'Oregon': 'or',
+ 'Pennsylvania': 'pa',
+ 'Rhode Island': 'ri',
+ 'South Carolina': 'sc',
+ 'South Dakota': 'sd',
+ 'Tennessee': 'tn',
+ 'Texas': 'tx',
+ 'Utah': 'ut',
+ 'Vermont': 'vt',
+ 'Virginia': 'va',
+ 'Washington': 'wa',
+ 'West Virginia': 'wv',
+ 'Wisconsin': 'wi',
+ 'Wyoming': 'wy',
+ 'District of Columbia': 'dc'
+ }
+ return stateAbbreviations[stateName] || stateName.toLowerCase().replace(/\s+/g, '-')
+ }
+
const getDaysRemaining = (closeDate?: string) => {
if (!closeDate) return null
const now = new Date()
@@ -130,15 +188,12 @@ const AgencyProfile = () => {
>
{/* Breadcrumb */}
-
-
-
- Back to All Dockets
-
-
+
{/* Agency Header */}
@@ -304,22 +359,14 @@ const AgencyProfile = () => {
)}
- {docket.comment_count} comments
+
+ {docket.comment_count} comments
+
-
- {docket.tags.length > 0 && (
-
- {docket.tags.map(tag => (
-
- {tag}
-
- ))}
-
- )}
diff --git a/src/pages/public/CommentDetail.tsx b/src/pages/public/CommentDetail.tsx
index 53b0d7a..8e637c4 100644
--- a/src/pages/public/CommentDetail.tsx
+++ b/src/pages/public/CommentDetail.tsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
import { useParams, Link, useNavigate } from 'react-router-dom'
import { useCommentDetail } from '../../hooks/useCommentSearch'
import PublicLayout from '../../components/PublicLayout'
+import Breadcrumb from '../../components/Breadcrumb'
import {
ChevronLeft,
Calendar,
@@ -59,6 +60,63 @@ const CommentDetail = () => {
}
}
+ const getStateAbbreviation = (stateName: string) => {
+ const stateAbbreviations: Record
= {
+ 'Alabama': 'al',
+ 'Alaska': 'ak',
+ 'Arizona': 'az',
+ 'Arkansas': 'ar',
+ 'California': 'ca',
+ 'Colorado': 'co',
+ 'Connecticut': 'ct',
+ 'Delaware': 'de',
+ 'Florida': 'fl',
+ 'Georgia': 'ga',
+ 'Hawaii': 'hi',
+ 'Idaho': 'id',
+ 'Illinois': 'il',
+ 'Indiana': 'in',
+ 'Iowa': 'ia',
+ 'Kansas': 'ks',
+ 'Kentucky': 'ky',
+ 'Louisiana': 'la',
+ 'Maine': 'me',
+ 'Maryland': 'md',
+ 'Massachusetts': 'ma',
+ 'Michigan': 'mi',
+ 'Minnesota': 'mn',
+ 'Mississippi': 'ms',
+ 'Missouri': 'mo',
+ 'Montana': 'mt',
+ 'Nebraska': 'ne',
+ 'Nevada': 'nv',
+ 'New Hampshire': 'nh',
+ 'New Jersey': 'nj',
+ 'New Mexico': 'nm',
+ 'New York': 'ny',
+ 'North Carolina': 'nc',
+ 'North Dakota': 'nd',
+ 'Ohio': 'oh',
+ 'Oklahoma': 'ok',
+ 'Oregon': 'or',
+ 'Pennsylvania': 'pa',
+ 'Rhode Island': 'ri',
+ 'South Carolina': 'sc',
+ 'South Dakota': 'sd',
+ 'Tennessee': 'tn',
+ 'Texas': 'tx',
+ 'Utah': 'ut',
+ 'Vermont': 'vt',
+ 'Virginia': 'va',
+ 'Washington': 'wa',
+ 'West Virginia': 'wv',
+ 'Wisconsin': 'wi',
+ 'Wyoming': 'wy',
+ 'District of Columbia': 'dc'
+ }
+ return stateAbbreviations[stateName] || stateName.toLowerCase().replace(/\s+/g, '-')
+ }
+
if (loading) {
return (
@@ -101,15 +159,13 @@ const CommentDetail = () => {
>
{/* Breadcrumb */}
-
-
-
+
{/* Header Section */}
diff --git a/src/pages/public/CommentSearch.tsx b/src/pages/public/CommentSearch.tsx
index dfc7d8a..01e9043 100644
--- a/src/pages/public/CommentSearch.tsx
+++ b/src/pages/public/CommentSearch.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react'
+import React, { useState, useEffect, useCallback } from 'react'
import { useSearchParams, Link } from 'react-router-dom'
import { useCommentSearch, CommentSearchFilters } from '../../hooks/useCommentSearch'
import PublicLayout from '../../components/PublicLayout'
@@ -18,17 +18,67 @@ import {
const CommentSearch = () => {
const [searchParams, setSearchParams] = useSearchParams()
- const query = searchParams.get('q') || ''
+ const query = (() => {
+ const q = searchParams.get('q')
+ return q && q.length <= 500 ? q : ''
+ })()
const { results, loading, error, hasMore, total, searchComments, loadMore, reset } = useCommentSearch()
const [filters, setFilters] = useState
({
query: query,
- sort_by: 'newest',
- limit: 20,
- offset: 0
+ sort_by: (() => {
+ const sort = searchParams.get('sort')
+ const validSorts = ['newest', 'oldest', 'agency', 'docket'] as const
+ return validSorts.includes(sort as any) ? (sort as any) : 'newest'
+ })(),
+ limit: Math.max(1, Math.min(50, parseInt(searchParams.get('limit') || '10') || 10)),
+ offset: Math.max(0, parseInt(searchParams.get('offset') || '0') || 0),
+ agency_name: (() => {
+ const agency = searchParams.get('agency')
+ return agency && agency.length <= 200 ? agency : undefined
+ })(),
+ state: (() => {
+ const state = searchParams.get('state')
+ return state && state.length <= 100 ? state : undefined
+ })(),
+ comment_filter: (() => {
+ const filter = searchParams.get('comment_filter')
+ return filter && filter.length <= 200 ? filter : undefined
+ })(),
+ filing_company: (() => {
+ const company = searchParams.get('filing_company')
+ return company && company.length <= 200 ? company : undefined
+ })(),
+ comment_id: (() => {
+ const id = searchParams.get('comment_id')
+ return id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ? id : undefined
+ })(),
+ docket_id: (() => {
+ const id = searchParams.get('docket_id')
+ return id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ? id : undefined
+ })(),
+ date_from: (() => {
+ const date = searchParams.get('date_from')
+ return date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined
+ })(),
+ date_to: (() => {
+ const date = searchParams.get('date_to')
+ return date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined
+ })(),
+ commenter_type: (() => {
+ const type = searchParams.get('commenter_type')
+ const validTypes = ['individual', 'organization', 'agent', 'anonymous'] as const
+ return validTypes.includes(type as any) ? (type as any) : undefined
+ })(),
+ position: (() => {
+ const pos = searchParams.get('position')
+ const validPositions = ['support', 'oppose', 'neutral', 'unclear', 'not_specified'] as const
+ return validPositions.includes(pos as any) ? (pos as any) : undefined
+ })()
})
const [showFilters, setShowFilters] = useState(false)
+ const [searchTimeout, setSearchTimeout] = useState(null)
// Available filter options
const stateOptions = [
@@ -65,31 +115,127 @@ const CommentSearch = () => {
]
useEffect(() => {
- if (query) {
- const searchFilters = { ...filters, query, offset: 0 }
- setFilters(searchFilters)
- reset()
- searchComments(searchFilters)
- } else {
- // Show all results if no query
- const searchFilters = { ...filters, offset: 0 }
- reset()
- searchComments(searchFilters)
+ // Handle URL parameter changes
+ const urlQuery = (() => {
+ const q = searchParams.get('q')
+ return q && q.length <= 500 ? q : ''
+ })()
+ const urlSort = (() => {
+ const sort = searchParams.get('sort')
+ const validSorts = ['newest', 'oldest', 'agency', 'docket'] as const
+ return validSorts.includes(sort as any) ? (sort as any) : 'newest'
+ })()
+ const urlLimit = Math.max(1, Math.min(50, parseInt(searchParams.get('limit') || '10') || 10))
+ const urlOffset = Math.max(0, parseInt(searchParams.get('offset') || '0') || 0)
+ const urlAgency = (() => {
+ const agency = searchParams.get('agency')
+ return agency && agency.length <= 200 ? agency : undefined
+ })()
+ const urlState = (() => {
+ const state = searchParams.get('state')
+ return state && state.length <= 100 ? state : undefined
+ })()
+ const urlCommentFilter = (() => {
+ const filter = searchParams.get('comment_filter')
+ return filter && filter.length <= 200 ? filter : undefined
+ })()
+ const urlFilingCompany = (() => {
+ const company = searchParams.get('filing_company')
+ return company && company.length <= 200 ? company : undefined
+ })()
+ const urlCommentId = (() => {
+ const id = searchParams.get('comment_id')
+ return id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ? id : undefined
+ })()
+ const urlDocketId = (() => {
+ const id = searchParams.get('docket_id')
+ return id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ? id : undefined
+ })()
+ const urlDateFrom = (() => {
+ const date = searchParams.get('date_from')
+ return date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined
+ })()
+ const urlDateTo = (() => {
+ const date = searchParams.get('date_to')
+ return date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined
+ })()
+ const urlCommenterType = (() => {
+ const type = searchParams.get('commenter_type')
+ const validTypes = ['individual', 'organization', 'agent', 'anonymous'] as const
+ return validTypes.includes(type as any) ? (type as any) : undefined
+ })()
+ const urlPosition = (() => {
+ const pos = searchParams.get('position')
+ const validPositions = ['support', 'oppose', 'neutral', 'unclear', 'not_specified'] as const
+ return validPositions.includes(pos as any) ? (pos as any) : undefined
+ })()
+
+ const newFilters = {
+ query: urlQuery,
+ sort_by: urlSort,
+ limit: urlLimit,
+ offset: urlOffset,
+ agency_name: urlAgency,
+ state: urlState,
+ comment_filter: urlCommentFilter,
+ filing_company: urlFilingCompany,
+ comment_id: urlCommentId,
+ docket_id: urlDocketId,
+ date_from: urlDateFrom,
+ date_to: urlDateTo,
+ commenter_type: urlCommenterType,
+ position: urlPosition
+ }
+
+ setFilters(newFilters)
+ reset()
+ searchComments(newFilters)
+ }, [searchParams, reset, searchComments])
+
+ // Function to update URL with all parameters
+ const updateURL = useCallback((newFilters: CommentSearchFilters) => {
+ const newSearchParams = new URLSearchParams()
+
+ if (newFilters.query) newSearchParams.set('q', newFilters.query)
+ if (newFilters.sort_by) newSearchParams.set('sort', newFilters.sort_by)
+ if (newFilters.limit) newSearchParams.set('limit', newFilters.limit.toString())
+ if (newFilters.offset) newSearchParams.set('offset', newFilters.offset.toString())
+ if (newFilters.agency_name) newSearchParams.set('agency', newFilters.agency_name)
+ if (newFilters.state) newSearchParams.set('state', newFilters.state)
+ if (newFilters.comment_filter) newSearchParams.set('comment_filter', newFilters.comment_filter)
+ if (newFilters.filing_company) newSearchParams.set('filing_company', newFilters.filing_company)
+ if (newFilters.comment_id) newSearchParams.set('comment_id', newFilters.comment_id)
+ if (newFilters.docket_id) newSearchParams.set('docket_id', newFilters.docket_id)
+ if (newFilters.date_from) newSearchParams.set('date_from', newFilters.date_from)
+ if (newFilters.date_to) newSearchParams.set('date_to', newFilters.date_to)
+ if (newFilters.commenter_type) newSearchParams.set('commenter_type', newFilters.commenter_type)
+ if (newFilters.position) newSearchParams.set('position', newFilters.position)
+
+ setSearchParams(newSearchParams)
+ }, [setSearchParams])
+
+ // Debounced search function
+ const debouncedSearch = useCallback((query: string) => {
+ if (searchTimeout) {
+ clearTimeout(searchTimeout)
}
- }, [query])
+
+ const timeout = setTimeout(() => {
+ const newFilters = { ...filters, query, offset: 0 }
+ setFilters(newFilters)
+ updateURL(newFilters)
+ reset()
+ searchComments(newFilters)
+ }, 150) // 150ms delay
+
+ setSearchTimeout(timeout)
+ }, [filters, searchComments, reset, updateURL])
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
const newFilters = { ...filters, offset: 0 }
setFilters(newFilters)
-
- // Update URL with search query
- const newSearchParams = new URLSearchParams()
- if (newFilters.query) {
- newSearchParams.set('q', newFilters.query)
- }
- setSearchParams(newSearchParams)
-
+ updateURL(newFilters)
reset()
searchComments(newFilters)
}
@@ -97,15 +243,26 @@ const CommentSearch = () => {
const handleFilterChange = (key: keyof CommentSearchFilters, value: any) => {
const newFilters = { ...filters, [key]: value, offset: 0 }
setFilters(newFilters)
- reset()
- searchComments(newFilters)
+ updateURL(newFilters)
+
+ // For text fields, use debounced search
+ const textFields = ['agency_name', 'comment_filter', 'filing_company', 'comment_id', 'docket_id']
+ if (textFields.includes(key) && typeof value === 'string') {
+ // Search immediately for text fields
+ reset()
+ searchComments(newFilters)
+ } else {
+ // For non-text fields (dropdowns, etc.), search immediately
+ reset()
+ searchComments(newFilters)
+ }
}
const clearFilters = () => {
- const newFilters = { query: '', sort_by: 'newest' as const, limit: 20, offset: 0 }
+ const newFilters = { query: '', sort_by: 'newest' as const, limit: 10, offset: 0 }
setFilters(newFilters)
- setSearchParams(new URLSearchParams())
+ updateURL(newFilters)
reset()
searchComments(newFilters)
}
@@ -188,27 +345,95 @@ const CommentSearch = () => {
const highlightText = (text: string, query?: string) => {
if (!query || !text) return text
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
- const parts = text.split(regex)
+ // Sanitize the query to prevent regex injection
+ const sanitizedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- return parts.map((part, index) =>
- regex.test(part) ? (
-
- {part}
-
- ) : part
- )
+ try {
+ const regex = new RegExp(`(${sanitizedQuery})`, 'gi')
+ const parts = text.split(regex)
+
+ return parts.map((part, index) =>
+ regex.test(part) ? (
+
+ {part}
+
+ ) : part
+ )
+ } catch (error) {
+ // If regex creation fails, return original text
+ console.warn('Invalid search query for highlighting:', query)
+ return text
+ }
+ }
+
+ const getStateAbbreviation = (stateName: string) => {
+ const stateAbbreviations: Record = {
+ 'Alabama': 'al',
+ 'Alaska': 'ak',
+ 'Arizona': 'az',
+ 'Arkansas': 'ar',
+ 'California': 'ca',
+ 'Colorado': 'co',
+ 'Connecticut': 'ct',
+ 'Delaware': 'de',
+ 'Florida': 'fl',
+ 'Georgia': 'ga',
+ 'Hawaii': 'hi',
+ 'Idaho': 'id',
+ 'Illinois': 'il',
+ 'Indiana': 'in',
+ 'Iowa': 'ia',
+ 'Kansas': 'ks',
+ 'Kentucky': 'ky',
+ 'Louisiana': 'la',
+ 'Maine': 'me',
+ 'Maryland': 'md',
+ 'Massachusetts': 'ma',
+ 'Michigan': 'mi',
+ 'Minnesota': 'mn',
+ 'Mississippi': 'ms',
+ 'Missouri': 'mo',
+ 'Montana': 'mt',
+ 'Nebraska': 'ne',
+ 'Nevada': 'nv',
+ 'New Hampshire': 'nh',
+ 'New Jersey': 'nj',
+ 'New Mexico': 'nm',
+ 'New York': 'ny',
+ 'North Carolina': 'nc',
+ 'North Dakota': 'nd',
+ 'Ohio': 'oh',
+ 'Oklahoma': 'ok',
+ 'Oregon': 'or',
+ 'Pennsylvania': 'pa',
+ 'Rhode Island': 'ri',
+ 'South Carolina': 'sc',
+ 'South Dakota': 'sd',
+ 'Tennessee': 'tn',
+ 'Texas': 'tx',
+ 'Utah': 'ut',
+ 'Vermont': 'vt',
+ 'Virginia': 'va',
+ 'Washington': 'wa',
+ 'West Virginia': 'wv',
+ 'Wisconsin': 'wi',
+ 'Wyoming': 'wy',
+ 'District of Columbia': 'dc',
+ 'Federal': 'us'
+ }
+ return stateAbbreviations[stateName] || 'us'
}
const hasActiveFilters = () => {
- return !!(filters.agency_name || filters.state || filters.tags?.length ||
- filters.date_from || filters.date_to || filters.commenter_type ||
- filters.position)
+ return !!(filters.agency_name || filters.state || filters.comment_filter ||
+ filters.filing_company || filters.comment_id || filters.docket_id ||
+ filters.tags?.length || filters.date_from || filters.date_to ||
+ filters.commenter_type || filters.position)
}
return (
@@ -216,9 +441,6 @@ const CommentSearch = () => {
- {/* Date From and To - Row 2 */}
+ {/* Comment Filter and Filing Company - Row 2 */}
+
+
+ handleFilterChange('comment_filter', e.target.value || undefined)}
+ className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder="Commenter name"
+ />
+
+
+
+
+ handleFilterChange('filing_company', e.target.value || undefined)}
+ className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder="Company name"
+ />
+
+
+ {/* Comment ID and Docket ID - Row 3 */}
+
+
+ handleFilterChange('comment_id', e.target.value || undefined)}
+ className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder="Comment ID"
+ />
+
+
+
+
+ handleFilterChange('docket_id', e.target.value || undefined)}
+ className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder="Docket ID"
+ />
+
+
+ {/* Date From and To - Row 4 */}