From 0dfdbca4707507b230dd2552de8c7b27a509d65b Mon Sep 17 00:00:00 2001 From: James Gonzalez Date: Sat, 20 Sep 2025 03:26:59 -0500 Subject: [PATCH 1/2] Add basic Agency tab for debugging Add more debug logs to observe results data structure Add Agency tab with basic result filtering - Filter existing search results by content_type === 'Agency' - Does not handle officer-to-agency searches yet Add agencies API integration Implement Agency tab with a dedicated agencies API endpoint - Add /agencies endpoint to apiRoutes - Add searchAgencies function to SearchProvider - Update Agency tab to call agencies API when selected - Encountered CORS error Fix Agency search results - Fixed agency endpoint - Return proper agency information - Adjust Agency info card rendering in search results Add useEffect for reactive agency search state and remove debug logs --- frontend/app/search/SearchResults.tsx | 169 +++++++++++++++++++------- frontend/providers/SearchProvider.tsx | 55 ++++++++- frontend/utils/api.ts | 47 +++++++ frontend/utils/apiRoutes.ts | 6 +- 4 files changed, 231 insertions(+), 46 deletions(-) diff --git a/frontend/app/search/SearchResults.tsx b/frontend/app/search/SearchResults.tsx index 86e1e98b9..6a8823021 100644 --- a/frontend/app/search/SearchResults.tsx +++ b/frontend/app/search/SearchResults.tsx @@ -1,7 +1,10 @@ "use client" import { Tab, Tabs, Box, CardHeader, Typography } from "@mui/material" import React, { useState } from "react" -import { SearchResponse } from "@/utils/api" +import { useSearchParams } from "next/navigation" +import { SearchResponse, AgencyResponse } from "@/utils/api" +import { useSearch } from "@/providers/SearchProvider" +import { useEffect } from "react" type SearchResultsProps = { total: number @@ -10,6 +13,34 @@ 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) + + useEffect(() => { + const performAgencySearch = async () => { + if (tab !== 3 || !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) + } + } + performAgencySearch() + }, [currentQuery, tab, searchAgencies]) const handleChange = (event: React.SyntheticEvent, newValue: number) => { setTab(newValue) @@ -35,47 +66,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 4953534cb..f2e035a62 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 9e6c7053e..44fc05cc5 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 4cdf2981f..10e4637c1 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 = From adcd2f59c661e2a22d7211612a88d27be2726948 Mon Sep 17 00:00:00 2001 From: James Gonzalez Date: Tue, 16 Dec 2025 21:38:05 -0600 Subject: [PATCH 2/2] Move performAgencySearch outside of useEffect - Better scalability for future tab functions - Also consolidated React imports --- frontend/app/search/SearchResults.tsx | 40 ++++++++++++++------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/app/search/SearchResults.tsx b/frontend/app/search/SearchResults.tsx index 6a8823021..92ce97671 100644 --- a/frontend/app/search/SearchResults.tsx +++ b/frontend/app/search/SearchResults.tsx @@ -1,10 +1,9 @@ "use client" import { Tab, Tabs, Box, CardHeader, Typography } from "@mui/material" -import React, { useState } from "react" +import React, { useState, useEffect } from "react" import { useSearchParams } from "next/navigation" import { SearchResponse, AgencyResponse } from "@/utils/api" import { useSearch } from "@/providers/SearchProvider" -import { useEffect } from "react" type SearchResultsProps = { total: number @@ -22,25 +21,28 @@ const SearchResults = ({ total, results }: SearchResultsProps) => { const [agencyLoading, setAgencyLoading] = useState(false) const [agencyTotal, setAgencyTotal] = useState(0) - useEffect(() => { - const performAgencySearch = async () => { - if (tab !== 3 || !currentQuery) return + 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) - } + 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) } - performAgencySearch() - }, [currentQuery, tab, searchAgencies]) + } + + useEffect(() => { + + if (tab === 3) performAgencySearch() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentQuery, tab]) const handleChange = (event: React.SyntheticEvent, newValue: number) => { setTab(newValue)