From 4330a746b18b520b72922dc4cbee2fe5dc382bf6 Mon Sep 17 00:00:00 2001 From: HexWarrior6 <3083512469@qq.com> Date: Fri, 14 Nov 2025 09:46:06 +0800 Subject: [PATCH 1/2] feat(search): add search history functionality and dropdown menu styling --- frontend/src/components/SearchArticles.tsx | 169 +++++++++++++++++++-- frontend/src/styles/SearchPage.module.scss | 73 +++++++++ 2 files changed, 230 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/SearchArticles.tsx b/frontend/src/components/SearchArticles.tsx index 704bea1..5066dc1 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,66 @@ 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(); + 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 +97,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 +148,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 +186,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 +211,62 @@ 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} + /> + {showHistoryDropdown && ( +
+ {searchHistory.length > 0 ? ( + <> +
+ Recent Searches + +
+
    + {searchHistory.map((item, index) => ( +
  • handleHistoryItemClick(item)} + > + 🕒 + {item} +
  • + ))} +
+ + ) : ( +
+

No recent searches

+
+ )} +
+ )} -
-
    - {searchHistory.map((item, index) => ( -
  • handleHistoryItemClick(item)} +
    + {searchHistory.length > 0 ? ( + <> +
    + Recent Searches +
  • - ))} -
- - ) : ( -
-

No recent searches

-
- )} + Clear + +
+
    + {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

+
+ )} +
+ )}