// ═══════════════════════════════════════════════════════════ // UTILS CLASS // ═══════════════════════════════════════════════════════════ /** * Static utility class for common helper functions */ class Utils { /** * Escape HTML special characters * @param {string} s - String to escape * @returns {string} Escaped string */ static esc(s) { if (!s) return ""; const map = { "'": "'", '"': """, "&": "&", "<": "<", ">": ">", }; return s.replace(/[&<>"']/g, (ch) => map[ch]); } /** * Decode HTML entities * @param {string} s - String with HTML entities * @returns {string} Decoded string */ static decodeHTML(s) { const textarea = document.createElement("textarea"); textarea.innerHTML = s; return textarea.value; } /** * Format large numbers (K, M) * @param {number} n - Number to format * @returns {string} Formatted string */ static fmt(n) { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; return n?.toString() || "0"; } /** * Convert timestamp to relative time string * @param {number} ts - Timestamp in milliseconds * @returns {string} Relative time string */ static timeAgo(ts) { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return "NOW"; if (s < 3600) return `${Math.floor(s / 60)}M`; if (s < 86400) return `${Math.floor(s / 3600)}H`; return `${Math.floor(s / 86400)}D`; } /** * Highlight search terms in text * @param {string} text - Text to highlight * @param {string} query - Search query * @returns {string} HTML with highlighted terms */ static highlight(text, query) { if (!query) return text; const words = query.split(/\s+/).filter((w) => w.length > 2); let result = text; words.forEach((w) => { const regex = new RegExp(`(${w})`, "gi"); result = result.replace(regex, "$1"); }); return result; } /** * Copy text to clipboard * @param {string} text - Text to copy */ static async copy(text) { try { await navigator.clipboard.writeText(text); Toast.show("COPIED"); } catch (err) { // Fallback for older browsers const textarea = document.createElement("textarea"); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); Toast.show("COPIED"); } } /** * Show toast notification * @param {string} message - Message to display * @param {number} duration - Duration in ms (default: 3000) */ static toast(message, duration = 3000) { Toast.show(message, duration); } /** * Safely evaluate mathematical expressions * @param {string} expr - Mathematical expression * @returns {number|string} Result or error string */ static safeEval(expr) { // Remove unsafe characters and evaluate const safeExpr = expr.replace(/[^0-9+\-*/().^]/g, "").replace(/\^/g, "**"); try { // Use Function constructor in a safer way with restricted scope const result = new Function(`return ${safeExpr}`)(); return typeof result === "number" && !isNaN(result) && isFinite(result) ? result : "ERR"; } catch { return "ERR"; } } /** * Generate a random UUID v4 * @returns {string} UUID */ static generateUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); }); } /** * Convert RGB to HSL * @param {number} r - Red (0-255) * @param {number} g - Green (0-255) * @param {number} b - Blue (0-255) * @returns {string} HSL string */ static rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / d + 2) / 6; break; case b: h = ((r - g) / d + 4) / 6; break; } } return `${Math.round(h * 360)}°, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%`; } /** * Debounce function * @param {Function} func - Function to debounce * @param {number} wait - Wait time in ms * @returns {Function} Debounced function */ static debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Generate random password * @param {number} length - Password length * @returns {string} Random password */ static generatePassword(length = 20) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; let password = ""; for (let i = 0; i < length; i++) { password += chars[Math.floor(Math.random() * chars.length)]; } return password; } } // ═══════════════════════════════════════════════════════════ // TOAST CLASS // ═══════════════════════════════════════════════════════════ class Toast { /** * Show toast notification * @param {string} message - Message to display * @param {number} duration - Duration in ms */ static show(message, duration = 3000) { const container = document.getElementById("toast-container") || (() => { const div = document.createElement("div"); div.id = "toast-container"; div.className = "toast-container"; document.body.appendChild(div); return div; })(); const toast = document.createElement("div"); toast.className = "toast"; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateY(20px)"; setTimeout(() => toast.remove(), 200); }, duration); } } // ═══════════════════════════════════════════════════════════ // TRADINGVIEW WIDGET CLASS // ═══════════════════════════════════════════════════════════ class TradingViewWidget { /** * Create a TradingView widget * @param {string} containerId - Container element ID * @param {string} symbol - Trading symbol * @param {boolean} isLight - Light theme flag */ constructor(containerId, symbol, isLight = false) { this.containerId = containerId; this.symbol = symbol; this.isLight = isLight; this.widget = null; } /** * Load and initialize the TradingView widget */ async load() { if (!window.TradingView) { await this.loadScript(); } this.createWidget(); } /** * Load TradingView script * @returns {Promise} */ loadScript() { return new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = "https://s3.tradingview.com/tv.js"; script.onload = () => resolve(); script.onerror = () => reject(new Error("Failed to load TradingView script")); document.head.appendChild(script); }); } /** * Create the TradingView widget */ createWidget() { if (this.widget) { this.widget.remove(); } this.widget = new TradingView.widget({ allow_symbol_change: true, backgroundColor: this.isLight ? "rgba(244, 244, 245, 1)" : "rgba(0,0,0,1)", container_id: this.containerId, enable_publishing: false, gridColor: this.isLight ? "rgba(220, 220, 220, 1)" : "rgba(30,30,30,1)", height: 500, interval: "D", locale: "en", studies: ["Volume@tv-basicstudies"], style: "1", symbol: this.symbol, theme: this.isLight ? "light" : "dark", timezone: "Etc/UTC", toolbar_bg: this.isLight ? "#f4f4f5" : "#000", width: "100%", }); } /** * Remove the widget */ destroy() { if (this.widget) { const container = document.getElementById(this.containerId); if (container) { container.innerHTML = ""; } this.widget = null; } } /** * Update widget symbol * @param {string} symbol - New symbol */ updateSymbol(symbol) { this.symbol = symbol; if (this.widget) { this.widget.chart().setSymbol(symbol); } } } // ═══════════════════════════════════════════════════════════ // SEARCH CLASS // ═══════════════════════════════════════════════════════════ class Search { /** * Create a new Search instance */ constructor() { // State this.state = { calcExpr: "", calcResult: "0", hasSearched: false, history: JSON.parse(localStorage.getItem("sh") || "[]"), query: "", results: [], selectedResultIndex: -1, watchlist: JSON.parse(localStorage.getItem("sw") || '["TSLA", "AAPL", "BTC", "ETH"]'), }; // Request cancellation this.abortController = null; // Suggestions this.suggestions = []; this.showingSuggestions = false; // DOM Elements this.searchInput = document.getElementById("search-input"); this.resultsEl = document.getElementById("results"); this.heroEl = document.getElementById("hero"); this.mainEl = document.getElementById("main-content"); this.statsBar = document.getElementById("stats-bar"); // Stock exchanges mapping this.stockExchanges = { AAPL: "NASDAQ", ADA: "CRYPTO", AMD: "NASDAQ", AMZN: "NASDAQ", BAC: "NYSE", BTC: "CRYPTO", DIS: "NYSE", DOGE: "CRYPTO", ETH: "CRYPTO", EURUSD: "FX", GBPUSD: "FX", GOOG: "NASDAQ", GOOGL: "NASDAQ", INTC: "NASDAQ", JNJ: "NYSE", JPM: "NYSE", KO: "NYSE", META: "NASDAQ", MSFT: "NASDAQ", NFLX: "NASDAQ", NVDA: "NASDAQ", PYPL: "NASDAQ", QQQ: "NASDAQ", SOL: "CRYPTO", SPY: "AMEX", TSLA: "NASDAQ", USDJPY: "FX", V: "NYSE", WMT: "NYSE", XOM: "NYSE", XRP: "CRYPTO", }; // Sources configuration this.sources = { dictionary: { bang: "!d", count: 0, enabled: true, fetch: this.createSourceFetchHandler("dictionary", async (q, signal) => { const response = await fetch( `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`, { signal }, ); if (!response.ok) return []; const data = await response.json(); return data.slice(0, 3).flatMap((entry) => entry.meanings.slice(0, 2).map((meaning) => ({ meta: { phonetic: entry.phonetic }, snippet: meaning.definitions[0]?.definition || "", source: "dictionary", title: `${entry.word} [${meaning.partOfSpeech}]`, url: entry.sourceUrls?.[0] || "#", })), ); }), name: "DICTIONARY", }, github: { bang: "!g", count: 0, enabled: true, fetch: this.createSourceFetchHandler("github", async (q, signal) => { const response = await fetch( `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`, { signal }, ); const data = await response.json(); return (data.items || []).map((item) => ({ meta: { forks: item.forks_count, lang: item.language, stars: item.stargazers_count }, snippet: item.description || "NO DESCRIPTION", source: "github", title: item.full_name, url: item.html_url, })); }), name: "GITHUB", }, hackernews: { bang: "!hn", count: 0, enabled: true, fetch: this.createSourceFetchHandler("hackernews", async (q, signal) => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`, { signal }, ); const data = await response.json(); return (data.hits || []) .filter((hit) => hit.title) .map((hit) => ({ meta: { comments: hit.num_comments, points: hit.points }, snippet: `${hit.points || 0} POINTS // ${hit.author}`, source: "hackernews", title: hit.title, url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, })); }), name: "HACKERNEWS", }, news: { bang: "!n", count: 0, enabled: true, fetch: this.createSourceFetchHandler("news", async (q, signal) => { const [hn, rd] = await Promise.all([ fetch( `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`, { signal }, ) .then((r) => r.json()) .catch(() => ({ hits: [] })), fetch( `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`, { signal }, ) .then((r) => r.json()) .catch(() => ({ data: { children: [] } })), ]); const hnResults = (hn.hits || []).map((hit) => ({ meta: { date: hit.created_at_i, lang: "HN" }, snippet: `HACKERNEWS // ${hit.points || 0} PTS // ${Utils.timeAgo(hit.created_at_i * 1000)}`, source: "news", title: hit.title, url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, })); const redditResults = (rd.data?.children || []).map((child) => ({ meta: { date: child.data.created_utc, lang: "RD" }, snippet: `REDDIT r/${child.data.subreddit.toUpperCase()} // ${child.data.score} UPVOTES`, source: "news", title: child.data.title, url: `https://reddit.com${child.data.permalink}`, })); return [...hnResults, ...redditResults].sort((a, b) => b.meta.date - a.meta.date); }), name: "AGGREGATE NEWS", }, npm: { bang: "!npm", count: 0, enabled: true, fetch: this.createSourceFetchHandler("npm", async (q, signal) => { const response = await fetch( `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`, { signal }, ); const data = await response.json(); return (data.objects || []).map((obj) => ({ meta: { version: obj.package.version }, snippet: obj.package.description || "NO DESCRIPTION", source: "npm", title: obj.package.name, url: `https://npmjs.com/package/${obj.package.name}`, })); }), name: "NPM", }, openlibrary: { bang: "!b", count: 0, enabled: true, fetch: this.createSourceFetchHandler("openlibrary", async (q, signal) => { const response = await fetch( `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`, { signal }, ); const data = await response.json(); return (data.docs || []).map((book) => ({ meta: { year: book.first_publish_year }, snippet: `BY ${(book.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, source: "openlibrary", title: book.title, url: `https://openlibrary.org${book.key}`, })); }), name: "OPENLIBRARY", }, reddit: { bang: "!r", count: 0, enabled: true, fetch: this.createSourceFetchHandler("reddit", async (q, signal) => { const response = await fetch( `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`, { signal }, ); const data = await response.json(); return (data.data?.children || []).map((child) => ({ meta: { comments: child.data.num_comments, score: child.data.score }, snippet: child.data.selftext?.substring(0, 200) || `r/${child.data.subreddit}`, source: "reddit", title: child.data.title, url: `https://reddit.com${child.data.permalink}`, })); }), name: "REDDIT", }, stackoverflow: { bang: "!so", count: 0, enabled: true, fetch: this.createSourceFetchHandler("stackoverflow", async (q, signal) => { const response = await fetch( `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`, { signal }, ); const data = await response.json(); return (data.items || []).map((item) => ({ meta: { answers: item.answer_count, score: item.score }, snippet: `${item.answer_count} ANSWERS // ${item.view_count} VIEWS`, source: "stackoverflow", title: Utils.decodeHTML(item.title), url: item.link, })); }), name: "STACKOVERFLOW", }, wikipedia: { bang: "!w", count: 0, enabled: true, fetch: this.createSourceFetchHandler("wikipedia", async (q, signal) => { const response = await fetch( `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`, { signal }, ); const data = await response.json(); return (data.query?.search || []).map((item) => ({ meta: { words: item.wordcount }, snippet: item.snippet.replace(/<[^>]*>/g, ""), source: "wikipedia", title: item.title, url: `https://en.wikipedia.org/wiki/${encodeURIComponent(item.title)}`, })); }), instant: async (q, signal) => { const searchResponse = await fetch( `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`, { signal }, ); const searchData = await searchResponse.json(); if (!searchData.query?.search?.length) return null; const title = searchData.query.search[0].title; const pageResponse = await fetch( `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`, { signal }, ); const pageData = await pageResponse.json(); const page = Object.values(pageData.query.pages)[0]; return { content: page.extract, source: "WIKIPEDIA", title: page.title, url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, }; }, name: "WIKIPEDIA", }, }; // Initialize this.init(); } /** * Create a fetch handler with error handling * @param {string} sourceName - Name of the source * @param {Function} fetchFn - Fetch function * @returns {Function} Wrapped fetch function */ createSourceFetchHandler(sourceName, fetchFn) { return async (query, signal) => { try { return await fetchFn(query, signal); } catch (error) { if (error.name === "AbortError") { throw error; // Let abort errors pass through } console.error(`Error fetching from ${sourceName}:`, error); return []; } }; } /** * Initialize the search engine */ init() { this.renderSources(); this.renderHistory(); this.renderWatchlist(); this.initTheme(); this.updateClocks(); setInterval(() => this.updateClocks(), 1000); this.setupCalc(); this.setupListeners(); this.setupWatchlistListeners(); this.handleUrlQueryOnLoad(); this.setupKeyboardNavigation(); } /** * Render sources list */ renderSources() { const sourcesList = document.getElementById("sources-list"); if (!sourcesList) return; sourcesList.innerHTML = Object.entries(this.sources) .map( ([key, source]) => `
${source.name} ${source.count}
`, ) .join(""); } /** * Toggle source enabled state * @param {string} key - Source key */ toggleSource(key) { if (this.sources[key]) { this.sources[key].enabled = !this.sources[key].enabled; this.renderSources(); if (this.state.query) { this.search(this.state.query); } } } /** * Initialize theme from localStorage */ initTheme() { const theme = localStorage.getItem("theme"); if (theme === "light") { document.body.classList.add("light-mode"); } } /** * Toggle theme between dark and light */ toggleTheme() { document.body.classList.toggle("light-mode"); const isLight = document.body.classList.contains("light-mode"); localStorage.setItem("theme", isLight ? "light" : "dark"); } /** * Render watchlist */ renderWatchlist() { const container = document.getElementById("watchlist-container"); if (!container) return; if (!this.state.watchlist.length) { container.innerHTML = '
EMPTY
'; return; } container.innerHTML = this.state.watchlist .map((ticker) => { const exchange = this.stockExchanges[ticker] || "STOCK"; return `
${ticker} ${exchange} ×
`; }) .join(""); } /** * Add ticker to watchlist */ addTicker() { const input = document.getElementById("watchlist-input"); if (!input) return; const ticker = input.value.toUpperCase().trim(); if (ticker && !this.state.watchlist.includes(ticker)) { this.state.watchlist.push(ticker); localStorage.setItem("sw", JSON.stringify(this.state.watchlist)); this.renderWatchlist(); input.value = ""; } } /** * Remove ticker from watchlist * @param {Event} event - Click event * @param {string} ticker - Ticker to remove */ removeTicker(event, ticker) { event.stopPropagation(); this.state.watchlist = this.state.watchlist.filter((t) => t !== ticker); localStorage.setItem("sw", JSON.stringify(this.state.watchlist)); this.renderWatchlist(); } /** * Setup watchlist event listeners */ setupWatchlistListeners() { const addBtn = document.getElementById("watchlist-add-btn"); const input = document.getElementById("watchlist-input"); if (addBtn) { addBtn.onclick = () => this.addTicker(); } if (input) { input.addEventListener("keydown", (e) => { if (e.key === "Enter") this.addTicker(); }); } } /** * Get trading symbol for ticker * @param {string} ticker - Stock ticker * @returns {string} Trading symbol */ getSymbol(ticker) { const t = ticker.toUpperCase(); const exchange = this.stockExchanges[t]; if (exchange === "CRYPTO") return `BINANCE:${t}USDT`; if (exchange === "FX") return `FX:${t}`; if (exchange) return `${exchange}:${t}`; return `NASDAQ:${t}`; } /** * Update URL with search query * @param {string} query - Search query */ updateUrlWithQuery(query) { const url = new URL(window.location); if (query && query.trim()) { url.searchParams.set("q", query); } else { url.searchParams.delete("q"); } window.history.replaceState({}, "", url); } /** * Get query from URL * @returns {string} Query string */ getQueryFromUrl() { const params = new URLSearchParams(window.location.search); return params.get("q") || ""; } /** * Handle URL query on page load */ handleUrlQueryOnLoad() { const query = this.getQueryFromUrl(); if (query.trim()) { this.searchInput.value = query; this.search(query); } } /** * Main search function * @param {string} query - Search query */ async search(query) { // Cancel any ongoing request if (this.abortController) { this.abortController.abort(); } this.abortController = new AbortController(); const trimmedQuery = query.trim(); if (!trimmedQuery) { this.updateUrlWithQuery(""); return this.goHome(); } this.updateUrlWithQuery(trimmedQuery); // Show results view if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } // Stock ticker const stockMatch = trimmedQuery.match(/^\$([A-Za-z]{1,5})$/); if (stockMatch) { this.showStockWidget(stockMatch[1].toUpperCase()); this.addHistory(trimmedQuery); return; } // Bangs const bangMatch = trimmedQuery.match(/^!(\w+)\s*(.*)/); if (bangMatch) { const bang = `!${bangMatch[1]}`; const bangQuery = bangMatch[2]; const sourceEntry = Object.entries(this.sources).find(([_, s]) => s.bang === bang); if (sourceEntry && bangQuery) { // Enable only this source Object.keys(this.sources).forEach((k) => { this.sources[k].enabled = k === sourceEntry[0]; }); this.renderSources(); query = bangQuery; } } // Calculator if (trimmedQuery.startsWith("=")) { this.showCalcResult(trimmedQuery.slice(1)); return; } this.state.query = trimmedQuery; this.showLoading(); this.addHistory(trimmedQuery); const startTime = performance.now(); const enabledSources = Object.entries(this.sources).filter(([_, s]) => s.enabled); // Fetch instant results this.fetchInstant(trimmedQuery); // Fetch from all enabled sources const promises = enabledSources.map(async ([key, source]) => { try { const results = await source.fetch(trimmedQuery, this.abortController.signal); this.sources[key].count = results.length; const countEl = document.getElementById(`c-${key}`); if (countEl) countEl.textContent = results.length; return results; } catch (error) { if (error.name !== "AbortError") { console.error(`Source ${key} failed:`, error); } return []; } }); try { const allResults = await Promise.all(promises); this.state.results = allResults.flat(); const elapsed = Math.round(performance.now() - startTime); // Update stats document.getElementById("stat-results").textContent = this.state.results.length; document.getElementById("stat-sources").textContent = enabledSources.length; document.getElementById("stat-time").textContent = `${elapsed}MS`; document.getElementById("results-label").textContent = `${this.state.results.length} RESULTS`; document.getElementById("results-info").textContent = `${elapsed}MS // ${enabledSources.length} SOURCES`; this.renderResults(); this.state.selectedResultIndex = -1; // Reset selection } catch (error) { if (error.name !== "AbortError") { console.error("Search failed:", error); Toast.show("SEARCH FAILED"); } } } /** * Go to home state */ goHome() { this.state.hasSearched = false; this.heroEl.classList.remove("compact"); this.mainEl.style.display = "none"; this.statsBar.classList.remove("visible"); this.searchInput.value = ""; this.updateUrlWithQuery(""); this.hideSuggestions(); this.state.selectedResultIndex = -1; } /** * Show stock widget * @param {string} ticker - Stock ticker */ showStockWidget(ticker) { const symbol = this.getSymbol(ticker); const widgetId = `tv_${Math.random().toString(36).substr(2, 9)}`; const isLight = document.body.classList.contains("light-mode"); this.resultsEl.innerHTML = `

