diff --git a/frontend/app/search/SearchResults.tsx b/frontend/app/search/SearchResults.tsx index 86e1e98b..92ce9767 100644 --- a/frontend/app/search/SearchResults.tsx +++ b/frontend/app/search/SearchResults.tsx @@ -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 @@ -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([]) + 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) @@ -35,47 +68,101 @@ const SearchResults = ({ total, results }: SearchResultsProps) => { - - {total} results - - {results.map((result) => ( - - {result.content_type} - {result.source} - {result.last_updated} - - } - 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" - }} - /> - ))} - - + {loading ? ( + + Loading... + + ) : ( + + {total} results + + {results.map((result) => ( + + {result.content_type} + {result.source} + {result.last_updated} + + } + 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" + }} + /> + ))} + + + + {agencyLoading ? ( + Searching agencies... + ) : ( + <> + + {agencyTotal} agency results + + {agencyResults.map((result) => ( + + Agency + {result.jurisdiction && {result.jurisdiction}} + {result.website_url && {result.website_url}} + + } + 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" + }} + /> + ))} + + )} + + + )} ) } diff --git a/frontend/providers/SearchProvider.tsx b/frontend/providers/SearchProvider.tsx index 4953534c..f2e035a6 100644 --- a/frontend/providers/SearchProvider.tsx +++ b/frontend/providers/SearchProvider.tsx @@ -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" @@ -11,6 +11,9 @@ interface SearchContext { searchAll: ( query: Omit ) => Promise + searchAgencies: ( + params: Omit + ) => Promise searchResults?: PaginatedSearchResponses loading: boolean error: string | null @@ -213,8 +216,54 @@ function useHook(): SearchContext { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]) + const searchAgencies = useCallback( + async (params: Omit) => { + 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] ) + } diff --git a/frontend/utils/api.ts b/frontend/utils/api.ts index 9e6c7053..44fc05cc 100644 --- a/frontend/utils/api.ts +++ b/frontend/utils/api.ts @@ -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 +} diff --git a/frontend/utils/apiRoutes.ts b/frontend/utils/apiRoutes.ts index 4cdf2981..10e4637c 100644 --- a/frontend/utils/apiRoutes.ts +++ b/frontend/utils/apiRoutes.ts @@ -10,9 +10,13 @@ const API_ROUTES = { all: "/search/", incidents: "/incidents/search" }, + users: { self: "/users/self" - } + }, + + agencies: "/agencies/" + } export const apiBaseUrl: string =