diff --git a/frontend/src/components/PopulatedNavBar.module.scss b/frontend/src/components/PopulatedNavBar.module.scss index d2577fa..c6a3daa 100644 --- a/frontend/src/components/PopulatedNavBar.module.scss +++ b/frontend/src/components/PopulatedNavBar.module.scss @@ -4,7 +4,7 @@ padding: 0 var(--spacing-lg); position: sticky; top: 0; - z-index: 100; + z-index: 1001; } .navContainer { diff --git a/frontend/src/components/SearchArticles.tsx b/frontend/src/components/SearchArticles.tsx index 704bea1..480fee6 100644 --- a/frontend/src/components/SearchArticles.tsx +++ b/frontend/src/components/SearchArticles.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Article, EvidenceType, ArticleStatus } from "../types/article"; import styles from "../styles/SearchPage.module.scss"; -// Enhanced search functionality with real-time suggestions +// Enhanced search functionality with real-time suggestions and search history const SearchArticles: React.FC = () => { const [keywords, setKeywords] = useState(""); @@ -19,6 +19,78 @@ const SearchArticles: React.FC = () => { const [sortField, setSortField] = useState("createdAt"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); + const [searchHistory, setSearchHistory] = useState([]); + const [showHistoryDropdown, setShowHistoryDropdown] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); + const searchInputRef = useRef(null); + const dropdownRef = useRef(null); + + // Load search history from localStorage on component mount + useEffect(() => { + const savedHistory = localStorage.getItem('searchHistory'); + if (savedHistory) { + try { + const history = JSON.parse(savedHistory); + if (Array.isArray(history)) { + setSearchHistory(history); + } + } catch (e) { + console.error('Error parsing search history from localStorage:', e); + } + } + }, []); + + // Save search history to localStorage whenever searchHistory changes + useEffect(() => { + localStorage.setItem('searchHistory', JSON.stringify(searchHistory)); + }, [searchHistory]); + + // Update dropdown position when it's shown or when the window is resized + useEffect(() => { + const updatePosition = () => { + if (showHistoryDropdown && searchInputRef.current) { + const inputRect = searchInputRef.current.getBoundingClientRect(); + // Calculate position relative to the offset parent instead of viewport + const offsetParent = searchInputRef.current.offsetParent as HTMLElement; + if (offsetParent) { + const parentRect = offsetParent.getBoundingClientRect(); + setDropdownPosition({ + top: inputRect.bottom - parentRect.top, // Position relative to parent + left: inputRect.left - parentRect.left, // Position relative to parent + width: inputRect.width, + }); + } else { + // Fallback to document coordinates if no offset parent + setDropdownPosition({ + top: inputRect.bottom + window.scrollY, + left: inputRect.left + window.scrollX, + width: inputRect.width, + }); + } + } + }; + + // Only update position when dropdown is shown + if (showHistoryDropdown) { + // Use setTimeout to ensure element is rendered before calculating position + setTimeout(updatePosition, 0); + } + + // Add event listeners for scroll and resize to update position + const handleScrollAndResize = () => { + if (showHistoryDropdown) { + updatePosition(); + } + }; + + window.addEventListener('scroll', handleScrollAndResize); + window.addEventListener('resize', handleScrollAndResize); + + return () => { + window.removeEventListener('scroll', handleScrollAndResize); + window.removeEventListener('resize', handleScrollAndResize); + }; + }, [showHistoryDropdown]); // Enhanced search function with debouncing useEffect(() => { @@ -37,6 +109,15 @@ const SearchArticles: React.FC = () => { setError(null); setSearchPerformed(true); + // Add to search history if the search term is not already in the history + if (keywords.trim()) { + setSearchHistory(prev => { + const newHistory = prev.filter(item => item.toLowerCase() !== keywords.trim().toLowerCase()); + // Limit history to 10 items + return [keywords.trim(), ...newHistory].slice(0, 10); + }); + } + try { const params = new URLSearchParams(); if (keywords.trim()) params.append("keywords", keywords.trim()); @@ -79,6 +160,20 @@ const SearchArticles: React.FC = () => { } }; + const handleHistoryItemClick = (searchTerm: string) => { + setKeywords(searchTerm); + setShowHistoryDropdown(false); + // Focus the search input after setting the value + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + }; + + const handleClearHistory = () => { + setSearchHistory([]); + localStorage.removeItem('searchHistory'); + }; + // Handle table sorting const handleSort = (field: keyof Article) => { if (sortField === field) { @@ -103,6 +198,22 @@ const SearchArticles: React.FC = () => { setSearchPerformed(false); }; + // Close the dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if (searchInputRef.current && !searchInputRef.current.contains(target) && + dropdownRef.current && !dropdownRef.current.contains(target)) { + setShowHistoryDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + return ( <> {/* Search Form Container */} @@ -112,16 +223,71 @@ const SearchArticles: React.FC = () => { {/* Main Search Bar */}
- setKeywords(e.target.value)} - onKeyPress={handleKeyPress} - className={styles.mainSearchInput} - placeholder="Enter keywords to search across titles, authors, and claims..." - aria-label="Search keywords" - /> + setKeywords(e.target.value)} + onKeyPress={handleKeyPress} + onFocus={() => setShowHistoryDropdown(true)} + onClick={() => setShowHistoryDropdown(true)} + className={styles.mainSearchInput} + placeholder="Enter keywords to search across titles, authors, and claims..." + aria-label="Search keywords" + autoComplete="off" + ref={searchInputRef} + /> + {/* This is where the dropdown would be rendered in the normal DOM */} + {/* The actual dropdown will be rendered in a portal-like fashion */} + {showHistoryDropdown && ( +
+
+ {searchHistory.length > 0 ? ( + <> +
+ Recent Searches + +
+
    + {searchHistory.map((item, index) => ( +
  • handleHistoryItemClick(item)} + > + 🕒 + {item} +
  • + ))} +
+ + ) : ( +
+

No recent searches

+
+ )} +
+
+ )} + {/* Search history dropdown positioned separately to avoid clipping */} + {showHistoryDropdown && ( +
+ {searchHistory.length > 0 ? ( + <> +
+ Recent Searches + +
+
    + {searchHistory.map((item, index) => ( +
  • handleHistoryItemClick(item)} + > + 🕒 + {item} +
  • + ))} +
+ + ) : ( +
+

No recent searches

+
+ )} +
+ )}
diff --git a/frontend/src/styles/SearchPage.module.scss b/frontend/src/styles/SearchPage.module.scss index aa70229..d514dbb 100644 --- a/frontend/src/styles/SearchPage.module.scss +++ b/frontend/src/styles/SearchPage.module.scss @@ -160,6 +160,7 @@ .searchBarContainer { display: flex; gap: 0.5rem; + position: relative; /* Make this the positioning context for dropdown */ } .mainSearchInput { @@ -177,6 +178,78 @@ } } +.searchHistoryDropdown { + position: fixed; /* Fixed position to escape parent containers and position absolutely */ + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + z-index: 1000; /* Higher z-index to appear above parent */ + max-height: 300px; + overflow-y: auto; +} + +.historyHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid #e5e7eb; + font-weight: 600; + color: #374151; + background-color: #f9fafb; + border-radius: 8px 8px 0 0; +} + +.noHistoryMessage { + padding: 1rem; + text-align: center; + color: #6b7280; + font-style: italic; +} + +.clearHistoryButton { + background: none; + border: none; + color: #3b82f6; + cursor: pointer; + font-size: 0.875rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + + &:hover { + background-color: #eff6ff; + } +} + +.historyList { + list-style: none; + margin: 0; + padding: 0; +} + +.historyItem { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background-color 0.15s; + + &:hover { + background-color: #f9fafb; + } + + &:last-child { + border-bottom: none; + } +} + +.historyIcon { + font-size: 1rem; +} + .mainSearchButton { display: inline-flex; align-items: center;