${ticker}

${symbol}
`; document.getElementById("results-label").textContent = `STOCK: ${ticker}`; document.getElementById("results-info").textContent = symbol; const tvWidget = new TradingViewWidget(widgetId, symbol, isLight); tvWidget.load(); } /** * Fetch instant results (dictionary/Wikipedia) * @param {string} query - Search query */ async fetchInstant(query) { // Dictionary if (/^[a-zA-Z]+$/.test(query) && this.sources.dictionary.enabled) { try { const response = await fetch( `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}`, { signal: this.abortController?.signal }, ); if (response.ok) { const data = await response.json(); if (data[0]) { const meanings = data[0].meanings .slice(0, 3) .map((m) => `[${m.partOfSpeech.toUpperCase()}] ${m.definitions[0]?.definition}`) .join("\n\n"); this.showInstant({ content: meanings, source: "DICTIONARY", title: data[0].word.toUpperCase() + (data[0].phonetic ? ` ${data[0].phonetic}` : ""), }); return; } } } catch (error) { if (error.name !== "AbortError") { console.error("Dictionary fetch failed:", error); } } } // Wikipedia if (this.sources.wikipedia.enabled && this.sources.wikipedia.instant) { try { const instantResult = await this.sources.wikipedia.instant( query, this.abortController?.signal, ); if (instantResult) { this.showInstant(instantResult); } } catch (error) { if (error.name !== "AbortError") { console.error("Wikipedia instant fetch failed:", error); } } } } /** * Show instant answer * @param {Object} data - Instant answer data */ showInstant(data) { const instantBox = document.createElement("div"); instantBox.className = "instant-box"; instantBox.innerHTML = `
INSTANT ANSWER
${Utils.esc(data.title)}
${Utils.esc(data.content) .split("\n") .map((p) => `

${p}

`) .join("")}
SOURCE: ${Utils.esc(data.source)} ${data.url ? `VIEW FULL →` : ""}
`; // Clear previous results but keep instant box if it exists const existingInstant = this.resultsEl.querySelector(".instant-box"); if (existingInstant) { existingInstant.remove(); } // Insert at the beginning if (this.resultsEl.firstChild) { this.resultsEl.insertBefore(instantBox, this.resultsEl.firstChild); } else { this.resultsEl.appendChild(instantBox); } } /** * Render search results (batched for performance) */ renderResults() { if (this.state.results.length === 0) { this.resultsEl.innerHTML = `
NO RESULTS
TRY DIFFERENT KEYWORDS OR ENABLE MORE SOURCES
`; return; } // Use DocumentFragment for batch DOM updates const fragment = document.createDocumentFragment(); // Keep instant box if present const instantBox = this.resultsEl.querySelector(".instant-box"); if (instantBox) { fragment.appendChild(instantBox); } // Create result elements this.state.results.forEach((result, index) => { const resultEl = document.createElement("div"); resultEl.className = "result"; resultEl.dataset.index = index; resultEl.innerHTML = `
${String(index + 1).padStart(2, "0")}
${result.source.toUpperCase()}
${Utils.esc(result.title)}
${Utils.esc(result.url)}
${Utils.highlight(Utils.esc(result.snippet), this.state.query)}
${ result.meta ? `
${result.meta.stars !== undefined ? `★ ${Utils.fmt(result.meta.stars)}` : ""} ${result.meta.forks !== undefined ? `⑂ ${Utils.fmt(result.meta.forks)}` : ""} ${result.meta.score !== undefined ? `↑ ${Utils.fmt(result.meta.score)}` : ""} ${result.meta.comments !== undefined ? `◨ ${Utils.fmt(result.meta.comments)}` : ""} ${result.meta.words !== undefined ? `◎ ${Utils.fmt(result.meta.words)}W` : ""} ${result.meta.lang ? `${result.meta.lang.toUpperCase()}` : ""} ${result.meta.version ? `V${result.meta.version}` : ""}
` : "" }
`; // Add click handler resultEl.addEventListener("click", (e) => { if (!e.target.closest(".result-meta")) { window.open(result.url, "_blank"); } }); fragment.appendChild(resultEl); }); // Batch update this.resultsEl.innerHTML = ""; this.resultsEl.appendChild(fragment); } /** * Show loading state */ showLoading() { this.resultsEl.innerHTML = Array(4) .fill( '
FETCHING
', ) .join(""); } /** * Show calculator result * @param {string} expr - Mathematical expression */ showCalcResult(expr) { const result = Utils.safeEval(expr); this.resultsEl.innerHTML = `
CALCULATOR
${Utils.esc(expr)} = ${result}
`; document.getElementById("results-label").textContent = "CALCULATOR"; } /** * Setup calculator */ setupCalc() { document.querySelectorAll(".calc-btn").forEach((btn) => { btn.addEventListener("click", () => { const value = btn.dataset.v; if (value === "C") { this.state.calcExpr = ""; this.state.calcResult = "0"; } else if (value === "=") { try { this.state.calcResult = Utils.safeEval(this.state.calcExpr); } catch { this.state.calcResult = "ERR"; } } else { this.state.calcExpr += value; } document.getElementById("calc-expr").textContent = this.state.calcExpr; document.getElementById("calc-result").textContent = this.state.calcResult; }); }); } /** * Add query to history * @param {string} query - Search query */ addHistory(query) { this.state.history = this.state.history.filter((h) => h.q !== query); this.state.history.unshift({ q: query, t: Date.now() }); this.state.history = this.state.history.slice(0, 20); localStorage.setItem("sh", JSON.stringify(this.state.history)); this.renderHistory(); } /** * Render history */ renderHistory() { const list = document.getElementById("history-list"); if (!list) return; if (!this.state.history.length) { list.innerHTML = '
NO HISTORY
'; return; } list.innerHTML = this.state.history .slice(0, 6) .map( (item) => `
${Utils.esc(item.q)} ${Utils.timeAgo(item.t)}
`, ) .join(""); } /** * Update clocks */ updateClocks() { const now = new Date(); const clockEl = document.getElementById("clock"); if (clockEl) { clockEl.textContent = now.toLocaleTimeString("en-GB"); } const zones = [ { label: "LOCAL", tz: Intl.DateTimeFormat().resolvedOptions().timeZone }, { label: "UTC", tz: "UTC" }, { label: "NYC", tz: "America/New_York" }, { label: "TOKYO", tz: "Asia/Tokyo" }, ]; const clocksEl = document.getElementById("world-clocks"); if (clocksEl) { clocksEl.innerHTML = zones .map( (zone) => `
${now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: zone.tz, })}
${zone.label}
`, ) .join(""); } } /** * Setup event listeners */ setupListeners() { // Debounced search on input const debouncedSearch = Utils.debounce((value) => { this.search(value); this.updateSuggestions(value); }, 300); this.searchInput.addEventListener("input", (e) => { debouncedSearch(e.target.value); }); // Enter key for immediate search this.searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { this.search(this.searchInput.value); this.hideSuggestions(); } else if (e.key === "Escape") { this.goHome(); } }); // Global keyboard shortcuts document.addEventListener("keydown", (e) => { if ( e.key === "/" && document.activeElement !== this.searchInput && document.activeElement !== document.getElementById("watchlist-input") ) { e.preventDefault(); this.searchInput.focus(); } if (e.key === "Escape") { this.goHome(); } }); // Clear button const clearBtn = document.getElementById("clear-btn"); if (clearBtn) { clearBtn.onclick = () => { this.goHome(); this.searchInput.focus(); }; } // Voice search const voiceBtn = document.getElementById("voice-btn"); if (voiceBtn) { voiceBtn.onclick = () => this.voiceSearch(); } // Handle browser back/forward window.addEventListener("popstate", () => { const query = this.getQueryFromUrl(); if (query !== this.state.query) { if (query.trim()) { this.searchInput.value = query; this.search(query); } else { this.goHome(); } } }); } /** * Setup keyboard navigation for results */ setupKeyboardNavigation() { document.addEventListener("keydown", (e) => { if (!this.state.hasSearched || this.state.results.length === 0) return; const results = Array.from(document.querySelectorAll(".result")); if (results.length === 0) return; switch (e.key) { case "ArrowDown": e.preventDefault(); this.navigateResults(1); break; case "ArrowUp": e.preventDefault(); this.navigateResults(-1); break; case "Enter": if (this.state.selectedResultIndex >= 0) { const result = results[this.state.selectedResultIndex]; if (result) { const url = result.querySelector(".result-url").textContent; if (url) window.open(url, "_blank"); } } break; } }); } /** * Navigate through results with arrow keys * @param {number} direction - 1 for down, -1 for up */ navigateResults(direction) { const results = Array.from(document.querySelectorAll(".result")); if (results.length === 0) return; // Remove previous selection if (this.state.selectedResultIndex >= 0 && results[this.state.selectedResultIndex]) { results[this.state.selectedResultIndex].classList.remove("selected"); } // Calculate new index let newIndex = this.state.selectedResultIndex + direction; if (newIndex < 0) newIndex = results.length - 1; if (newIndex >= results.length) newIndex = 0; // Apply new selection this.state.selectedResultIndex = newIndex; results[newIndex].classList.add("selected"); results[newIndex].scrollIntoView({ behavior: "smooth", block: "nearest" }); } /** * Update search suggestions * @param {string} query - Current query */ updateSuggestions(query) { if (!query.trim()) { this.hideSuggestions(); return; } // Get suggestions from history and bangs this.suggestions = [ ...this.state.history .filter((item) => item.q.toLowerCase().includes(query.toLowerCase())) .map((item) => item.q) .slice(0, 3), ...Object.values(this.sources) .map((source) => `${source.bang} `) .filter((bang) => bang.includes(query.toLowerCase())), ]; this.showSuggestions(); } /** * Show suggestions dropdown */ showSuggestions() { if (this.suggestions.length === 0) { this.hideSuggestions(); return; } // Create or update suggestions container let container = document.querySelector(".suggestions-container"); if (!container) { container = document.createElement("div"); container.className = "suggestions-container"; this.searchInput.parentNode.appendChild(container); } container.innerHTML = this.suggestions .map( (suggestion) => `
${Utils.esc(suggestion)}
`, ) .join(""); this.showingSuggestions = true; } /** * Hide suggestions dropdown */ hideSuggestions() { const container = document.querySelector(".suggestions-container"); if (container) { container.remove(); } this.showingSuggestions = false; } /** * Select a suggestion * @param {string} suggestion - Selected suggestion */ selectSuggestion(suggestion) { this.searchInput.value = suggestion; this.search(suggestion); this.hideSuggestions(); } /** * Voice search */ voiceSearch() { if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) { Toast.show("VOICE NOT SUPPORTED"); return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.lang = "en-US"; const voiceBtn = document.getElementById("voice-btn"); voiceBtn.style.background = "#fff"; voiceBtn.style.color = "#000"; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; this.searchInput.value = transcript; this.search(transcript); }; recognition.onend = () => { voiceBtn.style.background = ""; voiceBtn.style.color = ""; }; recognition.start(); } /** * Search for specific query * @param {string} query - Query to search for */ searchFor(query) { this.searchInput.value = query; this.search(query); } /** * Insert bang into search input * @param {string} bang - Bang to insert */ insertBang(bang) { this.searchInput.value = `${bang} `; this.searchInput.focus(); this.updateUrlWithQuery(`${bang} `); } /** * Toggle widget visibility * @param {string} id - Widget ID */ toggleWidget(id) { const widget = document.getElementById(id); if (widget) { widget.classList.toggle("open"); } } // Tool methods toolUUID() { const uuid = Utils.generateUUID(); Utils.copy(uuid); Toast.show(`UUID: ${uuid.substring(0, 13)}...`); } toolTimer() { if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } this.resultsEl.innerHTML = `
TIMER
00:00:00
`; document.getElementById("results-label").textContent = "TIMER"; } startTimer() { if (this.timerInterval) return; this.timerSec = this.timerSec || 0; this.timerInterval = setInterval(() => { this.timerSec++; const h = String(Math.floor(this.timerSec / 3600)).padStart(2, "0"); const m = String(Math.floor((this.timerSec % 3600) / 60)).padStart(2, "0"); const s = String(this.timerSec % 60).padStart(2, "0"); const display = document.getElementById("timer-display"); if (display) display.textContent = `${h}:${m}:${s}`; }, 1000); } stopTimer() { clearInterval(this.timerInterval); this.timerInterval = null; } resetTimer() { this.stopTimer(); this.timerSec = 0; const display = document.getElementById("timer-display"); if (display) display.textContent = "00:00:00"; } toolBase64() { if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } this.resultsEl.innerHTML = `
BASE64
OUTPUT
`; document.getElementById("results-label").textContent = "BASE64"; } toolJSON() { if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } this.resultsEl.innerHTML = `
JSON
OUTPUT
`; document.getElementById("results-label").textContent = "JSON"; } formatJSON() { try { const input = document.getElementById("json-input").value; const output = JSON.stringify(JSON.parse(input), null, 2); document.getElementById("json-output").textContent = output; } catch (e) { document.getElementById("json-output").textContent = `INVALID: ${e.message}`; } } minifyJSON() { try { const input = document.getElementById("json-input").value; const output = JSON.stringify(JSON.parse(input)); document.getElementById("json-output").textContent = output; } catch (e) { document.getElementById("json-output").textContent = `INVALID: ${e.message}`; } } toolHash() { if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } this.resultsEl.innerHTML = `
SHA-256 HASH
OUTPUT
`; document.getElementById("results-label").textContent = "HASH"; } async genHash() { const input = document.getElementById("hash-input").value; const data = new TextEncoder().encode(input); const hash = await crypto.subtle.digest("SHA-256", data); const output = Array.from(new Uint8Array(hash)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); document.getElementById("hash-output").textContent = output; } toolColor() { if (!this.state.hasSearched) { this.state.hasSearched = true; this.heroEl.classList.add("compact"); this.mainEl.style.display = "grid"; this.statsBar.classList.add("visible"); } this.resultsEl.innerHTML = `
COLOR
`; const colorInput = document.getElementById("color-input"); const colorOutput = document.getElementById("color-output"); const updateColor = () => { const hex = colorInput.value; const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); colorOutput.innerHTML = `HEX: ${hex}
RGB: ${r}, ${g}, ${b}
HSL: ${Utils.rgbToHsl(r, g, b)}`; }; colorInput.addEventListener("input", updateColor); updateColor(); document.getElementById("results-label").textContent = "COLOR"; } toolPassword() { const password = Utils.generatePassword(); Utils.copy(password); Toast.show(`PASSWORD: ${password.substring(0, 10)}...`); } toolLoremIpsum() { const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; Utils.copy(lorem); Toast.show("LOREM IPSUM COPIED"); } } // ═══════════════════════════════════════════════════════════ // GLOBAL INSTANCE AND INITIALIZATION // ═══════════════════════════════════════════════════════════ // Create global instance const searchEngine = new Search(); // Expose methods to global scope for onclick handlers window.toggleTheme = () => searchEngine.toggleTheme(); window.toggleWidget = (id) => searchEngine.toggleWidget(id); window.toggleSource = (key) => searchEngine.toggleSource(key); window.insertBang = (bang) => searchEngine.insertBang(bang); window.searchFor = (query) => searchEngine.searchFor(query); window.removeTicker = (event, ticker) => searchEngine.removeTicker(event, ticker); // Tool functions window.toolUUID = () => searchEngine.toolUUID(); window.toolTimer = () => searchEngine.toolTimer(); window.toolBase64 = () => searchEngine.toolBase64(); window.toolJSON = () => searchEngine.toolJSON(); window.toolHash = () => searchEngine.toolHash(); window.toolColor = () => searchEngine.toolColor(); window.toolPassword = () => searchEngine.toolPassword(); window.toolLoremIpsum = () => searchEngine.toolLoremIpsum(); // Timer functions window.startTimer = () => searchEngine.startTimer(); window.stopTimer = () => searchEngine.stopTimer(); window.resetTimer = () => searchEngine.resetTimer(); // JSON functions window.formatJSON = () => searchEngine.formatJSON(); window.minifyJSON = () => searchEngine.minifyJSON(); // Hash function window.genHash = () => searchEngine.genHash(); // Copy function window.copy = (text) => Utils.copy(text);