import React, { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { Search, X, LayoutGrid, History, Zap, MessageCircle, Target, ShoppingCart, Fingerprint, PencilLine, Lock, Globe, Settings, Send, Crown, Route, ShieldCheck, ShieldBan, Mail, ChevronRight, CornerDownLeft, SearchX, Users, RefreshCw, Package, ChevronDown, FileText, Image, Folder } from 'lucide-react'; import { cn } from "@/lib/utils"; import { ModernCardHeader, GhostButton } from '../ui/settings-ui'; import { LOCAL_INDEX } from './searchIndex'; import type { LucideIcon } from 'lucide-react'; interface GlobalSearchProps { rtl?: boolean; } interface SearchOption { title: string; description: string; target: string; link: string; } interface SearchResult { title: string; description: string; icon: string; section: string; link: string; source: 'plugin' | 'users' | 'orders' | 'products' | 'subscriptions' | 'coupons' | 'campaign_broadcasts' | 'notification_rules' | 'posts' | 'pages' | 'media' | 'categories'; user_id?: number; options?: SearchOption[]; thumbnail_url?: string; } const IconRenderer = ({ iconName, className }: { iconName: string, className?: string }) => { const icons: Record = { LayoutGrid, History, Zap, MessageCircle, Target, ShoppingCart, Fingerprint, PencilLine, Lock, Globe, Settings, Send, Crown, Route, ShieldCheck, ShieldBan, Mail, Search, Users, RefreshCw, Package, FileText, Image, Folder }; const Icon = icons[iconName] || Search; return ; }; const GlobalSearch: React.FC = ({ rtl }) => { const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const [isMac] = useState(() => { if (typeof navigator === 'undefined') return false; return navigator.platform.toUpperCase().indexOf('MAC') >= 0; }); const [activeSources, setActiveSources] = useState(['plugin', 'users', 'orders', 'products', 'subscriptions', 'coupons', 'campaign_broadcasts', 'notification_rules', 'posts', 'pages', 'media', 'categories']); const [history, setHistory] = useState(() => { try { const saved = localStorage.getItem('wawp_search_history'); return saved ? JSON.parse(saved) : []; } catch { return []; } }); const inputRef = useRef(null); const searchTimeout = useRef | null>(null); const [dropdownOpen, setDropdownOpen] = useState(false); const dropdownRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); useEffect(() => { const handleGlobalShortcut = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setIsOpen(true); } }; window.addEventListener('keydown', handleGlobalShortcut); return () => window.removeEventListener('keydown', handleGlobalShortcut); }, []); useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } }, [isOpen]); const performSearch = useCallback(async (searchQuery: string, sources: string[]) => { if (searchQuery.length < 2) { setResults([]); setIsLoading(false); return; } // 1. Instantaneous Local Client-Side Search for plugin sections & specific settings const localMatches: SearchResult[] = []; if (sources.includes('plugin')) { const q = searchQuery.toLowerCase(); LOCAL_INDEX.forEach(item => { let pageMatch = false; if ( item.title.toLowerCase().includes(q) || item.description.toLowerCase().includes(q) || item.keywords.some(kw => kw.toLowerCase().includes(q)) ) { pageMatch = true; } const matchedOpts: SearchOption[] = []; if (item.options) { item.options.forEach(opt => { if ( opt.title.toLowerCase().includes(q) || opt.description.toLowerCase().includes(q) || opt.keywords.some(kw => kw.toLowerCase().includes(q)) ) { matchedOpts.push({ title: opt.title, description: opt.description, target: opt.target, link: `${window.location.pathname}?page=wawp&wawp_section=${item.section}&highlight_setting=${opt.target}` }); } }); } if (pageMatch || matchedOpts.length > 0) { localMatches.push({ title: item.title, description: item.description, icon: item.icon, section: item.section, link: `${window.location.pathname}?page=wawp&wawp_section=${item.section}`, source: 'plugin', options: matchedOpts }); } }); // Update state instantly! setResults(localMatches); } const backendSources = sources.filter(s => s !== 'plugin'); if (backendSources.length === 0) { setIsLoading(false); return; } try { const win = window as unknown as { wawpDashboardData?: { nonce?: string; global?: { nonce?: string; restUrl?: string; wpRestNonce?: string } }; WAWPAutoCheck?: { nonce?: string }; wpApiSettings?: { nonce?: string }; }; const dashboardData = win.wawpDashboardData; const nonce = dashboardData?.global?.wpRestNonce || dashboardData?.nonce || dashboardData?.global?.nonce || win.WAWPAutoCheck?.nonce || win.wpApiSettings?.nonce; let baseUrl = dashboardData?.global?.restUrl || '/wp-json/wawp/v1'; baseUrl = baseUrl.replace(/\/$/, ''); try { const urlObj = new URL(baseUrl, window.location.origin); if (urlObj.origin === window.location.origin) { baseUrl = urlObj.pathname + urlObj.search; baseUrl = baseUrl.replace(/\/$/, ''); } else { if (baseUrl.startsWith('http:') && window.location.protocol === 'https:') { baseUrl = baseUrl.replace('http:', 'https:'); } else if (baseUrl.startsWith('https:') && window.location.protocol === 'http:') { baseUrl = baseUrl.replace('https:', 'http:'); } } } catch (e) { console.warn('[GlobalSearch] URL parsing failed, using raw baseUrl:', e); } const searchUrl = `${baseUrl}/global-search?q=${encodeURIComponent(searchQuery)}&sources=${encodeURIComponent(backendSources.join(','))}&_wpnonce=${encodeURIComponent(nonce || '')}`; const res = await fetch(searchUrl, { credentials: 'include', headers: { 'X-WP-Nonce': nonce || '', 'Content-Type': 'application/json' } }); if (!res.ok) { console.error(`[GlobalSearch] Search request failed with status: ${res.status}`); return; } const json = await res.json() as { results?: SearchResult[] }; const backendResults = json.results || []; // Combine local instantaneous plugin results and remote database records elegantly! setResults([...localMatches, ...backendResults]); setSelectedIndex(-1); } catch (err) { console.error('[GlobalSearch] Search exception:', err); } finally { setIsLoading(false); } }, []); useEffect(() => { if (query.length > 1) { if (searchTimeout.current) clearTimeout(searchTimeout.current); searchTimeout.current = setTimeout(() => performSearch(query, activeSources), 300); } return () => { if (searchTimeout.current) clearTimeout(searchTimeout.current); }; }, [query, activeSources, performSearch]); const toggleSearch = () => { setIsOpen(!isOpen); if (isOpen) { setQuery(''); setResults([]); setSelectedIndex(-1); } }; const toggleSource = (source: string) => { setActiveSources(prev => { let next; if (prev.includes(source)) { if (prev.length === 1) next = prev; else next = prev.filter(s => s !== source); } else { next = [...prev, source]; } if (query.length > 1) setIsLoading(true); return next; }); }; const handleKeyDown = (e: React.KeyboardEvent) => { const activeResults = query.length < 2 ? history : results; if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(prev => (prev < activeResults.length - 1 ? prev + 1 : prev)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev)); } else if (e.key === 'Enter' && selectedIndex >= 0) { handleItemClick(activeResults[selectedIndex]); } else if (e.key === 'Escape') { toggleSearch(); } }; const addToHistory = (item: SearchResult) => { const newHistory = [item, ...history.filter(h => h.link !== item.link)].slice(0, 5); setHistory(newHistory); localStorage.setItem('wawp_search_history', JSON.stringify(newHistory)); }; const handleItemClick = (item: SearchResult) => { addToHistory(item); if (item.source !== 'plugin') { window.open(item.link, '_blank'); return; } window.history.pushState({ section: item.section }, "", item.link); window.dispatchEvent(new CustomEvent('wawp-navigate', { detail: item.section })); window.scrollTo({ top: 0, behavior: 'smooth' }); toggleSearch(); }; const handleOptionClick = (item: SearchResult, opt: SearchOption) => { addToHistory({ ...item, title: `${item.title} - ${opt.title}`, description: opt.description, link: opt.link }); window.history.pushState({ section: item.section, highlight_setting: opt.target }, "", opt.link); window.dispatchEvent(new CustomEvent('wawp-navigate', { detail: item.section })); window.scrollTo({ top: 0, behavior: 'smooth' }); toggleSearch(); }; const clearHistory = (e: React.MouseEvent) => { e.stopPropagation(); setHistory([]); localStorage.removeItem('wawp_search_history'); }; return ( <> {isOpen && createPortal(
e.stopPropagation()} > } />
{ const val = e.target.value; setQuery(val); if (val.length > 1) { setIsLoading(true); } else { setResults([]); setIsLoading(false); setSelectedIndex(-1); } }} onKeyDown={handleKeyDown} style={{ border: 'none', outline: 'none', boxShadow: 'none' }} /> {query && ( )}
{activeSources.slice(0, 2).map((val) => { const config = { plugin: { label: 'Wawp Plugin', color: 'bg-emerald-50 text-emerald-700 border-emerald-100' }, users: { label: 'Users', color: 'bg-blue-50 text-blue-700 border-blue-100' }, orders: { label: 'Orders', color: 'bg-indigo-50 text-indigo-700 border-indigo-100' }, products: { label: 'Products', color: 'bg-amber-50 text-amber-700 border-amber-100' }, subscriptions: { label: 'Subscriptions', color: 'bg-teal-50 text-teal-700 border-teal-100' }, coupons: { label: 'Coupons', color: 'bg-pink-50 text-pink-700 border-pink-100' }, campaign_broadcasts: { label: 'Campaigns', color: 'bg-rose-50 text-rose-700 border-rose-100' }, notification_rules: { label: 'Rules', color: 'bg-violet-50 text-violet-700 border-violet-100' }, posts: { label: 'Posts', color: 'bg-orange-50 text-orange-700 border-orange-100' }, pages: { label: 'Pages', color: 'bg-sky-50 text-sky-700 border-sky-100' }, media: { label: 'Media', color: 'bg-yellow-50 text-yellow-700 border-yellow-100' }, categories: { label: 'Categories', color: 'bg-cyan-50 text-cyan-700 border-cyan-100' }, }[val] || { label: val, color: 'bg-slate-50 text-slate-600 border-slate-100' }; return ( {config.label} ); })} {activeSources.length > 2 && ( +{activeSources.length - 2} more )}
{dropdownOpen && (
Filter Sources
{[ { value: 'plugin', label: 'Wawp Plugin', icon: 'Zap', colorClass: 'text-emerald-500' }, { value: 'users', label: 'Users', icon: 'Users', colorClass: 'text-blue-500' }, { value: 'orders', label: 'Orders', icon: 'ShoppingCart', colorClass: 'text-indigo-500' }, { value: 'products', label: 'Products', icon: 'Package', colorClass: 'text-amber-500' }, { value: 'subscriptions', label: 'Subscriptions', icon: 'RefreshCw', colorClass: 'text-teal-500' }, { value: 'coupons', label: 'Coupons', icon: 'Crown', colorClass: 'text-pink-500' }, { value: 'campaign_broadcasts', label: 'Campaigns', icon: 'Target', colorClass: 'text-rose-500' }, { value: 'notification_rules', label: 'Rules', icon: 'Zap', colorClass: 'text-violet-500' }, { value: 'posts', label: 'Posts', icon: 'FileText', colorClass: 'text-orange-500' }, { value: 'pages', label: 'Pages', icon: 'FileText', colorClass: 'text-sky-500' }, { value: 'media', label: 'Media', icon: 'Image', colorClass: 'text-yellow-500' }, { value: 'categories', label: 'Categories', icon: 'Folder', colorClass: 'text-cyan-500' }, ].map((opt) => { const isSelected = activeSources.includes(opt.value); return ( ); })}
)}
{query.length < 2 ? ( <> {history.length > 0 ? (

Recent Searches

{history.map((item, index) => ( ))}
) : (

OmniSearch Discovery

Search across your entire platform, commerce, and users in real-time.

)} ) : ( <>

Search Results

{results.length > 0 && {results.length} Matches}
{results.length > 0 ? (
{results.map((item, index) => { const showHeader = index === 0 || item.source !== results[index - 1]?.source; const srcConfig = { plugin: { label: 'Wawp Pages & Settings', color: 'emerald' }, users: { label: 'Registered Users', color: 'blue' }, orders: { label: 'WooCommerce Orders', color: 'amber' }, products: { label: 'WooCommerce Products', color: 'indigo' }, subscriptions: { label: 'WooCommerce Subscriptions', color: 'teal' }, coupons: { label: 'WooCommerce Coupons', color: 'pink' }, campaign_broadcasts: { label: 'Broadcast Campaigns', color: 'rose' }, notification_rules: { label: 'Notification Rules', color: 'violet' }, posts: { label: 'WordPress Posts', color: 'orange' }, pages: { label: 'WordPress Pages', color: 'sky' }, media: { label: 'Media Library', color: 'yellow' }, categories: { label: 'Categories & Tags', color: 'cyan' }, }[item.source] || { label: item.source, color: 'slate' }; return (
{showHeader && (
{srcConfig.label} {results.filter(r => r.source === item.source).length}
)}
); })}
) : (

No results found for "{query}"

Try toggling filters or checking your keywords.

)} )}
to select
to navigate
Wawp
, document.body )} ); }; export default GlobalSearch;