'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 = 'Generating...'; } // 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 = 'Download'; } } 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 = 'Download'; } } }; 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 }) => (
); // 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(); } }); return elements; }; const SkeletonLoader = () => (
{Array.from({ length: 4 }).map((_, index) => (
))}
); 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 (

{topic.topic}

{topic.userName} ({topic.usn})

{topic.subjectName} {new Date(topic.timestamp).toLocaleDateString('en-GB')}
{isMenuOpen && (
)}
{topic.content &&

{topic.content}

} {hasImages && (
{displayImages.map((imageUrl, imgIndex) => (
{`${topic.topic}
))}
{validImages.length > 2 && ( )}
)}
); }; return (
{ setSearchQuery(e.target.value); handleSearch(e.target.value, 1); }} className="ws-search-input" />
{/* Show cached saved topics immediately while loading */} {isLoading && cachedSavedTopics.length > 0 && (

Your Saved Topics

{cachedSavedTopics.map((topic, index) => renderTopicCard(topic, index))}
)} {/* Show skeleton loader below cached topics or alone if no cached */} {isLoading && } {!searchQuery && selectedSubjects.length === 0 && selectedTopics.length === 0 && !isLoading && (

{showSavedOnly ? 'Saved Topics' : (sortOrder === 'oldest' ? 'Oldest Topics' : 'Latest Topics')}

{showFilterPopup && (

Sort by

Filter

)}
{renderTopicsWithAds(displayedTopics)}
{hasMore && (
)} {!hasMore && displayedTopics.length > 0 && (
🎉 All topics loaded.
)}
)} {(searchQuery || selectedSubjects.length > 0 || selectedTopics.length > 0) && !isLoading && (

Search Results ({searchTotal} found)

{searchResults.length > 0 ? ( <>
{renderTopicsWithAds(searchResults)}
{hasMore && (
)} {!hasMore && searchResults.length > 0 && (
✅ All matching results loaded.
)} ) : (

No results found

Try searching with different keywords

)}
)}
); }; export default WorkSearchInterface;