learnix / src /app /works /WorkSearchInterface.js
shashidharak99's picture
Upload files
7d51e81 verified
'use client';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { FiSearch, FiDownload, FiEye, FiChevronDown, FiCalendar, FiBook, FiShare2, FiMoreVertical, FiExternalLink, FiFilter, FiCheck } from 'react-icons/fi';
import { FaBookmark, FaRegBookmark } from 'react-icons/fa';
import SubjectTopicFilter from './SubjectTopicFilter';
import Ads from '../components/ads/Ads';
import './styles/WorkSearchInterface.css';
// LocalStorage key for saved topics (same as in /works/[id])
const SAVED_TOPICS_KEY = 'learnix_saved_topics';
// Helper functions for localStorage
const getSavedTopics = () => {
if (typeof window === 'undefined') return [];
try {
const saved = localStorage.getItem(SAVED_TOPICS_KEY);
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
};
const isTopicSaved = (topicId) => {
const savedTopics = getSavedTopics();
return savedTopics.some(t => t.topic._id === topicId);
};
const saveTopic = (topic) => {
if (typeof window === 'undefined' || !topic) return false;
try {
const savedTopics = getSavedTopics();
const existingIndex = savedTopics.findIndex(t => t.topic._id === topic.topicId);
const topicToSave = {
topic: {
_id: topic.topicId,
topic: topic.topic,
content: topic.content,
images: topic.images,
timestamp: topic.timestamp,
},
subject: {
subject: topic.subjectName,
},
user: {
name: topic.userName,
usn: topic.usn,
profileimg: topic.profileimg || '',
},
savedAt: new Date().toISOString(),
};
if (existingIndex !== -1) {
savedTopics[existingIndex] = topicToSave;
} else {
savedTopics.push(topicToSave);
}
localStorage.setItem(SAVED_TOPICS_KEY, JSON.stringify(savedTopics));
return true;
} catch (err) {
console.error('Error saving topic:', err);
return false;
}
};
const removeSavedTopic = (topicId) => {
if (typeof window === 'undefined') return false;
try {
const savedTopics = getSavedTopics();
const filtered = savedTopics.filter(t => t.topic._id !== topicId);
localStorage.setItem(SAVED_TOPICS_KEY, JSON.stringify(filtered));
return true;
} catch (err) {
console.error('Error removing saved topic:', err);
return false;
}
};
const WorkSearchInterface = () => {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
// Restore state from URL params so refresh keeps the search/filter state
const initialQuery = searchParams.get('q') || '';
const initialSubjects = searchParams.get('subjects')
? searchParams.get('subjects').split(',').filter(Boolean)
: [];
const initialTopics = searchParams.get('topics')
? searchParams.get('topics').split(',').filter(Boolean)
: [];
const [searchQuery, setSearchQuery] = useState(initialQuery);
const [searchResults, setSearchResults] = useState([]);
const [displayedTopics, setDisplayedTopics] = useState([]);
const [allTopics, setAllTopics] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [expandedImages, setExpandedImages] = useState({});
const [currentIndex, setCurrentIndex] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [selectedSubjects, setSelectedSubjects] = useState(initialSubjects);
const [selectedTopics, setSelectedTopics] = useState(initialTopics);
const [savedTopicIds, setSavedTopicIds] = useState([]);
const [cachedSavedTopics, setCachedSavedTopics] = useState([]);
const [openMenuId, setOpenMenuId] = useState(null);
const [showFilterPopup, setShowFilterPopup] = useState(false);
const [sortOrder, setSortOrder] = useState('latest'); // 'latest' or 'oldest'
const [showSavedOnly, setShowSavedOnly] = useState(false);
const menuRef = useRef(null);
const filterRef = useRef(null);
const ITEMS_PER_LOAD = 8;
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [searchPage, setSearchPage] = useState(1);
const [searchTotalPages, setSearchTotalPages] = useState(1);
const [searchTotal, setSearchTotal] = useState(0); // real total count from API
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setOpenMenuId(null);
}
if (filterRef.current && !filterRef.current.contains(event.target)) {
setShowFilterPopup(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Load saved topics from localStorage immediately on mount (before server data)
useEffect(() => {
const saved = getSavedTopics();
setSavedTopicIds(saved.map(t => t.topic._id));
// Convert saved topics to display format for immediate rendering
const savedForDisplay = saved.map(s => ({
...s.topic,
topic: s.topic.topic,
content: s.topic.content || '',
images: s.topic.images || [],
timestamp: s.topic.timestamp,
topicId: s.topic._id,
userName: s.user?.name || 'Unknown',
usn: s.user?.usn || '',
profileimg: s.user?.profileimg || '',
subjectName: s.subject?.subject || 'Unknown Subject',
userId: s.user?._id || '',
isCached: true, // Mark as cached for visual indicator
})).sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
setCachedSavedTopics(savedForDisplay);
}, []);
useEffect(() => {
if (initialQuery || initialSubjects.length > 0 || initialTopics.length > 0) {
// Restore search from URL params
handleSearch(initialQuery, 1, initialSubjects, initialTopics);
} else {
fetchPagedTopics(1, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchPagedTopics = async (pageToFetch = 1, reset = false) => {
// Only show the full-page skeleton when doing an initial/reset load.
if (reset) setIsLoading(true);
try {
// Use oldest API when sortOrder is 'oldest', otherwise use default (latest first)
const apiEndpoint = sortOrder === 'oldest' ? '/api/work/oldest/paged' : '/api/work/paged';
const response = await fetch(`${apiEndpoint}?page=${pageToFetch}&pageSize=${ITEMS_PER_LOAD}`);
const data = await response.json();
if (data && Array.isArray(data.topics)) {
const newTopics = data.topics.map(topic => ({
...topic,
topicId: topic._id,
subjectName: topic.subject,
userName: topic.userName,
usn: topic.usn,
profileimg: topic.profileimg,
userId: topic.userId,
}));
if (reset) {
setAllTopics(newTopics);
setDisplayedTopics(newTopics);
setCurrentIndex(newTopics.length);
} else {
setAllTopics(prev => [...prev, ...newTopics]);
setDisplayedTopics(prev => [...prev, ...newTopics]);
setCurrentIndex(prev => prev + newTopics.length);
}
setPage(data.page);
setTotalPages(data.totalPages);
setHasMore(data.page < data.totalPages);
}
} catch (error) {
console.error('Error fetching paged topics:', error);
} finally {
if (reset) setIsLoading(false);
}
};
const handleSearch = async (query, pageNum = 1, subjectsOverride, topicsOverride) => {
// Use overrides (from URL restore on mount) or current state
const activeSubjects = subjectsOverride !== undefined ? subjectsOverride : selectedSubjects;
const activeTopics = topicsOverride !== undefined ? topicsOverride : selectedTopics;
if (!query.trim() && activeSubjects.length === 0 && activeTopics.length === 0) {
setSearchResults([]);
setSearchPage(1);
setSearchTotalPages(1);
setSearchTotal(0);
const filteredTopics = getFilteredTopics(allTopics);
const firstBatch = filteredTopics.slice(0, ITEMS_PER_LOAD);
setDisplayedTopics(firstBatch);
setCurrentIndex(ITEMS_PER_LOAD);
setHasMore(filteredTopics.length > ITEMS_PER_LOAD);
window.history.replaceState({}, '', pathname);
return;
}
// Page 1 = full skeleton; page > 1 = only button loading state (no skeleton)
if (pageNum === 1) setIsLoading(true);
else setIsLoadingMore(true);
try {
// Build URL with query, subjects, and topics params
const urlParams = new globalThis.URLSearchParams();
if (query.trim()) urlParams.set('q', query);
if (activeSubjects.length > 0) urlParams.set('subjects', activeSubjects.join(','));
if (activeTopics.length > 0) urlParams.set('topics', activeTopics.join(','));
urlParams.set('page', pageNum.toString());
urlParams.set('pageSize', ITEMS_PER_LOAD.toString());
const browserUrl = `${pathname}?${urlParams.toString()}`;
window.history.replaceState({}, '', browserUrl);
// Use oldest API when sortOrder is 'oldest', otherwise use default (latest first)
const apiEndpoint = sortOrder === 'oldest' ? '/api/work/search-oldest' : '/api/work/search';
const response = await fetch(`${apiEndpoint}?${urlParams.toString()}`);
const data = await response.json();
if (data && Array.isArray(data.topics)) {
const newTopics = data.topics.map(topic => ({
...topic,
topicId: topic._id,
subjectName: topic.subject,
userName: topic.userName,
usn: topic.usn,
profileimg: topic.profileimg,
userId: topic.userId,
}));
if (pageNum === 1) {
setSearchResults(newTopics);
setDisplayedTopics(newTopics);
setSearchTotal(data.totalResults ?? data.total ?? newTopics.length);
} else {
// Append for subsequent pages (View More)
setSearchResults(prev => [...prev, ...newTopics]);
setDisplayedTopics(prev => [...prev, ...newTopics]);
// keep searchTotal unchanged — it was set on page 1
}
setSearchPage(data.page);
setSearchTotalPages(data.totalPages);
setHasMore(data.page < data.totalPages);
}
} catch (error) {
console.error('Error searching:', error);
} finally {
if (pageNum === 1) setIsLoading(false);
else setIsLoadingMore(false);
}
};
const loadMoreTopics = useCallback(() => {
if (isLoadingMore || !hasMore) return;
fetchPagedTopics(page + 1);
}, [hasMore, isLoadingMore, page]);
const getFilteredTopics = (topics) => {
let filtered = topics.filter(topic => {
// Filter by saved if showSavedOnly is enabled
if (showSavedOnly) {
const topicId = topic.topicId || topic._id;
if (!savedTopicIds.includes(topicId)) {
return false;
}
}
// If no subjects selected, show all
if (selectedSubjects.length === 0) {
return true;
}
// OR logic for subjects - if topic matches any selected subject
const subjectMatch = selectedSubjects.includes(topic.subjectName);
if (!subjectMatch) {
return false;
}
// OR logic for topics - if no topics selected, show all from matched subjects
if (selectedTopics.length === 0) {
return true;
}
// If topics are selected, topic must match any selected topic
return selectedTopics.includes(topic.topic);
});
// Apply sorting based on sortOrder
filtered = filtered.sort((a, b) => {
if (sortOrder === 'oldest') {
return new Date(a.timestamp) - new Date(b.timestamp);
}
return new Date(b.timestamp) - new Date(a.timestamp); // latest by default
});
return filtered;
};
const handleFilterChange = (filters) => {
const newSubjects = filters.subjects || [];
const newTopics = filters.topics || [];
setSelectedSubjects(newSubjects);
setSelectedTopics(newTopics);
// Immediately sync the URL so deselecting a chip removes it from the address bar
const urlParams = new globalThis.URLSearchParams();
if (searchQuery.trim()) urlParams.set('q', searchQuery);
if (newSubjects.length > 0) urlParams.set('subjects', newSubjects.join(','));
if (newTopics.length > 0) urlParams.set('topics', newTopics.join(','));
const newUrl = urlParams.toString() ? `${pathname}?${urlParams.toString()}` : pathname;
window.history.replaceState({}, '', newUrl);
};
// Apply filters when they change (subject/topic filters trigger search immediately)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
// If subject or topic filters are applied, trigger search with current query
if (selectedSubjects.length > 0 || selectedTopics.length > 0) {
handleSearch(searchQuery, 1);
} else if (showSavedOnly) {
// Show saved only - client-side filter
const filteredTopics = getFilteredTopics(allTopics);
const initialTopics = filteredTopics.slice(0, ITEMS_PER_LOAD);
setDisplayedTopics(initialTopics);
setCurrentIndex(ITEMS_PER_LOAD);
setHasMore(filteredTopics.length > ITEMS_PER_LOAD);
} else if (!searchQuery) {
// No filters and no search query - fetch fresh from backend based on sortOrder
// Reset to page 1 and fetch from appropriate API (latest or oldest)
setPage(1);
fetchPagedTopics(1, true);
}
}, [selectedSubjects, selectedTopics, sortOrder, showSavedOnly, savedTopicIds]);
const toggleImageExpansion = (topicKey) => {
setExpandedImages(prev => ({ ...prev, [topicKey]: !prev[topicKey] }));
};
const downloadTopicAsPDF = async (topic, index) => {
if (!topic.images || topic.images.length === 0) {
alert('No images available for this topic');
return;
}
try {
// Show loading state
const downloadBtn = document.querySelector(`[data-topic-index="${index}"] .ws-download-btn`);
if (downloadBtn) {
downloadBtn.disabled = true;
downloadBtn.innerHTML = '<span>Generating...</span>';
}
// Call the API to generate PDF with template
const response = await fetch('/api/work/download-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topicId: topic._id,
user: {
name: topic.userName,
usn: topic.usn,
profileimg: topic.userProfileImg || '',
},
subject: {
subject: topic.subjectName,
},
topic: {
topic: topic.topic,
timestamp: topic.timestamp,
images: topic.images,
content: topic.content || '',
},
}),
});
if (!response.ok) {
throw new Error('Failed to generate PDF');
}
// Get the PDF blob
const blob = await response.blob();
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${topic.topic}_${topic.subjectName}_${topic.userName}`.replace(/[^a-zA-Z0-9]/g, '_') + '.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
// Restore button state
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<span>Download</span>';
}
} catch (error) {
console.error('Error generating PDF:', error);
alert('Error generating PDF. Please try again.');
// Restore button state
const downloadBtn = document.querySelector(`[data-topic-index="${index}"] .ws-download-btn`);
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<span>Download</span>';
}
}
};
const handleShare = (topic) => {
const url = `${window.location.origin}/works/${topic.topicId}`;
const title = `${topic.topic} - ${topic.subjectName}`;
const text = `Check out "${topic.topic}" uploaded by ${topic.userName} on Learnix`;
if (navigator.share) {
navigator.share({ title, text, url }).catch(err => console.log(err));
} else {
navigator.clipboard.writeText(`${text}\n${url}`)
.then(() => alert('Link copied to clipboard!'))
.catch(() => alert('Failed to copy link'));
}
};
const handleSaveToggle = (topic) => {
const topicId = topic.topicId || topic._id;
const isSaved = savedTopicIds.includes(topicId);
if (isSaved) {
removeSavedTopic(topicId);
setSavedTopicIds(prev => prev.filter(id => id !== topicId));
} else {
saveTopic(topic);
setSavedTopicIds(prev => [...prev, topicId]);
}
};
// Ad component wrapper
const AdSlot = ({ index }) => (
<div className="ws-ad-slot" key={`ad-${index}`}>
<Ads />
</div>
);
// Render topics with ads after every 2 topics
const renderTopicsWithAds = (topics) => {
const elements = [];
topics.forEach((topic, index) => {
elements.push(renderTopicCard(topic, index));
// After every 2 topics, insert an ad (after index 1, 3, 5, etc.)
if ((index + 1) % 2 === 0) {
elements.push(<AdSlot key={`ad-${index}`} index={index} />);
}
});
return elements;
};
const SkeletonLoader = () => (
<div className="ws-skeleton-grid">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="ws-skeleton-card">
<div className="ws-skeleton-header">
<div className="ws-skeleton-info">
<div className="ws-skeleton-title"></div>
<div className="ws-skeleton-subtitle"></div>
</div>
<div className="ws-skeleton-actions">
<div className="ws-skeleton-btn"></div>
<div className="ws-skeleton-btn"></div>
</div>
</div>
<div className="ws-skeleton-meta">
<div className="ws-skeleton-tag"></div>
<div className="ws-skeleton-tag"></div>
</div>
<div className="ws-skeleton-content"></div>
<div className="ws-skeleton-images">
<div className="ws-skeleton-image"></div>
<div className="ws-skeleton-image"></div>
</div>
</div>
))}
</div>
);
const renderTopicCard = (topic, index) => {
const topicKey = `${topic.userId}-${topic.subjectName}-${topic.topic}`;
const isExpanded = expandedImages[topicKey];
const hasImages = topic.images && topic.images.filter(img => img && img.trim() !== '').length > 0;
const validImages = hasImages ? topic.images.filter(img => img && img.trim() !== '') : [];
const displayImages = isExpanded ? validImages : validImages.slice(0, 2);
const topicId = topic.topicId || topic._id;
const isSaved = savedTopicIds.includes(topicId);
const isMenuOpen = openMenuId === topicId;
const handleMenuToggle = (e) => {
e.stopPropagation();
setOpenMenuId(isMenuOpen ? null : topicId);
};
const handleOpenClick = () => {
setOpenMenuId(null);
router.push(`/works/${topic.topicId}`);
};
const handleShareClick = () => {
setOpenMenuId(null);
handleShare(topic);
};
const handleSaveClick = () => {
setOpenMenuId(null);
handleSaveToggle(topic);
};
return (
<div key={`${topicKey}-${index}`} className={`ws-topic-card ${isSaved ? 'ws-saved-card' : ''}`} data-topic-index={index}>
<div className="ws-card-header">
<div className="ws-topic-info">
<Link href={`/works/${topic.topicId}`} className="ws-topic-link">
<h3 className="ws-topic-title">{topic.topic}</h3>
</Link>
<Link href={`/search/${topic.usn.toLowerCase()}`} className="ws-user-link">
<p className="ws-user-name">{topic.userName} ({topic.usn})</p>
</Link>
<div className="ws-topic-meta">
<span className="ws-meta-item">
<FiBook className="ws-meta-icon" />
{topic.subjectName}
</span>
<span className="ws-meta-item">
<FiCalendar className="ws-meta-icon" />
{new Date(topic.timestamp).toLocaleDateString('en-GB')}
</span>
</div>
</div>
<div className="ws-card-actions">
<button
onClick={() => downloadTopicAsPDF(topic, index)}
className="ws-action-btn ws-download-btn"
disabled={!hasImages}
title="Download as PDF"
>
<FiDownload />
</button>
<div className="ws-menu-container" ref={isMenuOpen ? menuRef : null}>
<button
onClick={handleMenuToggle}
className="ws-action-btn ws-more-btn"
title="More options"
>
<FiMoreVertical />
</button>
{isMenuOpen && (
<div className="ws-dropdown-menu">
<button className="ws-menu-item" onClick={handleOpenClick}>
<FiExternalLink className="ws-menu-icon" />
<span>Open</span>
</button>
<button className="ws-menu-item" onClick={handleShareClick}>
<FiShare2 className="ws-menu-icon" />
<span>Share</span>
</button>
<button className="ws-menu-item" onClick={handleSaveClick}>
{isSaved ? <FaBookmark className="ws-menu-icon" /> : <FaRegBookmark className="ws-menu-icon" />}
<span>{isSaved ? 'Unsave' : 'Save'}</span>
</button>
</div>
)}
</div>
</div>
</div>
{topic.content && <p className="ws-topic-content">{topic.content}</p>}
{hasImages && (
<div className="ws-images-section">
<div className="ws-images-grid">
{displayImages.map((imageUrl, imgIndex) => (
<Link key={imgIndex} href={`/works/${topic.topicId}`} className="ws-image-link">
<div className="ws-image-container">
<div className="ws-image-wrapper">
<img
src={imageUrl}
alt={`${topic.topic} - Image ${imgIndex + 1}`}
className="ws-topic-image"
loading="lazy"
/>
</div>
</div>
</Link>
))}
</div>
{validImages.length > 2 && (
<button onClick={() => toggleImageExpansion(topicKey)} className="ws-view-more-btn">
<FiEye />
{isExpanded ? 'Show Less' : `View More (${validImages.length - 2} more)`}
<FiChevronDown className={`ws-chevron ${isExpanded ? 'ws-rotated' : ''}`} />
</button>
)}
</div>
)}
</div>
);
};
return (
<div className="ws-container">
<div className="ws-header">
<div className="ws-search-container">
<div className="ws-search-box">
<input
type="text"
placeholder="Search by name, USN, subject, or topic..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
handleSearch(e.target.value, 1);
}}
className="ws-search-input"
/>
<button
type="button"
className="ws-search-btn"
onClick={() => handleSearch(searchQuery, 1)}
aria-label="Search"
>
<FiSearch className="ws-search-icon" />
</button>
</div>
</div>
<SubjectTopicFilter
onFilterChange={handleFilterChange}
initialSubjects={initialSubjects}
initialTopics={initialTopics}
/>
</div>
<div className="ws-content">
{/* Show cached saved topics immediately while loading */}
{isLoading && cachedSavedTopics.length > 0 && (
<div className="ws-saved-section">
<h2 className="ws-section-title ws-saved-title">
<FaBookmark className="ws-saved-icon" />
Your Saved Topics
</h2>
<div className="ws-topics-grid">
{cachedSavedTopics.map((topic, index) => renderTopicCard(topic, index))}
</div>
</div>
)}
{/* Show skeleton loader below cached topics or alone if no cached */}
{isLoading && <SkeletonLoader />}
{!searchQuery && selectedSubjects.length === 0 && selectedTopics.length === 0 && !isLoading && (
<div className="ws-latest-section">
<div className="ws-section-header">
<h2 className="ws-section-title">
{showSavedOnly ? 'Saved Topics' : (sortOrder === 'oldest' ? 'Oldest Topics' : 'Latest Topics')}
</h2>
<div className="ws-filter-container" ref={filterRef}>
<button
className={`ws-filter-btn ${(sortOrder !== 'latest' || showSavedOnly) ? 'ws-filter-active' : ''}`}
onClick={() => setShowFilterPopup(!showFilterPopup)}
title="Filter & Sort"
>
<FiFilter />
</button>
{showFilterPopup && (
<div className="ws-filter-popup">
<div className="ws-filter-section">
<h4 className="ws-filter-label">Sort by</h4>
<button
className={`ws-filter-option ${sortOrder === 'latest' ? 'ws-filter-selected' : ''}`}
onClick={() => setSortOrder('latest')}
>
<span>Latest</span>
{sortOrder === 'latest' && <FiCheck className="ws-filter-check" />}
</button>
<button
className={`ws-filter-option ${sortOrder === 'oldest' ? 'ws-filter-selected' : ''}`}
onClick={() => setSortOrder('oldest')}
>
<span>Oldest</span>
{sortOrder === 'oldest' && <FiCheck className="ws-filter-check" />}
</button>
</div>
<div className="ws-filter-divider"></div>
<div className="ws-filter-section">
<h4 className="ws-filter-label">Filter</h4>
<button
className={`ws-filter-option ${showSavedOnly ? 'ws-filter-selected' : ''}`}
onClick={() => setShowSavedOnly(!showSavedOnly)}
>
<span>Saved Only</span>
{showSavedOnly && <FiCheck className="ws-filter-check" />}
</button>
</div>
</div>
)}
</div>
</div>
<div className="ws-topics-grid">{renderTopicsWithAds(displayedTopics)}</div>
{hasMore && (
<div className="ws-load-more-section">
<button
onClick={loadMoreTopics}
className="ws-load-more-btn"
disabled={isLoadingMore}
>
{isLoadingMore ? (
<span className="ws-load-more-dots">
<span /><span /><span />
</span>
) : (
'View More'
)}
</button>
</div>
)}
{!hasMore && displayedTopics.length > 0 && (
<div className="ws-end-message">🎉 All topics loaded.</div>
)}
</div>
)}
{(searchQuery || selectedSubjects.length > 0 || selectedTopics.length > 0) && !isLoading && (
<div className="ws-results-section">
<h2 className="ws-section-title">Search Results ({searchTotal} found)</h2>
{searchResults.length > 0 ? (
<>
<div className="ws-topics-grid">{renderTopicsWithAds(searchResults)}</div>
{hasMore && (
<div className="ws-load-more-section">
<button
onClick={() => handleSearch(searchQuery, searchPage + 1)}
className="ws-load-more-btn"
disabled={isLoadingMore}
>
{isLoadingMore ? (
<span className="ws-load-more-dots">
<span /><span /><span />
</span>
) : (
'View More'
)}
</button>
</div>
)}
{!hasMore && searchResults.length > 0 && (
<div className="ws-end-message">✅ All matching results loaded.</div>
)}
</>
) : (
<div className="ws-no-results">
<FiSearch className="ws-no-results-icon" />
<h3>No results found</h3>
<p>Try searching with different keywords</p>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default WorkSearchInterface;