Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 130 additions & 43 deletions frontend/app/search/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client"
import { Tab, Tabs, Box, CardHeader, Typography } from "@mui/material"
import React, { useState } from "react"
import { SearchResponse } from "@/utils/api"
import React, { useState, useEffect } from "react"
import { useSearchParams } from "next/navigation"
import { SearchResponse, AgencyResponse } from "@/utils/api"
import { useSearch } from "@/providers/SearchProvider"

type SearchResultsProps = {
total: number
Expand All @@ -10,6 +12,37 @@ type SearchResultsProps = {

const SearchResults = ({ total, results }: SearchResultsProps) => {
const [tab, setTab] = useState(0)
const { loading, searchAgencies } = useSearch()

const searchParams = useSearchParams()
const currentQuery = searchParams.get('query') || ''

const [agencyResults, setAgencyResults] = useState<AgencyResponse[]>([])
const [agencyLoading, setAgencyLoading] = useState(false)
const [agencyTotal, setAgencyTotal] = useState(0)

const performAgencySearch = async () => {
if (!currentQuery) return

setAgencyLoading(true)
try {
const response = await searchAgencies({ name: currentQuery })
setAgencyResults(response.results || [])
setAgencyTotal(response.total || 0)
} catch (error) {
console.error('Agency search failed:', error)
setAgencyResults([])
setAgencyTotal(0)
} finally {
setAgencyLoading(false)
}
}

useEffect(() => {

if (tab === 3) performAgencySearch()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentQuery, tab])

const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setTab(newValue)
Expand All @@ -35,47 +68,101 @@ const SearchResults = ({ total, results }: SearchResultsProps) => {
<Tab key="litigation" label="Litigation" />
</Tabs>
</Box>
<Box sx={{ p: 3 }}>
<Typography sx={{ marginBottom: "1rem", fontWeight: "bold" }}>{total} results</Typography>
<CustomTabPanel value={tab} index={0}>
{results.map((result) => (
<CardHeader
key={result.uid}
title={result.title}
subheader={result.subtitle}
slotProps={{ subheader: { fontWeight: "bold", color: "#000" } }}
action={
<Box sx={{ display: "flex", gap: "1rem" }}>
<span style={{ fontSize: "12px", color: "#666" }}>{result.content_type}</span>
<span style={{ fontSize: "12px", color: "#666" }}>{result.source}</span>
<span style={{ fontSize: "12px", color: "#666" }}>{result.last_updated}</span>
</Box>
}
sx={{
flexDirection: "column",
alignItems: "flex-start",
gap: "0.5rem",
border: "1px solid #ddd",
borderBottom: "none",
":first-of-type": {
borderTopLeftRadius: "4px",
borderTopRightRadius: "4px"
},
":last-of-type": {
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
borderBottom: "1px solid #ddd"
},
"& .MuiCardHeader-content": {
overflow: "hidden"
},
paddingInline: "4.5rem",
paddingBlock: "2rem"
}}
/>
))}
</CustomTabPanel>
</Box>
{loading ? (
<Box sx={{ p: 3, textAlign: "center" }}>
<Typography>Loading...</Typography>
</Box>
) : (
<Box sx={{ p: 3 }}>
<Typography sx={{ marginBottom: "1rem", fontWeight: "bold" }}>{total} results</Typography>
<CustomTabPanel value={tab} index={0}>
{results.map((result) => (
<CardHeader
key={result.uid}
title={result.title}
subheader={result.subtitle}
slotProps={{ subheader: { fontWeight: "bold", color: "#000" } }}
action={
<Box sx={{ display: "flex", gap: "1rem" }}>
<span style={{ fontSize: "12px", color: "#666" }}>{result.content_type}</span>
<span style={{ fontSize: "12px", color: "#666" }}>{result.source}</span>
<span style={{ fontSize: "12px", color: "#666" }}>{result.last_updated}</span>
</Box>
}
sx={{
flexDirection: "column",
alignItems: "flex-start",
gap: "0.5rem",
border: "1px solid #ddd",
borderBottom: "none",
":first-of-type": {
borderTopLeftRadius: "4px",
borderTopRightRadius: "4px"
},
":last-of-type": {
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
borderBottom: "1px solid #ddd"
},
"& .MuiCardHeader-content": {
overflow: "hidden"
},
paddingInline: "4.5rem",
paddingBlock: "2rem"
}}
/>
))}
</CustomTabPanel>

<CustomTabPanel value={tab} index={3}>
{agencyLoading ? (
<Typography>Searching agencies...</Typography>
) : (
<>
<Typography sx={{ marginBottom: "1rem", fontWeight: "bold" }}>
{agencyTotal} agency results
</Typography>
{agencyResults.map((result) => (
<CardHeader
Comment on lines +117 to +126
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my other comment:
I'm wondering if we can move this into another component that can be customized according to what's being searched. That way we don't have all these hardcoded (by agency, but unit, etc) CustomTabPanels.

I don't know if you want to do this or I can make that change when I start my Unit Search task.

key={result.uid}
title={result.name}
subheader={`${result.hq_city || 'Unknown City'}, ${result.hq_state || 'Unknown State'}`}
slotProps={{ subheader: { fontWeight: "bold", color: "#000" } }}
action={
<Box sx={{ display: "flex", gap: "1rem" }}>
<span style={{ fontSize: "12px", color: "#666" }}>Agency</span>
{result.jurisdiction && <span style={{ fontSize: "12px", color: "#666" }}>{result.jurisdiction}</span>}
{result.website_url && <span style={{ fontSize: "12px", color: "#666" }}>{result.website_url}</span>}
</Box>
}
sx={{
flexDirection: "column",
alignItems: "flex-start",
gap: "0.5rem",
border: "1px solid #ddd",
borderBottom: "none",
":first-of-type": {
borderTopLeftRadius: "4px",
borderTopRightRadius: "4px"
},
":last-of-type": {
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
borderBottom: "1px solid #ddd"
},
"& .MuiCardHeader-content": {
overflow: "hidden"
},
paddingInline: "4.5rem",
paddingBlock: "2rem"
}}
/>
))}
</>
)}
</CustomTabPanel>
</Box>
)}
</>
)
}
Expand Down
55 changes: 52 additions & 3 deletions frontend/providers/SearchProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { createContext, useCallback, useContext, useMemo, useState, useEffect, useRef } from "react"
import { apiFetch } from "@/utils/apiFetch"
import { useAuth } from "@/providers/AuthProvider"
import { SearchRequest, SearchResponse, PaginatedSearchResponses } from "@/utils/api"
import { SearchRequest, SearchResponse, PaginatedSearchResponses, AgenciesRequest, AgenciesApiResponse } from "@/utils/api"
import API_ROUTES, { apiBaseUrl } from "@/utils/apiRoutes"
import { ApiError } from "@/utils/apiError"
import { useRouter, useSearchParams } from "next/navigation"
Expand All @@ -11,6 +11,9 @@ interface SearchContext {
searchAll: (
query: Omit<SearchRequest, "access_token" | "accessToken">
) => Promise<PaginatedSearchResponses>
searchAgencies: (
params: Omit<AgenciesRequest, "access_token" | "accessToken">
) => Promise<AgenciesApiResponse>
searchResults?: PaginatedSearchResponses
loading: boolean
error: string | null
Expand Down Expand Up @@ -213,8 +216,54 @@ function useHook(): SearchContext {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])

const searchAgencies = useCallback(
async (params: Omit<AgenciesRequest, "access_token" | "accessToken">) => {
if (!accessToken) throw new ApiError("No access token", "NO_ACCESS_TOKEN", 401)
setLoading(true)

try {
const queryParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.set(key, String(value))
}
})

const apiUrl = `${apiBaseUrl}${API_ROUTES.agencies}?${queryParams.toString()}`

const response = await apiFetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
})

