diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-12-14 15:16:33 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-12-14 15:16:33 +0200 |
| commit | 1906216807d03447ea0594f2fc7a2a0a24979e00 (patch) | |
| tree | ddb415789c39b770dcdf16bcb5b017fe89ed8bde | |
| parent | 693a048d4aaff9f6cfcdf5093638fbe80f28f91b (diff) | |
| download | page-1906216807d03447ea0594f2fc7a2a0a24979e00.tar.zst | |
Refactor
| -rw-r--r-- | src/index.html | 98 | ||||
| -rw-r--r-- | src/main.js | 1892 |
2 files changed, 740 insertions, 1250 deletions
diff --git a/src/index.html b/src/index.html index 470050f..6af44a2 100644 --- a/src/index.html +++ b/src/index.html @@ -18,7 +18,7 @@ <div class="tagline">Universal Search Engine</div> <!-- Wrap in form for better URL handling --> - <form class="search-container" onsubmit="event.preventDefault(); search(searchInput.value);"> + <form class="search-container" id="search-form"> <div class="search-box"> <div class="search-prefix">→</div> <input @@ -34,18 +34,17 @@ <button class="action-btn" id="clear-btn" title="CLEAR" type="button">×</button> </div> </div> - <!-- Hidden submit button for accessibility --> <button type="submit" style="display: none;">Search</button> </form> <div class="shortcuts"> - <span class="shortcut" onclick="insertBang('!n')">!N NEWS</span> - <span class="shortcut" onclick="searchFor('$TSLA')">$TSLA</span> - <span class="shortcut" onclick="insertBang('!w')">!W WIKI</span> - <span class="shortcut" onclick="insertBang('!g')">!G GITHUB</span> - <span class="shortcut" onclick="insertBang('!r')">!R REDDIT</span> - <span class="shortcut" onclick="toolUUID()">UUID</span> - <span class="shortcut" onclick="toolTimer()">TIMER</span> + <span class="shortcut" data-bang="!n">!N NEWS</span> + <span class="shortcut" data-query="$TSLA">$TSLA</span> + <span class="shortcut" data-bang="!w">!W WIKI</span> + <span class="shortcut" data-bang="!g">!G GITHUB</span> + <span class="shortcut" data-bang="!r">!R REDDIT</span> + <span class="shortcut" data-tool="uuid">UUID</span> + <span class="shortcut" data-tool="timer">TIMER</span> </div> </section> @@ -71,7 +70,7 @@ <aside class="sidebar-left"> <!-- SOURCES --> <div class="widget" id="widget-sources"> - <div class="widget-header" onclick="toggleWidget('widget-sources')"> + <div class="widget-header" data-toggle="widget-sources"> <span class="widget-title">Sources</span> <span class="widget-toggle">+</span> </div> @@ -82,34 +81,34 @@ <!-- TOOLS --> <div class="widget" id="widget-tools"> - <div class="widget-header" onclick="toggleWidget('widget-tools')"> + <div class="widget-header" data-toggle="widget-tools"> <span class="widget-title">Tools</span> <span class="widget-toggle">+</span> </div> <div class="widget-body"> <div class="tools-grid"> - <button class="tool-btn" onclick="toolUUID()"> + <button class="tool-btn" data-tool="uuid"> <span class="icon">◈</span>UUID </button> - <button class="tool-btn" onclick="toolTimer()"> + <button class="tool-btn" data-tool="timer"> <span class="icon">◷</span>TIMER </button> - <button class="tool-btn" onclick="toolBase64()"> + <button class="tool-btn" data-tool="base64"> <span class="icon">◐</span>BASE64 </button> - <button class="tool-btn" onclick="toolJSON()"> + <button class="tool-btn" data-tool="json"> <span class="icon">{ }</span>JSON </button> - <button class="tool-btn" onclick="toolHash()"> + <button class="tool-btn" data-tool="hash"> <span class="icon">#</span>HASH </button> - <button class="tool-btn" onclick="toolColor()"> + <button class="tool-btn" data-tool="color"> <span class="icon">◼</span>COLOR </button> - <button class="tool-btn" onclick="toolPassword()"> + <button class="tool-btn" data-tool="password"> <span class="icon">※</span>PASSWORD </button> - <button class="tool-btn" onclick="toolLoremIpsum()"> + <button class="tool-btn" data-tool="lorem"> <span class="icon">¶</span>LOREM </button> </div> @@ -118,29 +117,29 @@ <!-- BANGS --> <div class="widget" id="widget-bangs"> - <div class="widget-header" onclick="toggleWidget('widget-bangs')"> + <div class="widget-header" data-toggle="widget-bangs"> <span class="widget-title">Bangs</span> <span class="widget-toggle">+</span> </div> <div class="widget-body"> <div class="bangs-grid"> - <span class="bang" onclick="insertBang('!n')">!N NEWS</span> - <span class="bang" onclick="insertBang('!w')">!W WIKI</span> - <span class="bang" onclick="insertBang('!g')">!G GITHUB</span> - <span class="bang" onclick="insertBang('!r')">!R REDDIT</span> - <span class="bang" onclick="insertBang('!hn')">!HN NEWS</span> - <span class="bang" onclick="insertBang('!so')">!SO STACK</span> - <span class="bang" onclick="insertBang('!npm')">!NPM</span> - <span class="bang" onclick="insertBang('!d')">!D DICT</span> - <span class="bang" onclick="insertBang('!b')">!B BOOKS</span> - <span class="bang" onclick="insertBang('$')">$ STOCK</span> + <span class="bang" data-bang="!n">!N NEWS</span> + <span class="bang" data-bang="!w">!W WIKI</span> + <span class="bang" data-bang="!g">!G GITHUB</span> + <span class="bang" data-bang="!r">!R REDDIT</span> + <span class="bang" data-bang="!hn">!HN NEWS</span> + <span class="bang" data-bang="!so">!SO STACK</span> + <span class="bang" data-bang="!npm">!NPM</span> + <span class="bang" data-bang="!d">!D DICT</span> + <span class="bang" data-bang="!b">!B BOOKS</span> + <span class="bang" data-bang="$">$ STOCK</span> </div> </div> </div> <!-- HISTORY --> <div class="widget" id="widget-history"> - <div class="widget-header" onclick="toggleWidget('widget-history')"> + <div class="widget-header" data-toggle="widget-history"> <span class="widget-title">History</span> <span class="widget-toggle">+</span> </div> @@ -165,7 +164,7 @@ <aside class="sidebar-right"> <!-- CALCULATOR --> <div class="widget" id="widget-calc"> - <div class="widget-header" onclick="toggleWidget('widget-calc')"> + <div class="widget-header" data-toggle="widget-calc"> <span class="widget-title">Calculator</span> <span class="widget-toggle">+</span> </div> @@ -174,34 +173,13 @@ <div class="calc-expr" id="calc-expr"></div> <div class="calc-result" id="calc-result">0</div> </div> - <div class="calc-grid"> - <button class="calc-btn" data-v="C">C</button> - <button class="calc-btn" data-v="(">(</button> - <button class="calc-btn" data-v=")">)</button> - <button class="calc-btn op" data-v="/">÷</button> - <button class="calc-btn" data-v="7">7</button> - <button class="calc-btn" data-v="8">8</button> - <button class="calc-btn" data-v="9">9</button> - <button class="calc-btn op" data-v="*">×</button> - <button class="calc-btn" data-v="4">4</button> - <button class="calc-btn" data-v="5">5</button> - <button class="calc-btn" data-v="6">6</button> - <button class="calc-btn op" data-v="-">−</button> - <button class="calc-btn" data-v="1">1</button> - <button class="calc-btn" data-v="2">2</button> - <button class="calc-btn" data-v="3">3</button> - <button class="calc-btn op" data-v="+">+</button> - <button class="calc-btn" data-v="0">0</button> - <button class="calc-btn" data-v=".">.</button> - <button class="calc-btn" data-v="^">^</button> - <button class="calc-btn op" data-v="=">=</button> - </div> + <div class="calc-grid" id="calc-grid"></div> </div> </div> <!-- WORLD CLOCKS --> <div class="widget" id="widget-clocks"> - <div class="widget-header" onclick="toggleWidget('widget-clocks')"> + <div class="widget-header" data-toggle="widget-clocks"> <span class="widget-title">World Time</span> <span class="widget-toggle">+</span> </div> @@ -212,7 +190,7 @@ <!-- STOCKS WATCHLIST (EDITABLE) --> <div class="widget" id="widget-watchlist"> - <div class="widget-header" onclick="toggleWidget('widget-watchlist')"> + <div class="widget-header" data-toggle="widget-watchlist"> <span class="widget-title">Watchlist</span> <span class="widget-toggle">+</span> </div> @@ -221,15 +199,13 @@ <input type="text" id="watchlist-input" placeholder="ADD TICKER..."> <button id="watchlist-add-btn">+</button> </div> - <div class="sources-list" id="watchlist-container"> - <!-- Items rendered via JS --> - </div> + <div class="sources-list" id="watchlist-container"></div> </div> </div> <!-- KEYBOARD --> <div class="widget" id="widget-keyboard"> - <div class="widget-header" onclick="toggleWidget('widget-keyboard')"> + <div class="widget-header" data-toggle="widget-keyboard"> <span class="widget-title">Shortcuts</span> <span class="widget-toggle">+</span> </div> @@ -255,7 +231,7 @@ </main> <footer class="footer"> - <button class="theme-btn" onclick="toggleTheme()">◐ Theme</button> + <button class="theme-btn" id="theme-btn">◐ Theme</button> <span>NO TRACKING</span> <span id="clock">00:00:00</span> </footer> diff --git a/src/main.js b/src/main.js index aaa9142..5667622 100644 --- a/src/main.js +++ b/src/main.js @@ -1,16 +1,7 @@ // ═══════════════════════════════════════════════════════════ // 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 = { @@ -23,33 +14,18 @@ class Utils { 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"; + 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"; @@ -58,12 +34,6 @@ class Utils { 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); @@ -75,56 +45,32 @@ class Utils { 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"); + console.error("Clipboard write failed:", err); + Toast.show("CLIPBOARD FAILED"); } } - /** - * 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" && !Number.isNaN(result) && Number.isFinite(result) ? result : "ERR"; + return typeof result === "number" && !Number.isNaN(result) && Number.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; @@ -132,13 +78,6 @@ class Utils { }); } - /** - * 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; @@ -169,49 +108,24 @@ class Utils { 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); - }; + return (...args) => { clearTimeout(timeout); - timeout = setTimeout(later, wait); + timeout = setTimeout(() => func(...args), 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; + return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join(""); } } // ═══════════════════════════════════════════════════════════ // 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") || @@ -226,7 +140,6 @@ class Toast { const toast = document.createElement("div"); toast.className = "toast"; toast.textContent = message; - container.appendChild(toast); setTimeout(() => { @@ -238,16 +151,40 @@ class Toast { } // ═══════════════════════════════════════════════════════════ -// TRADINGVIEW WIDGET CLASS +// GENERIC FETCHER // ═══════════════════════════════════════════════════════════ +class Fetcher { + constructor() { + this.abortController = null; + } + + async fetch(url, options = {}) { + this.abort(); + this.abortController = new AbortController(); + + try { + const response = await fetch(url, { + ...options, + signal: this.abortController.signal, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return await response.json(); + } catch (error) { + if (error.name === "AbortError") throw error; + console.error("Fetch failed:", error); + return null; + } + } + + abort() { + this.abortController?.abort(); + } +} +// ═══════════════════════════════════════════════════════════ +// 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; @@ -255,37 +192,25 @@ class TradingViewWidget { this.widget = null; } - /** - * Load and initialize the TradingView widget - */ - async load() { + async init() { if (!window.TradingView) { await this.loadScript(); } this.createWidget(); } - /** - * Load TradingView script - * @returns {Promise<void>} - */ loadScript() { return new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = "https://s3.tradingview.com/tv.js"; - script.onload = () => resolve(); + 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?.remove(); this.widget = new TradingView.widget({ allow_symbol_change: true, @@ -306,126 +231,105 @@ class TradingViewWidget { }); } - /** - * Remove the widget - */ destroy() { - if (this.widget) { - const container = document.getElementById(this.containerId); - if (container) { - container.innerHTML = ""; - } - this.widget = null; - } + this.widget?.remove(); + this.widget = null; } - /** - * Update widget symbol - * @param {string} symbol - New symbol - */ updateSymbol(symbol) { this.symbol = symbol; - if (this.widget) { - this.widget.chart().setSymbol(symbol); - } + this.widget?.chart()?.setSymbol?.(symbol); } } -SOURCES = { +// ═══════════════════════════════════════════════════════════ +// SOURCES CONFIGURATION +// ═══════════════════════════════════════════════════════════ +const 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] || "#", - })), + fetch: async (q, signal) => { + const data = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${q}`, { + signal, + }).then((r) => (r.ok ? r.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`, + fetch: async (q, signal) => { + const data = await fetch( + `https://api.github.com/search/repositories?q=${q}&per_page=8&sort=stars`, { signal }, - ); - const data = await response.json(); - return (data.items || []).map((item) => ({ + ).then((r) => r.json()); + return (data.items ?? []).map((item) => ({ meta: { forks: item.forks_count, lang: item.language, stars: item.stargazers_count }, - snippet: item.description || "NO DESCRIPTION", + 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 || []) + fetch: async (q, signal) => { + const data = await fetch(`https://hn.algolia.com/api/v1/search?query=${q}&hitsPerPage=8`, { + signal, + }).then((r) => r.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}`, + snippet: `${hit.points ?? 0} POINTS // ${hit.author}`, source: "hackernews", title: hit.title, - url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, + 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) => { + fetch: 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 }, - ) + fetch(`https://hn.algolia.com/api/v1/search_by_date?query=${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`, + `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${q}&restrict_sr=1&sort=new&limit=6`, { signal }, ) .then((r) => r.json()) .catch(() => ({ data: { children: [] } })), ]); - const hnResults = (hn.hits || []).map((hit) => ({ + 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)}`, + 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}`, + url: hit.url ?? `https://news.ycombinator.com/item?id=${hit.objectID}`, })); - const redditResults = (rd.data?.children || []).map((child) => ({ + 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", @@ -434,136 +338,123 @@ SOURCES = { })); 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) => ({ + fetch: async (q, signal) => { + const data = await fetch(`https://registry.npmjs.org/-/v1/search?text=${q}&size=8`, { + signal, + }).then((r) => r.json()); + return (data.objects ?? []).map((obj) => ({ meta: { version: obj.package.version }, - snippet: obj.package.description || "NO DESCRIPTION", + 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) => ({ + fetch: async (q, signal) => { + const data = await fetch(`https://openlibrary.org/search.json?q=${q}&limit=6`, { + signal, + }).then((r) => r.json()); + return (data.docs ?? []).map((book) => ({ meta: { year: book.first_publish_year }, - snippet: `BY ${(book.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, + 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) => ({ + fetch: async (q, signal) => { + const data = await fetch(`https://www.reddit.com/search.json?q=${q}&limit=8`, { + signal, + }).then((r) => r.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}`, + 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`, + fetch: async (q, signal) => { + const data = await fetch( + `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${q}&site=stackoverflow&pagesize=8`, { signal }, - ); - const data = await response.json(); - return (data.items || []).map((item) => ({ + ).then((r) => r.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`, + fetch: async (q, signal) => { + const data = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${q}&format=json&origin=*&srlimit=8`, { signal }, - ); - const data = await response.json(); - return (data.query?.search || []).map((item) => ({ + ).then((r) => r.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)}`, + url: `https://en.wikipedia.org/wiki/${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`, + const searchData = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${q}&format=json&origin=*&srlimit=1`, { signal }, - ); - const searchData = await searchResponse.json(); + ).then((r) => r.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=*`, + const pageData = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${title}&format=json&origin=*`, { signal }, - ); - const pageData = await pageResponse.json(); - const page = Object.values(pageData.query.pages)[0]; + ).then((r) => r.json()); + + const page = Object.values(pageData.query?.pages ?? {})[0]; + if (!page?.extract) return null; return { content: page.extract, source: "WIKIPEDIA", title: page.title, - url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, + url: `https://en.wikipedia.org/wiki/${title}`, }; }, name: "WIKIPEDIA", }, }; -// Stock exchanges mapping -STOCK_EXHANGES = { +const STOCK_EXCHANGES = { AAPL: "NASDAQ", ADA: "CRYPTO", AMD: "NASDAQ", @@ -600,83 +491,275 @@ STOCK_EXHANGES = { // ═══════════════════════════════════════════════════════════ // SEARCH CLASS // ═══════════════════════════════════════════════════════════ - class Search { - /** - * Create a new Search instance - */ constructor(sources, stockExchanges) { this.sources = sources; this.stockExchanges = stockExchanges; - // State + this.fetcher = new Fetcher(); + this.state = { calcExpr: "", calcResult: "0", hasSearched: false, - history: JSON.parse(localStorage.getItem("sh") || "[]"), + history: JSON.parse(localStorage.getItem("sh") ?? "[]"), query: "", results: [], selectedResultIndex: -1, - watchlist: JSON.parse(localStorage.getItem("sw") || '["TSLA", "AAPL", "BTC", "ETH"]'), + watchlist: JSON.parse(localStorage.getItem("sw") ?? '["TSLA", "AAPL", "BTC", "ETH"]'), }; - // Request cancellation - this.abortController = null; - - // Suggestions + this.timerSec = 0; + this.timerInterval = null; this.suggestions = []; - this.showingSuggestions = false; - // DOM Elements + this.initElements(); + this.init(); + } + + initElements() { 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"); - - 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.initEventListeners(); + this.initCalculator(); 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 - */ + // ─── Event Listeners ───────────────────────────── + initEventListeners() { + // Search + this.searchInput.addEventListener( + "input", + Utils.debounce((e) => { + this.search(e.target.value); + }, 300), + ); + + this.searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") this.search(this.searchInput.value); + if (e.key === "Escape") this.goHome(); + }); + + // Form + document.getElementById("search-form")?.addEventListener("submit", (e) => { + e.preventDefault(); + this.search(this.searchInput.value); + }); + + // Buttons + document.getElementById("clear-btn")?.addEventListener("pointerdown", () => { + this.goHome(); + this.searchInput.focus(); + }); + + document.getElementById("voice-btn")?.addEventListener("pointerdown", () => this.voiceSearch()); + document.getElementById("theme-btn")?.addEventListener("pointerdown", () => this.toggleTheme()); + + // Widget toggles + document.querySelectorAll("[data-toggle]").forEach((el) => { + el.addEventListener("pointerdown", (e) => { + e.currentTarget.closest(".widget")?.classList.toggle("open"); + }); + }); + + // Shortcuts, bangs, tools + document.addEventListener("pointerdown", (e) => { + const target = e.target; + + if (target.closest(".shortcut, .bang")) { + if (target.dataset.bang) this.insertBang(target.dataset.bang); + if (target.dataset.query) this.searchFor(target.dataset.query); + if (target.dataset.tool) this[`tool${target.dataset.tool.toUpperCase()}`]?.(); + } + + if (target.closest(".tool-btn")) { + const tool = target.closest(".tool-btn")?.dataset.tool; + if (tool) this[`tool${tool.toUpperCase()}`]?.(); + } + + if (target.closest(".source-item")) { + const source = target.closest(".source-item")?.dataset.source; + if (source) this.toggleSource(source); + } + + if (target.closest(".history-item")) { + const query = target.closest(".history-item")?.dataset.query; + if (query) this.searchFor(query); + } + + if (target.closest(".watchlist-del-btn")) { + const ticker = target.closest(".watchlist-item-row")?.dataset.ticker; + if (ticker) this.removeTicker(ticker); + } + }); + + // Watchlist + document + .getElementById("watchlist-add-btn") + ?.addEventListener("pointerdown", () => this.addTicker()); + document.getElementById("watchlist-input")?.addEventListener("keydown", (e) => { + if (e.key === "Enter") this.addTicker(); + }); + + // Global shortcuts + document.addEventListener("keydown", (e) => { + if (e.key === "/" && e.target !== this.searchInput) { + e.preventDefault(); + this.searchInput.focus(); + } + if (e.key === "Escape") this.goHome(); + }); + } + + // ─── Calculator ──────────────────────────────── + initCalculator() { + const buttons = [ + "C", + "(", + ")", + "÷", + "7", + "8", + "9", + "×", + "4", + "5", + "6", + "−", + "1", + "2", + "3", + "+", + "0", + ".", + "^", + "=", + ]; + + const grid = document.getElementById("calc-grid"); + if (!grid) return; + + grid.innerHTML = buttons + .map((value) => { + const isOp = ["÷", "×", "−", "+", "="].includes(value); + return `<button class="calc-btn ${isOp ? "op" : ""}" data-v="${value}">${value}</button>`; + }) + .join(""); + + grid.addEventListener("pointerdown", (e) => { + const value = e.target.dataset.v; + if (!value) return; + + if (value === "C") { + this.state.calcExpr = ""; + this.state.calcResult = "0"; + } else if (value === "=") { + this.state.calcResult = Utils.safeEval(this.state.calcExpr); + } else { + this.state.calcExpr += value; + } + + document.getElementById("calc-expr").textContent = this.state.calcExpr; + document.getElementById("calc-result").textContent = this.state.calcResult; + }); + } + + // ─── Search ───────────────────────────────────── + async search(query) { + const trimmed = query.trim(); + if (!trimmed) return this.goHome(); + + this.fetcher.abort(); + this.state.query = trimmed; + this.updateUrlWithQuery(trimmed); + this.ensureResultsView(); + this.addHistory(trimmed); + this.showLoading(); + + // Special queries + const stockMatch = trimmed.match(/^\$([A-Za-z]{1,5})$/); + if (stockMatch) return this.showStockWidget(stockMatch[1].toUpperCase()); + + if (trimmed.startsWith("=")) return this.showCalcResult(trimmed.slice(1)); + + // Bang handling + const bangMatch = trimmed.match(/^!(\w+)\s*(.*)/); + if (bangMatch) { + const bang = `!${bangMatch[1]}`; + const source = Object.entries(this.sources).find(([, s]) => s.bang === bang); + if (source) { + Object.keys(this.sources).forEach((k) => { + this.sources[k].enabled = k === source[0]; + }); + this.renderSources(); + } + } + + const startTime = performance.now(); + const enabledSources = Object.entries(this.sources).filter(([, s]) => s.enabled); + + // Fetch instant results + this.fetchInstant(trimmed); + + try { + const results = await Promise.all( + enabledSources.map(async ([key, source]) => { + try { + const data = await source.fetch( + encodeURIComponent(trimmed), + this.fetcher.abortController?.signal, + ); + this.updateSourceCount(key, data?.length ?? 0); + return data ?? []; + } catch (error) { + if (error.name !== "AbortError") console.error(`Source ${key} failed:`, error); + return []; + } + }), + ); + + this.state.results = results.flat(); + this.updateStats(startTime, enabledSources.length); + this.renderResults(); + } catch (error) { + if (error.name !== "AbortError") Toast.show("SEARCH FAILED"); + } + } + + // ─── UI Updates ──────────────────────────────── + ensureResultsView() { + if (this.state.hasSearched) return; + this.state.hasSearched = true; + this.heroEl.classList.add("compact"); + this.mainEl.style.display = "grid"; + this.statsBar.classList.add("visible"); + } + + updateStats(startTime, sourceCount) { + const elapsed = Math.round(performance.now() - startTime); + document.getElementById("stat-results").textContent = this.state.results.length; + document.getElementById("stat-sources").textContent = sourceCount; + document.getElementById("stat-time").textContent = `${elapsed}MS`; + document.getElementById("results-label").textContent = `${this.state.results.length} RESULTS`; + document.getElementById("results-info").textContent = `${elapsed}MS // ${sourceCount} SOURCES`; + } + + updateSourceCount(key, count) { + const countEl = document.getElementById(`c-${key}`); + if (countEl) countEl.textContent = count; + } + + // ─── Rendering ──────────────────────────────── renderSources() { const sourcesList = document.getElementById("sources-list"); if (!sourcesList) return; @@ -684,83 +767,290 @@ class Search { sourcesList.innerHTML = Object.entries(this.sources) .map( ([key, source]) => ` - <div class="source-item ${source.enabled ? "active" : ""}" - data-source="${key}" - onclick="searchEngine.toggleSource('${key}')"> - ${source.name} <span class="source-count" id="c-${key}">${source.count}</span> - </div> - `, + <div class="source-item ${source.enabled ? "active" : ""}" data-source="${key}"> + ${source.name} <span class="source-count" id="c-${key}">0</span> + </div> + `, ) .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"); - } - } + renderHistory() { + const list = document.getElementById("history-list"); + if (!list) return; - /** - * 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"); + list.innerHTML = + this.state.history + .slice(0, 6) + .map( + (item) => ` + <div class="history-item" data-query="${Utils.esc(item.q)}"> + <span>${Utils.esc(item.q)}</span> + <span class="history-time">${Utils.timeAgo(item.t)}</span> + </div> + `, + ) + .join("") || '<div style="color:var(--gray);font-size:11px;">NO HISTORY</div>'; } - /** - * Render watchlist - */ renderWatchlist() { const container = document.getElementById("watchlist-container"); if (!container) return; - if (!this.state.watchlist.length) { - container.innerHTML = - '<div style="color:var(--gray);font-size:11px;padding:10px;">EMPTY</div>'; + container.innerHTML = + this.state.watchlist + .map( + (ticker) => ` + <div class="watchlist-item-row" data-ticker="${ticker}"> + <span>${ticker} <span style="color:var(--gray)">${this.stockExchanges[ticker] ?? "STOCK"}</span></span> + <span class="watchlist-del-btn">×</span> + </div> + `, + ) + .join("") || '<div style="color:var(--gray);font-size:11px;">EMPTY</div>'; + } + + renderResults() { + if (!this.state.results.length) { + this.resultsEl.innerHTML = ` + <div class="empty-state"> + <div class="empty-title">NO RESULTS</div> + <div class="empty-text">TRY DIFFERENT KEYWORDS OR ENABLE MORE SOURCES</div> + </div>`; return; } - container.innerHTML = this.state.watchlist - .map((ticker) => { - const exchange = this.stockExchanges[ticker] || "STOCK"; - return ` - <div class="watchlist-item-row" onclick="searchEngine.searchFor('$${ticker}')"> - <span>${ticker} <span style="color:var(--gray)">${exchange}</span></span> - <span class="watchlist-del-btn" onclick="searchEngine.removeTicker(event, '${ticker}')">×</span> - </div> - `; - }) - .join(""); + const fragment = document.createDocumentFragment(); + this.state.results.forEach((result, index) => { + const div = document.createElement("div"); + div.className = "result"; + div.innerHTML = ` + <div class="result-index">${String(index + 1).padStart(2, "0")}</div> + <div class="result-body"> + <div class="result-source">${result.source.toUpperCase()}</div> + <div class="result-title">${Utils.esc(result.title)}</div> + <div class="result-url">${Utils.esc(result.url)}</div> + <div class="result-snippet">${Utils.highlight(Utils.esc(result.snippet), this.state.query)}</div> + ${ + result.meta + ? ` + <div class="result-meta"> + ${result.meta.stars !== undefined ? `<span>★ ${Utils.fmt(result.meta.stars)}</span>` : ""} + ${result.meta.forks !== undefined ? `<span>⑂ ${Utils.fmt(result.meta.forks)}</span>` : ""} + ${result.meta.score !== undefined ? `<span>↑ ${Utils.fmt(result.meta.score)}</span>` : ""} + ${result.meta.comments !== undefined ? `<span>◨ ${Utils.fmt(result.meta.comments)}</span>` : ""} + ${result.meta.words !== undefined ? `<span>◎ ${Utils.fmt(result.meta.words)}W</span>` : ""} + ${result.meta.lang ? `<span>${result.meta.lang.toUpperCase()}</span>` : ""} + ${result.meta.version ? `<span>V${result.meta.version}</span>` : ""} + </div>` + : "" + } + </div>`; + div.addEventListener("pointerdown", () => window.open(result.url, "_blank")); + fragment.appendChild(div); + }); + + this.resultsEl.innerHTML = ""; + this.resultsEl.appendChild(fragment); + } + + // ─── Tools ──────────────────────────────────── + showToolModal(title, content) { + this.ensureResultsView(); + this.resultsEl.innerHTML = ` + <div class="tool-modal"> + <div class="instant-label">${title}</div> + ${content} + </div>`; + document.getElementById("results-label").textContent = title; + } + + toolUUID() { + const uuid = Utils.generateUUID(); + Utils.copy(uuid); + Toast.show(`UUID: ${uuid.substring(0, 13)}...`); + } + + toolTIMER() { + this.showToolModal( + "TIMER", + ` + <div class="instant-title" id="timer-display" style="font-variant-numeric:tabular-nums">00:00:00</div> + <div class="tool-btn-row" style="margin-top:20px"> + <button class="tool-action" id="timer-start">START</button> + <button class="tool-action" id="timer-stop">STOP</button> + <button class="tool-action" id="timer-reset">RESET</button> + </div>`, + ); + + document.getElementById("timer-start").addEventListener("pointerdown", () => this.startTimer()); + document.getElementById("timer-stop").addEventListener("pointerdown", () => this.stopTimer()); + document.getElementById("timer-reset").addEventListener("pointerdown", () => this.resetTimer()); + } + + toolBASE64() { + this.showToolModal( + "BASE64", + ` + <textarea id="b64-input" placeholder="ENTER TEXT..."></textarea> + <div class="tool-btn-row"> + <button class="tool-action" id="b64-encode">ENCODE</button> + <button class="tool-action" id="b64-decode">DECODE</button> + </div> + <div class="tool-output" id="b64-output">OUTPUT</div>`, + ); + + document.getElementById("b64-encode").addEventListener("pointerdown", () => { + document.getElementById("b64-output").textContent = btoa( + document.getElementById("b64-input").value, + ); + }); + + document.getElementById("b64-decode").addEventListener("pointerdown", () => { + try { + document.getElementById("b64-output").textContent = atob( + document.getElementById("b64-input").value, + ); + } catch { + document.getElementById("b64-output").textContent = "INVALID"; + } + }); + } + + toolJSON() { + this.showToolModal( + "JSON", + ` + <textarea id="json-input" placeholder="PASTE JSON..."></textarea> + <div class="tool-btn-row"> + <button class="tool-action" id="json-format">FORMAT</button> + <button class="tool-action" id="json-minify">MINIFY</button> + </div> + <pre class="tool-output" id="json-output" style="white-space:pre-wrap;max-height:300px;overflow:auto">OUTPUT</pre>`, + ); + + document.getElementById("json-format").addEventListener("pointerdown", () => { + try { + const input = document.getElementById("json-input").value; + document.getElementById("json-output").textContent = JSON.stringify( + JSON.parse(input), + null, + 2, + ); + } catch (e) { + document.getElementById("json-output").textContent = `INVALID: ${e.message}`; + } + }); + + document.getElementById("json-minify").addEventListener("pointerdown", () => { + try { + const input = document.getElementById("json-input").value; + document.getElementById("json-output").textContent = JSON.stringify(JSON.parse(input)); + } catch (e) { + document.getElementById("json-output").textContent = `INVALID: ${e.message}`; + } + }); + } + + toolHASH() { + this.showToolModal( + "SHA-256 HASH", + ` + <textarea id="hash-input" placeholder="ENTER TEXT..."></textarea> + <div class="tool-btn-row"> + <button class="tool-action" id="hash-generate">GENERATE</button> + </div> + <div class="tool-output" id="hash-output">OUTPUT</div>`, + ); + + document.getElementById("hash-generate").addEventListener("pointerdown", async () => { + 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() { + this.showToolModal( + "COLOR", + ` + <input type="color" id="color-input" value="#ffffff" style="width:100%;height:100px;border:3px solid var(--fg);background:var(--bg);cursor:crosshair;"> + <div class="tool-output" id="color-output" style="margin-top:20px"></div>`, + ); + + const updateColor = () => { + const hex = document.getElementById("color-input").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); + document.getElementById("color-output").innerHTML = + `HEX: ${hex}<br>RGB: ${r}, ${g}, ${b}<br>HSL: ${Utils.rgbToHsl(r, g, b)}`; + }; + + document.getElementById("color-input").addEventListener("input", updateColor); + updateColor(); + } + + toolPASSWORD() { + Utils.copy(Utils.generatePassword()); + Toast.show("PASSWORD COPIED"); + } + + toolLOREM() { + const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."; + Utils.copy(lorem); + Toast.show("LOREM IPSUM COPIED"); + } + + // ─── Timer Methods ──────────────────────────── + startTimer() { + if (this.timerInterval) return; + 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 timer = document.getElementById("timer-display"); + if (timer !== null) { + timer.textContent = "00:00:00"; + } + } + + // ─── Helper Methods ─────────────────────────── + toggleSource(key) { + this.sources[key].enabled = !this.sources[key].enabled; + this.renderSources(); + if (this.state.query) this.search(this.state.query); + } + + toggleTheme() { + document.body.classList.toggle("light-mode"); + localStorage.setItem( + "theme", + document.body.classList.contains("light-mode") ? "light" : "dark", + ); } - /** - * Add ticker to watchlist - */ addTicker() { const input = document.getElementById("watchlist-input"); - if (!input) return; - - const ticker = input.value.toUpperCase().trim(); + 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)); @@ -769,195 +1059,34 @@ class Search { } } - /** - * Remove ticker from watchlist - * @param {Event} event - Click event - * @param {string} ticker - Ticker to remove - */ - removeTicker(event, ticker) { - event.stopPropagation(); + removeTicker(ticker) { 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}`; + return exchange ? `${exchange}:${t}` : `NASDAQ:${t}`; } - /** - * Update URL with search query - * @param {string} query - Search query - */ updateUrlWithQuery(query) { const url = new URL(window.location); - if (query?.trim()) { - url.searchParams.set("q", query); - } else { - url.searchParams.delete("q"); - } + query?.trim() ? url.searchParams.set("q", query) : 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()) { + const query = new URLSearchParams(window.location.search).get("q"); + 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"); @@ -965,17 +1094,20 @@ class Search { this.statsBar.classList.remove("visible"); this.searchInput.value = ""; this.updateUrlWithQuery(""); - this.hideSuggestions(); this.state.selectedResultIndex = -1; } - /** - * Show stock widget - * @param {string} ticker - Stock ticker - */ + showLoading() { + this.resultsEl.innerHTML = Array(4) + .fill( + '<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></div>', + ) + .join(""); + } + showStockWidget(ticker) { const symbol = this.getSymbol(ticker); - const widgetId = `tv_${Math.random().toString(36).substr(2, 9)}`; + const widgetId = `tv_${Math.random().toString(36).slice(2, 11)}`; const isLight = document.body.classList.contains("light-mode"); this.resultsEl.innerHTML = ` @@ -986,78 +1118,62 @@ class Search { <div class="ticker">${symbol}</div> </div> <div class="stock-actions-top"> - <button class="stock-btn" onclick="window.open('https://www.tradingview.com/symbols/${symbol}/','_blank')">TRADINGVIEW ↗</button> - <button class="stock-btn" onclick="Utils.copy('${symbol}')">COPY</button> + <button class="stock-btn" id="tv-open">TRADINGVIEW ↗</button> + <button class="stock-btn" id="tv-copy">COPY</button> </div> </div> <div class="tradingview-widget-container" id="${widgetId}"></div> - </div> - `; + </div>`; - document.getElementById("results-label").textContent = `STOCK: ${ticker}`; - document.getElementById("results-info").textContent = symbol; + document + .getElementById("tv-open") + .addEventListener("pointerdown", () => + window.open(`https://www.tradingview.com/symbols/${symbol}/`, "_blank"), + ); + document.getElementById("tv-copy").addEventListener("pointerdown", () => Utils.copy(symbol)); - const tvWidget = new TradingViewWidget(widgetId, symbol, isLight); - tvWidget.load(); + new TradingViewWidget(widgetId, symbol, isLight).init(); } - /** - * 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 }, + const data = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${query}`).then( + (r) => (r.ok ? r.json() : []), ); - 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; - } + 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); - } + 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); - } + const instantResult = await this.sources.wikipedia.instant(query); + if (instantResult) this.showInstant(instantResult); } catch (error) { - if (error.name !== "AbortError") { - console.error("Wikipedia instant fetch failed:", error); - } + console.error("Wikipedia instant fetch failed:", error); } } } - /** - * Show instant answer - * @param {Object} data - Instant answer data - */ showInstant(data) { + const existing = this.resultsEl.querySelector(".instant-box"); + existing?.remove(); + const instantBox = document.createElement("div"); instantBox.className = "instant-box"; instantBox.innerHTML = ` @@ -1070,106 +1186,11 @@ class Search { <div class="instant-meta"> <span>SOURCE: ${Utils.esc(data.source)}</span> ${data.url ? `<a href="${data.url}" target="_blank">VIEW FULL →</a>` : ""} - </div> - `; - - // 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 = ` - <div class="empty-state"> - <div class="empty-title">NO RESULTS</div> - <div class="empty-text">TRY DIFFERENT KEYWORDS OR ENABLE MORE SOURCES</div> - </div> - `; - return; - } - - // Use DocumentFragment for batch DOM updates - const fragment = document.createDocumentFragment(); + </div>`; - // 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 = ` - <div class="result-index">${String(index + 1).padStart(2, "0")}</div> - <div class="result-body"> - <div class="result-source">${result.source.toUpperCase()}</div> - <div class="result-title">${Utils.esc(result.title)}</div> - <div class="result-url">${Utils.esc(result.url)}</div> - <div class="result-snippet">${Utils.highlight(Utils.esc(result.snippet), this.state.query)}</div> - ${ - result.meta - ? ` - <div class="result-meta"> - ${result.meta.stars !== undefined ? `<span>★ ${Utils.fmt(result.meta.stars)}</span>` : ""} - ${result.meta.forks !== undefined ? `<span>⑂ ${Utils.fmt(result.meta.forks)}</span>` : ""} - ${result.meta.score !== undefined ? `<span>↑ ${Utils.fmt(result.meta.score)}</span>` : ""} - ${result.meta.comments !== undefined ? `<span>◨ ${Utils.fmt(result.meta.comments)}</span>` : ""} - ${result.meta.words !== undefined ? `<span>◎ ${Utils.fmt(result.meta.words)}W</span>` : ""} - ${result.meta.lang ? `<span>${result.meta.lang.toUpperCase()}</span>` : ""} - ${result.meta.version ? `<span>V${result.meta.version}</span>` : ""} - </div> - ` - : "" - } - </div> - `; - - // 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); + this.resultsEl.prepend(instantBox); } - /** - * Show loading state - */ - showLoading() { - this.resultsEl.innerHTML = Array(4) - .fill( - '<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></div>', - ) - .join(""); - } - - /** - * Show calculator result - * @param {string} expr - Mathematical expression - */ showCalcResult(expr) { const result = Utils.safeEval(expr); this.resultsEl.innerHTML = ` @@ -1178,41 +1199,10 @@ class Search { <div class="instant-title" style="font-variant-numeric:tabular-nums"> ${Utils.esc(expr)} = ${result} </div> - </div> - `; + </div>`; 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() }); @@ -1221,40 +1211,10 @@ class Search { this.renderHistory(); } - /** - * Render history - */ - renderHistory() { - const list = document.getElementById("history-list"); - if (!list) return; - - if (!this.state.history.length) { - list.innerHTML = '<div style="color:var(--gray);font-size:11px;">NO HISTORY</div>'; - return; - } - - list.innerHTML = this.state.history - .slice(0, 6) - .map( - (item) => ` - <div class="history-item" onclick="searchEngine.searchFor('${Utils.esc(item.q)}')"> - <span>${Utils.esc(item.q)}</span> - <span class="history-time">${Utils.timeAgo(item.t)}</span> - </div> - `, - ) - .join(""); - } - - /** - * Update clocks - */ updateClocks() { const now = new Date(); const clockEl = document.getElementById("clock"); - if (clockEl) { - clockEl.textContent = now.toLocaleTimeString("en-GB"); - } + if (clockEl) clockEl.textContent = now.toLocaleTimeString("en-GB"); const zones = [ { label: "LOCAL", tz: Intl.DateTimeFormat().resolvedOptions().timeZone }, @@ -1268,240 +1228,37 @@ class Search { clocksEl.innerHTML = zones .map( (zone) => ` - <div class="clock"> - <div class="clock-time"> - ${now.toLocaleTimeString("en-GB", { - hour: "2-digit", - minute: "2-digit", - timeZone: zone.tz, - })} - </div> - <div class="clock-label">${zone.label}</div> + <div class="clock"> + <div class="clock-time"> + ${now.toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + timeZone: zone.tz, + })} </div> - `, - ) - .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) => ` - <div class="suggestion" onclick="searchEngine.selectSuggestion('${Utils.esc(suggestion)}')"> - ${Utils.esc(suggestion)} + <div class="clock-label">${zone.label}</div> </div> `, - ) - .join(""); - - this.showingSuggestions = true; - } - - /** - * Hide suggestions dropdown - */ - hideSuggestions() { - const container = document.querySelector(".suggestions-container"); - if (container) { - container.remove(); + ) + .join(""); } - 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)) { + if (!("webkitSpeechRecognition" in window)) { Toast.show("VOICE NOT SUPPORTED"); return; } - const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; - const recognition = new SpeechRecognition(); + const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); 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; + recognition.onresult = (e) => { + const transcript = e.results[0][0].transcript; this.searchInput.value = transcript; this.search(transcript); }; @@ -1514,262 +1271,19 @@ class Search { 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 = ` - <div class="tool-modal"> - <div class="instant-label">TIMER</div> - <div class="instant-title" id="timer-display" style="font-variant-numeric:tabular-nums">00:00:00</div> - <div class="tool-btn-row" style="margin-top:20px"> - <button class="tool-action" onclick="searchEngine.startTimer()">START</button> - <button class="tool-action" onclick="searchEngine.stopTimer()">STOP</button> - <button class="tool-action" onclick="searchEngine.resetTimer()">RESET</button> - </div> - </div> - `; - 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 = ` - <div class="tool-modal"> - <div class="instant-label">BASE64</div> - <textarea id="b64-input" placeholder="ENTER TEXT..."></textarea> - <div class="tool-btn-row"> - <button class="tool-action" onclick="document.getElementById('b64-output').textContent = btoa(document.getElementById('b64-input').value)">ENCODE</button> - <button class="tool-action" onclick="try{document.getElementById('b64-output').textContent = atob(document.getElementById('b64-input').value)}catch{e){document.getElementById('b64-output').textContent='INVALID'}">DECODE</button> - </div> - <div class="tool-output" id="b64-output">OUTPUT</div> - </div> - `; - 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 = ` - <div class="tool-modal"> - <div class="instant-label">JSON</div> - <textarea id="json-input" placeholder="PASTE JSON..."></textarea> - <div class="tool-btn-row"> - <button class="tool-action" onclick="searchEngine.formatJSON()">FORMAT</button> - <button class="tool-action" onclick="searchEngine.minifyJSON()">MINIFY</button> - </div> - <pre class="tool-output" id="json-output" style="white-space:pre-wrap;max-height:300px;overflow:auto">OUTPUT</pre> - </div> - `; - 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 = ` - <div class="tool-modal"> - <div class="instant-label">SHA-256 HASH</div> - <textarea id="hash-input" placeholder="ENTER TEXT..."></textarea> - <div class="tool-btn-row"> - <button class="tool-action" onclick="searchEngine.genHash()">GENERATE</button> - </div> - <div class="tool-output" id="hash-output">OUTPUT</div> - </div> - `; - 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 = ` - <div class="tool-modal"> - <div class="instant-label">COLOR</div> - <input type="color" id="color-input" value="#ffffff" style="width:100%;height:100px;border:3px solid var(--fg);background:var(--bg);cursor:crosshair;"> - <div class="tool-output" id="color-output" style="margin-top:20px"></div> - </div> - `; - - 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}<br>RGB: ${r}, ${g}, ${b}<br>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 +// INITIALIZATION // ═══════════════════════════════════════════════════════════ - -// Create global instance -const searchEngine = new Search(SOURCES, STOCK_EXHANGES); - -// 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); +const searchEngine = new Search(SOURCES, STOCK_EXCHANGES); |
