Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/PopulatedNavBar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
padding: 0 var(--spacing-lg);
position: sticky;
top: 0;
z-index: 100;
z-index: 1001;
}

.navContainer {
Expand Down
234 changes: 222 additions & 12 deletions frontend/src/components/SearchArticles.tsx
Original file line number Diff line number Diff line change
@@ -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("");
Expand All @@ -19,6 +19,78 @@ const SearchArticles: React.FC = () => {
const [sortField, setSortField] = useState<keyof Article>("createdAt");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [searchHistory, setSearchHistory] = useState<string[]>([]);
const [showHistoryDropdown, setShowHistoryDropdown] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const searchInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(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(() => {
Expand All @@ -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());
Expand Down Expand Up @@ -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) {
Expand All @@ -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 */}
Expand All @@ -112,16 +223,71 @@ const SearchArticles: React.FC = () => {
{/* Main Search Bar */}
<div className={styles.mainSearchSection}>
<div className={styles.searchBarContainer}>
<input
id="keywords"
type="text"
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
onKeyPress={handleKeyPress}
className={styles.mainSearchInput}
placeholder="Enter keywords to search across titles, authors, and claims..."
aria-label="Search keywords"
/>
<input
id="keywords"
type="text"
value={keywords}
onChange={(e) => 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 && (
<div
id="search-history-dropdown-portal"
style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
>
<div
className={styles.searchHistoryDropdown}
ref={dropdownRef}
style={{
top: `${dropdownPosition.top + 4}px`, // Add 4px gap
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
position: 'absolute',
pointerEvents: 'auto',
}}
>
{searchHistory.length > 0 ? (
<>
<div className={styles.historyHeader}>
<span>Recent Searches</span>
<button
className={styles.clearHistoryButton}
onClick={handleClearHistory}
aria-label="Clear search history"
>
Clear
</button>
</div>
<ul className={styles.historyList}>
{searchHistory.map((item, index) => (
<li
key={index}
className={styles.historyItem}
onClick={() => handleHistoryItemClick(item)}
>
<span className={styles.historyIcon}>🕒</span>
<span className={styles.historyText}>{item}</span>
</li>
))}
</ul>
</>
) : (
<div className={styles.noHistoryMessage}>
<p>No recent searches</p>
</div>
)}
</div>
</div>
)}
<button
onClick={handleSearch}
disabled={loading}
Expand All @@ -136,6 +302,50 @@ const SearchArticles: React.FC = () => {
"Search"
)}
</button>
{/* Search history dropdown positioned separately to avoid clipping */}
{showHistoryDropdown && (
<div
className={styles.searchHistoryDropdown}
ref={dropdownRef}
style={{
top: `${dropdownPosition.top + 4}px`, // Add 4px gap
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
position: 'absolute',
}}
>
{searchHistory.length > 0 ? (
<>
<div className={styles.historyHeader}>
<span>Recent Searches</span>
<button
className={styles.clearHistoryButton}
onClick={handleClearHistory}
aria-label="Clear search history"
>
Clear
</button>
</div>
<ul className={styles.historyList}>
{searchHistory.map((item, index) => (
<li
key={index}
className={styles.historyItem}
onClick={() => handleHistoryItemClick(item)}
>
<span className={styles.historyIcon}>🕒</span>
<span className={styles.historyText}>{item}</span>
</li>
))}
</ul>
</>
) : (
<div className={styles.noHistoryMessage}>
<p>No recent searches</p>
</div>
)}
</div>
)}
</div>
</div>

Expand Down
73 changes: 73 additions & 0 deletions frontend/src/styles/SearchPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
.searchBarContainer {
display: flex;
gap: 0.5rem;
position: relative; /* Make this the positioning context for dropdown */
}

.mainSearchInput {
Expand All @@ -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;
Expand Down