if (!response.ok) {
throw new Error("Failed to search agencies")
}

const data: AgenciesApiResponse = await response.json()
return data
} catch (error) {
console.error("Error searching agencies:", error)
return {
error: String(error),
results: [],
page: 0,
per_page: 0,
pages: 0,
total: 0
}
} finally {
setLoading(false)
}
},
[accessToken]
)

return useMemo(
() => ({ searchAll, searchResults, loading, error, setPage }),
[searchResults, searchAll, loading, error, setPage]
() => ({ searchAll, searchResults, loading, error, setPage, searchAgencies }),
[searchResults, searchAll, loading, error, setPage, searchAgencies]
)

}
47 changes: 47 additions & 0 deletions frontend/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,50 @@ export type UpdateUserProfilePayload = {
}
primary_email?: string
}
export interface AgenciesRequest extends AuthenticatedRequest {
name?: string
city?: string
state?: string
zip_code?: string
jurisdiction?: string
page?: number
per_page?: number
}

export interface AgencyResponse {
uid: string
name: string
website_url?: string | null
hq_address?: string | null
hq_city?: string | null
hq_state?: string | null
hq_zip?: string | null
phone?: string | null
email?: string | null
description?: string | null
jurisdiction?: string | null
units?: Array<{
uid: string
name: string
website_url?: string | null
phone?: string | null
email?: string | null
description?: string | null
address?: string | null
city?: string | null
state?: string | null
zip?: string | null
agency_url?: string | null
officers_url?: string | null
date_established?: string | null
}>
}

export type AgenciesApiResponse = {
results: AgencyResponse[]
page: number
per_page: number
pages: number
total: number
error?: string
}
6 changes: 5 additions & 1 deletion frontend/utils/apiRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ const API_ROUTES = {
all: "/search/",
incidents: "/incidents/search"
},

users: {
self: "/users/self"
}
},

agencies: "/agencies/"

}

export const apiBaseUrl: string =
Expand Down