From e9c418e80c54773c9530de288df79ae4d5fa12a0 Mon Sep 17 00:00:00 2001 From: Kamalesh Mohanasundar <94128306+Kammohan@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:58:39 -0600 Subject: [PATCH 1/2] feat(BR-155): Add search functionality to data feed - Backend: Add search query parameter to /weekly-crises endpoint - Supports search by crisis name, location, and disaster type - Case-insensitive search with SQL ILIKE - Frontend: Add search input with 500ms debounce - Clean search UI with icon in data feed header - Auto-resets to page 1 on search - Smart empty state for no search results - API Client: Updated getWeeklyCrises() to accept search param - Uses URLSearchParams for clean query building --- client/app/dashboard/data-feed/page.tsx | 76 +++++++++++++++++++------ client/lib/api-client.ts | 14 ++++- server/routers/data_feed.py | 19 ++++++- 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/client/app/dashboard/data-feed/page.tsx b/client/app/dashboard/data-feed/page.tsx index 0713cfa..beefe3b 100644 --- a/client/app/dashboard/data-feed/page.tsx +++ b/client/app/dashboard/data-feed/page.tsx @@ -1,10 +1,11 @@ "use client" import { useEffect, useState } from "react" -import { ArrowUpRight } from "lucide-react" +import { ArrowUpRight, Search } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" +import { Input } from "@/components/ui/input" import { SentimentBadge } from "@/components/sentiment-badge" import { BlueskyIcon } from "@/components/bluesky-icon" import { Lordicon } from "@/components/lordicon" @@ -76,6 +77,8 @@ export default function DataFeedPage() { const [loading, setLoading] = useState(true) const [loadingCrises, setLoadingCrises] = useState(false) const [lastUpdated, setLastUpdated] = useState(null) + const [searchQuery, setSearchQuery] = useState("") + const [debouncedSearch, setDebouncedSearch] = useState("") useEffect(() => { async function fetchInitialData() { @@ -97,11 +100,21 @@ export default function DataFeedPage() { fetchInitialData() }, []) + // Debounce search input + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchQuery) + setCurrentPage(1) // Reset to first page on search + }, 500) + + return () => clearTimeout(timer) + }, [searchQuery]) + useEffect(() => { async function fetchCrises() { try { setLoadingCrises(true) - const crisesData = await getWeeklyCrises(7, currentPage, 10) + const crisesData = await getWeeklyCrises(7, currentPage, 10, debouncedSearch) setWeeklyCrises(crisesData.crises) setPagination(crisesData.pagination) setLastUpdated(new Date()) @@ -112,7 +125,7 @@ export default function DataFeedPage() { } } fetchCrises() - }, [currentPage]) + }, [currentPage, debouncedSearch]) const formatDateTime = (dateString: string) => { const date = new Date(dateString) @@ -429,15 +442,29 @@ export default function DataFeedPage() { {/* Crisis Feed */} - - Recent Crisis Events - - {pagination?.total_count || 0} total - - -

- Events detected in the past 7 days from Bluesky posts -

+
+
+ + Recent Crisis Events + + {pagination?.total_count || 0} total + + +

+ Events detected in the past 7 days from Bluesky posts +

+
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+
{loadingCrises ? ( @@ -552,12 +579,25 @@ export default function DataFeedPage() { colorize="currentColor" /> -

- No crises detected in the past week. This is good news! -

-

- Try adjusting your time range or filters -

+ {debouncedSearch ? ( + <> +

+ No results found for "{debouncedSearch}" +

+

+ Try a different search term or clear your search +

+ + ) : ( + <> +

+ No crises detected in the past week. This is good news! +

+

+ Try adjusting your time range or filters +

+ + )} )} diff --git a/client/lib/api-client.ts b/client/lib/api-client.ts index 7cbe4ea..f1b97ae 100644 --- a/client/lib/api-client.ts +++ b/client/lib/api-client.ts @@ -92,7 +92,17 @@ export async function getDataFeedOverview() { }>('/api/data-feed/overview'); } -export async function getWeeklyCrises(days = 7, page = 1, pageSize = 10) { +export async function getWeeklyCrises(days = 7, page = 1, pageSize = 10, search = "") { + const params = new URLSearchParams({ + days: days.toString(), + page: page.toString(), + page_size: pageSize.toString(), + }); + + if (search && search.trim()) { + params.append('search', search.trim()); + } + return apiGet<{ crises: Array<{ id: number; @@ -116,7 +126,7 @@ export async function getWeeklyCrises(days = 7, page = 1, pageSize = 10) { has_next: boolean; has_prev: boolean; } - }>(`/api/data-feed/weekly-crises?days=${days}&page=${page}&page_size=${pageSize}`); + }>(`/api/data-feed/weekly-crises?${params.toString()}`); } // Analysis API functions diff --git a/server/routers/data_feed.py b/server/routers/data_feed.py index d8e158f..1e3fac0 100644 --- a/server/routers/data_feed.py +++ b/server/routers/data_feed.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session -from sqlalchemy import func, desc, and_ +from sqlalchemy import func, desc, and_, or_ from datetime import datetime, timedelta from typing import Optional from db_utils.db import SessionLocal, DataFeed, Post, Disaster, CollectionRun @@ -82,18 +82,31 @@ def get_weekly_crises( days: int = Query(default=7, ge=1, le=90), page: int = Query(default=1, ge=1), page_size: int = Query(default=10, ge=1, le=100), + search: str = Query(default="", description="Search crises by name, location, or disaster type"), db: Session = Depends(get_db), ): - """Get recent crisis events with pagination (default last 7 days)""" + """Get recent crisis events with pagination and search (default last 7 days)""" cutoff_date = datetime.utcnow() - timedelta(days=days) query = ( db.query(Disaster) .join(Post, Disaster.post_id == Post.id, isouter=True) .filter(Disaster.extracted_at >= cutoff_date) - .order_by(desc(Disaster.extracted_at)) ) + # Apply search filter if provided + if search and search.strip(): + search_term = f"%{search.strip()}%" + query = query.filter( + or_( + Disaster.description.ilike(search_term), + Disaster.location_name.ilike(search_term), + Post.disaster_type.ilike(search_term) + ) + ) + + query = query.order_by(desc(Disaster.extracted_at)) + total_count = query.count() total_pages = (total_count + page_size - 1) // page_size From 3f965a81b83197bdfb9947afe28d609451b85f5d Mon Sep 17 00:00:00 2001 From: Kamalesh Mohanasundar <94128306+Kammohan@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:30:57 -0600 Subject: [PATCH 2/2] fix: resolve merge conflicts from main branch - Merged lucide-react imports (Search + Info, X) - Added showDisclaimer state from main - Kept BR-155 search functionality intact --- client/app/dashboard/data-feed/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/app/dashboard/data-feed/page.tsx b/client/app/dashboard/data-feed/page.tsx index beefe3b..210bc13 100644 --- a/client/app/dashboard/data-feed/page.tsx +++ b/client/app/dashboard/data-feed/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { ArrowUpRight, Search } from "lucide-react" +import { ArrowUpRight, Search, Info, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" @@ -79,6 +79,7 @@ export default function DataFeedPage() { const [lastUpdated, setLastUpdated] = useState(null) const [searchQuery, setSearchQuery] = useState("") const [debouncedSearch, setDebouncedSearch] = useState("") + const [showDisclaimer, setShowDisclaimer] = useState(true) useEffect(() => { async function fetchInitialData() {