aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.js')
-rw-r--r--src/main.js2633
1 files changed, 1688 insertions, 945 deletions
diff --git a/src/main.js b/src/main.js
index ab8b0b0..4db4edb 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,1032 +1,1775 @@
// ═══════════════════════════════════════════════════════════
-// SOURCES
+// UTILS CLASS
// ═══════════════════════════════════════════════════════════
-const SOURCES = {
- dictionary: {
- bang: "!d",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`,
- );
- if (!r.ok) return [];
- const d = await r.json();
- return d.slice(0, 3).flatMap((e) =>
- e.meanings.slice(0, 2).map((m) => ({
- meta: { phonetic: e.phonetic },
- snippet: m.definitions[0]?.definition || "",
- source: "dictionary",
- title: `${e.word} [${m.partOfSpeech}]`,
- url: e.sourceUrls?.[0] || "#",
- })),
- );
- },
- name: "DICTIONARY",
- },
- github: {
- bang: "!g",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`,
- );
- const d = await r.json();
- return (d.items || []).map((i) => ({
- meta: { forks: i.forks_count, lang: i.language, stars: i.stargazers_count },
- snippet: i.description || "NO DESCRIPTION",
- source: "github",
- title: i.full_name,
- url: i.html_url,
- }));
- },
- name: "GITHUB",
- },
- hackernews: {
- bang: "!hn",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`,
- );
- const d = await r.json();
- return (d.hits || [])
- .filter((h) => h.title)
- .map((h) => ({
- meta: { comments: h.num_comments, points: h.points },
- snippet: `${h.points || 0} POINTS // ${h.author}`,
- source: "hackernews",
- title: h.title,
- url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
- }));
- },
- name: "HACKERNEWS",
- },
- news: {
- bang: "!n",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- // Combine HN, Reddit News, and Wiki Current Events logic
- const p1 = fetch(
- `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`,
- )
- .then((r) => r.json())
- .catch(() => ({ hits: [] }));
- const p2 = fetch(
- `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`,
- )
- .then((r) => r.json())
- .catch(() => ({ data: { children: [] } }));
-
- const [hn, rd] = await Promise.all([p1, p2]);
-
- const hnRes = (hn.hits || []).map((h) => ({
- meta: { date: h.created_at_i, lang: "HN" },
- snippet: `HACKERNEWS // ${h.points || 0} PTS // ${timeAgo(h.created_at_i * 1000)}`,
- source: "news",
- title: h.title,
- url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
- }));
-
- const rdRes = (rd.data?.children || []).map((c) => ({
- meta: { date: c.data.created_utc, lang: "RD" },
- snippet: `REDDIT r/${c.data.subreddit.toUpperCase()} // ${c.data.score} UPVOTES`,
- source: "news",
- title: c.data.title,
- url: `https://reddit.com${c.data.permalink}`,
- }));
-
- // Interleave/Sort by recency
- return [...hnRes, ...rdRes].sort((a, b) => b.meta.date - a.meta.date);
- },
- name: "AGGREGATE NEWS",
- },
- npm: {
- bang: "!npm",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`,
- );
- const d = await r.json();
- return (d.objects || []).map((o) => ({
- meta: { version: o.package.version },
- snippet: o.package.description || "NO DESCRIPTION",
- source: "npm",
- title: o.package.name,
- url: `https://npmjs.com/package/${o.package.name}`,
- }));
- },
- name: "NPM",
- },
- openlibrary: {
- bang: "!b",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`,
- );
- const d = await r.json();
- return (d.docs || []).map((b) => ({
- meta: { year: b.first_publish_year },
- snippet: `BY ${(b.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`,
- source: "openlibrary",
- title: b.title,
- url: `https://openlibrary.org${b.key}`,
- }));
- },
- name: "OPENLIBRARY",
- },
- reddit: {
- bang: "!r",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`,
- );
- const d = await r.json();
- return (d.data?.children || []).map((c) => ({
- meta: { comments: c.data.num_comments, score: c.data.score },
- snippet: c.data.selftext?.substring(0, 200) || `r/${c.data.subreddit}`,
- source: "reddit",
- title: c.data.title,
- url: `https://reddit.com${c.data.permalink}`,
- }));
- },
- name: "REDDIT",
- },
- stackoverflow: {
- bang: "!so",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`,
- );
- const d = await r.json();
- return (d.items || []).map((i) => ({
- meta: { answers: i.answer_count, score: i.score },
- snippet: `${i.answer_count} ANSWERS // ${i.view_count} VIEWS`,
- source: "stackoverflow",
- title: decodeHTML(i.title),
- url: i.link,
- }));
- },
- name: "STACKOVERFLOW",
- },
- wikipedia: {
- bang: "!w",
- count: 0,
- enabled: true,
- fetch: async (q) => {
- const r = await fetch(
- `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`,
- );
- const d = await r.json();
- return (d.query?.search || []).map((i) => ({
- meta: { words: i.wordcount },
- snippet: i.snippet.replace(/<[^>]*>/g, ""),
- source: "wikipedia",
- title: i.title,
- url: `https://en.wikipedia.org/wiki/${encodeURIComponent(i.title)}`,
- }));
- },
- instant: async (q) => {
- const s = await fetch(
- `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`,
- );
- const sd = await s.json();
- if (!sd.query?.search?.length) return null;
- const title = sd.query.search[0].title;
- const r = await fetch(
- `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`,
- );
- const d = await r.json();
- const page = Object.values(d.query.pages)[0];
- return {
- content: page.extract,
- source: "WIKIPEDIA",
- title: page.title,
- url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`,
- };
- },
- name: "WIKIPEDIA",
- },
-};
-
-const STOCK_EXCHANGES = {
- 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",
-};
-
-function getSymbol(ticker) {
- const t = ticker.toUpperCase();
- const ex = STOCK_EXCHANGES[t];
- if (ex === "CRYPTO") return `BINANCE:${t}USDT`;
- if (ex === "FX") return `FX:${t}`;
- if (ex) return `${ex}:${t}`;
- return `NASDAQ:${t}`;
-}
-// ═══════════════════════════════════════════════════════════
-// STATE
-// ═══════════════════════════════════════════════════════════
-const state = {
- calcExpr: "",
- calcResult: "0",
- hasSearched: false,
- history: JSON.parse(localStorage.getItem("sh") || "[]"),
- query: "",
- results: [],
- watchlist: JSON.parse(localStorage.getItem("sw") || '["TSLA", "AAPL", "BTC", "ETH"]'),
-};
-
-const $ = (id) => document.getElementById(id);
-const searchInput = $("search-input");
-const resultsEl = $("results");
-const heroEl = $("hero");
-const mainEl = $("main-content");
-const statsBar = $("stats-bar");
+/**
+ * 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 = {
+ "'": "&#39;",
+ '"': "&quot;",
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ };
+ return s.replace(/[&<>"']/g, (ch) => map[ch]);
+ }
-// ═══════════════════════════════════════════════════════════
-// INIT
-// ═══════════════════════════════════════════════════════════
-function init() {
- renderSources();
- renderHistory();
- renderWatchlist();
- initTheme();
- updateClocks();
- setInterval(updateClocks, 1000);
- setupCalc();
- setupListeners();
- setupWatchlistListeners();
- handleUrlQueryOnLoad();
-}
+ /**
+ * 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;
+ }
-function renderSources() {
- $("sources-list").innerHTML = Object.entries(SOURCES)
- .map(
- ([k, s]) => `
- <div class="source-item ${s.enabled ? "active" : ""}" data-source="${k}" onclick="toggleSource('${k}')">
- ${s.name} <span class="source-count" id="c-${k}">${s.count}</span>
- </div>
- `,
- )
- .join("");
-}
+ /**
+ * 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";
+ }
-function toggleSource(k) {
- SOURCES[k].enabled = !SOURCES[k].enabled;
- renderSources();
- if (state.query) search(state.query);
-}
+ /**
+ * 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, "<mark>$1</mark>");
+ });
+ return result;
+ }
-function toggleWidget(id) {
- $(id).classList.toggle("open");
+ /**
+ * 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;
+ }
}
// ═══════════════════════════════════════════════════════════
-// THEME
+// TOAST CLASS
// ═══════════════════════════════════════════════════════════
-function initTheme() {
- const t = localStorage.getItem("theme");
- if (t === "light") document.body.classList.add("light-mode");
-}
-function toggleTheme() {
- document.body.classList.toggle("light-mode");
- const isLight = document.body.classList.contains("light-mode");
- localStorage.setItem("theme", isLight ? "light" : "dark");
+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);
+ }
}
// ═══════════════════════════════════════════════════════════
-// WATCHLIST
+// TRADINGVIEW WIDGET CLASS
// ═══════════════════════════════════════════════════════════
-function renderWatchlist() {
- const c = $("watchlist-container");
- if (!state.watchlist.length) {
- c.innerHTML = '<div style="color:var(--gray);font-size:11px;padding:10px;">EMPTY</div>';
- return;
- }
- c.innerHTML = state.watchlist
- .map((t) => {
- const ex = STOCK_EXCHANGES[t] || "STOCK";
- return `
- <div class="watchlist-item-row" onclick="searchFor('$${t}')">
- <span>${t} <span style="color:var(--gray)">${ex}</span></span>
- <span class="watchlist-del-btn" onclick="removeTicker(event, '${t}')">×</span>
- </div>`;
- })
- .join("");
-}
-function addTicker() {
- const val = $("watchlist-input").value.toUpperCase().trim();
- if (val && !state.watchlist.includes(val)) {
- state.watchlist.push(val);
- localStorage.setItem("sw", JSON.stringify(state.watchlist));
- renderWatchlist();
- $("watchlist-input").value = "";
+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<void>}
+ */
+ 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;
+ }
}
-}
-window.removeTicker = (e, t) => {
- e.stopPropagation(); // Prevent search trigger
- state.watchlist = state.watchlist.filter((i) => i !== t);
- localStorage.setItem("sw", JSON.stringify(state.watchlist));
- renderWatchlist();
-};
-
-function setupWatchlistListeners() {
- $("watchlist-add-btn").onclick = addTicker;
- $("watchlist-input").addEventListener("keydown", (e) => {
- if (e.key === "Enter") addTicker();
- });
+ /**
+ * Update widget symbol
+ * @param {string} symbol - New symbol
+ */
+ updateSymbol(symbol) {
+ this.symbol = symbol;
+ if (this.widget) {
+ this.widget.chart().setSymbol(symbol);
+ }
+ }
}
// ═══════════════════════════════════════════════════════════
-// SEARCH
+// SEARCH CLASS
// ═══════════════════════════════════════════════════════════
-function updateUrlWithQuery(query) {
- const url = new URL(window.location);
- if (query && query.trim()) {
- url.searchParams.set("q", query);
- } else {
- url.searchParams.delete("q");
- }
- // Use replaceState to avoid adding to browser history
- window.history.replaceState({}, "", url);
-}
-function getQueryFromUrl() {
- const params = new URLSearchParams(window.location.search);
- return params.get("q") || "";
-}
+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"]'),
+ };
-function handleUrlQueryOnLoad() {
- const query = getQueryFromUrl();
- if (query.trim()) {
- // Set the search input value
- searchInput.value = query;
- // Trigger search
- search(query);
+ // 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();
}
-}
-async function search(query) {
- if (!query.trim()) {
- updateUrlWithQuery("");
- return goHome();
- }
- updateUrlWithQuery(query);
-
- // Show results view
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
- }
-
- // Stock ticker
- const stockMatch = query.match(/^\$([A-Za-z]{1,5})$/);
- if (stockMatch) {
- showStockWidget(stockMatch[1].toUpperCase());
- addHistory(query);
- return;
- }
-
- // Bangs
- const bangMatch = query.match(/^!(\w+)\s*(.*)/);
- if (bangMatch) {
- const bang = `!${bangMatch[1]}`;
- const q = bangMatch[2];
- const src = Object.entries(SOURCES).find(([_, s]) => s.bang === bang);
- if (src && q) {
- Object.keys(SOURCES).forEach((k) => (SOURCES[k].enabled = k === src[0]));
- renderSources();
- query = q;
+ /**
+ * 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]) => `
+ <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>
+ `,
+ )
+ .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 =
+ '<div style="color:var(--gray);font-size:11px;padding:10px;">EMPTY</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("");
+ }
+
+ /**
+ * 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 = "";
}
}
- // Calc
- if (query.startsWith("=")) {
- showCalcResult(query.slice(1));
- return;
+ /**
+ * 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();
}
- state.query = query;
- showLoading();
- addHistory(query);
+ /**
+ * 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;
+ }
+ }
- const start = performance.now();
- const enabled = Object.entries(SOURCES).filter(([_, s]) => s.enabled);
+ // Calculator
+ if (trimmedQuery.startsWith("=")) {
+ this.showCalcResult(trimmedQuery.slice(1));
+ return;
+ }
- fetchInstant(query);
+ 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 [];
+ }
+ });
- const promises = enabled.map(async ([k, s]) => {
try {
- const r = await s.fetch(query);
- SOURCES[k].count = r.length;
- $(`c-${k}`).textContent = r.length;
- return r;
- } catch (e) {
- return [];
+ 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");
+ }
}
- });
+ }
- const all = await Promise.all(promises);
- state.results = all.flat();
- const elapsed = Math.round(performance.now() - start);
+ /**
+ * 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;
+ }
- $("stat-results").textContent = state.results.length;
- $("stat-sources").textContent = enabled.length;
- $("stat-time").textContent = `${elapsed}MS`;
- $("results-label").textContent = `${state.results.length} RESULTS`;
- $("results-info").textContent = `${elapsed}MS // ${enabled.length} SOURCES`;
+ /**
+ * 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 = `
+ <div class="stock-widget">
+ <div class="stock-header">
+ <div class="stock-info">
+ <h2>${ticker}</h2>
+ <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>
+ </div>
+ </div>
+ <div class="tradingview-widget-container" id="${widgetId}"></div>
+ </div>
+ `;
- renderResults();
-}
+ document.getElementById("results-label").textContent = `STOCK: ${ticker}`;
+ document.getElementById("results-info").textContent = symbol;
-function goHome() {
- state.hasSearched = false;
- heroEl.classList.remove("compact");
- mainEl.style.display = "none";
- statsBar.classList.remove("visible");
- searchInput.value = "";
- updateUrlWithQuery("");
-}
+ const tvWidget = new TradingViewWidget(widgetId, symbol, isLight);
+ tvWidget.load();
+ }
-// ═══════════════════════════════════════════════════════════
-// STOCK WIDGET
-// ═══════════════════════════════════════════════════════════
-function showStockWidget(ticker) {
- const symbol = getSymbol(ticker);
- const widgetId = `tv_${Math.random().toString(36).substr(2, 9)}`;
- const isLight = document.body.classList.contains("light-mode");
-
- resultsEl.innerHTML = `
- <div class="stock-widget">
- <div class="stock-header">
- <div class="stock-info">
- <h2>${ticker}</h2>
- <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="copy('${symbol}')">COPY</button>
+ /**
+ * 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 = `
+ <div class="instant-label">INSTANT ANSWER</div>
+ <div class="instant-title">${Utils.esc(data.title)}</div>
+ <div class="instant-content">${Utils.esc(data.content)
+ .split("\n")
+ .map((p) => `<p>${p}</p>`)
+ .join("")}</div>
+ <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();
+
+ // 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>
- <div class="tradingview-widget-container" id="${widgetId}"></div>
+ `
+ : ""
+ }
</div>
`;
- $("results-label").textContent = `STOCK: ${ticker}`;
- $("results-info").textContent = symbol;
+ // Add click handler
+ resultEl.addEventListener("click", (e) => {
+ if (!e.target.closest(".result-meta")) {
+ window.open(result.url, "_blank");
+ }
+ });
- loadTradingView(widgetId, symbol, isLight);
-}
+ fragment.appendChild(resultEl);
+ });
-function loadTradingView(containerId, symbol, isLight) {
- if (!window.TradingView) {
- const s = document.createElement("script");
- s.src = "https://s3.tradingview.com/tv.js";
- s.onload = () => createChart(containerId, symbol, isLight);
- document.head.appendChild(s);
- } else {
- createChart(containerId, symbol, isLight);
+ // Batch update
+ this.resultsEl.innerHTML = "";
+ this.resultsEl.appendChild(fragment);
}
-}
-function createChart(containerId, symbol, isLight) {
- new TradingView.widget({
- allow_symbol_change: true,
- backgroundColor: isLight ? "rgba(244, 244, 245, 1)" : "rgba(0,0,0,1)",
- container_id: containerId,
- enable_publishing: false,
- gridColor: isLight ? "rgba(220, 220, 220, 1)" : "rgba(30,30,30,1)",
- height: 500,
- interval: "D",
- locale: "en",
- studies: ["Volume@tv-basicstudies"],
- style: "1",
- symbol: symbol,
- theme: isLight ? "light" : "dark",
- timezone: "Etc/UTC",
- toolbar_bg: isLight ? "#f4f4f5" : "#000",
- width: "100%",
- });
-}
+ /**
+ * 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("");
+ }
-async function fetchInstant(query) {
- if (/^[a-zA-Z]+$/.test(query) && SOURCES.dictionary.enabled) {
- try {
- const r = await fetch(
- `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}`,
- );
- if (r.ok) {
- const d = await r.json();
- if (d[0]) {
- const meanings = d[0].meanings
- .slice(0, 3)
- .map((m) => `[${m.partOfSpeech.toUpperCase()}] ${m.definitions[0]?.definition}`)
- .join("\n\n");
- showInstant({
- content: meanings,
- source: "DICTIONARY",
- title: d[0].word.toUpperCase() + (d[0].phonetic ? ` ${d[0].phonetic}` : ""),
- });
- return;
+ /**
+ * Show calculator result
+ * @param {string} expr - Mathematical expression
+ */
+ showCalcResult(expr) {
+ const result = Utils.safeEval(expr);
+ this.resultsEl.innerHTML = `
+ <div class="instant-box">
+ <div class="instant-label">CALCULATOR</div>
+ <div class="instant-title" style="font-variant-numeric:tabular-nums">
+ ${Utils.esc(expr)} = ${result}
+ </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;
}
- }
- } catch {}
+
+ document.getElementById("calc-expr").textContent = this.state.calcExpr;
+ document.getElementById("calc-result").textContent = this.state.calcResult;
+ });
+ });
}
- if (SOURCES.wikipedia.enabled) {
- const w = await SOURCES.wikipedia.instant?.(query);
- if (w) showInstant(w);
+
+ /**
+ * 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();
}
-}
-function showInstant(data) {
- const el = document.createElement("div");
- el.className = "instant-box";
- el.innerHTML = `
- <div class="instant-label">INSTANT ANSWER</div>
- <div class="instant-title">${esc(data.title)}</div>
- <div class="instant-content">${esc(data.content)
- .split("\n")
- .map((p) => `<p>${p}</p>`)
- .join("")}</div>
- <div class="instant-meta">
- <span>SOURCE: ${esc(data.source)}</span>
- ${data.url ? `<a href="${data.url}" target="_blank">VIEW FULL →</a>` : ""}
+ /**
+ * 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>
- `;
- resultsEl.innerHTML = "";
- resultsEl.appendChild(el);
-}
+ `,
+ )
+ .join("");
+ }
-function renderResults() {
- if (state.results.length === 0) {
- 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;
- }
-
- const instant = resultsEl.querySelector(".instant-box");
- resultsEl.innerHTML = "";
- if (instant) resultsEl.appendChild(instant);
-
- state.results.forEach((r, i) => {
- const el = document.createElement("div");
- el.className = "result";
- el.innerHTML = `
- <div class="result-index">${String(i + 1).padStart(2, "0")}</div>
- <div class="result-body">
- <div class="result-source">${r.source.toUpperCase()}</div>
- <div class="result-title" onclick="window.open('${esc(r.url)}','_blank')">${esc(r.title)}</div>
- <div class="result-url">${esc(r.url)}</div>
- <div class="result-snippet">${highlight(esc(r.snippet), state.query)}</div>
- ${
- r.meta
- ? `<div class="result-meta">
- ${r.meta.stars !== undefined ? `<span>★ ${fmt(r.meta.stars)}</span>` : ""}
- ${r.meta.forks !== undefined ? `<span>⑂ ${fmt(r.meta.forks)}</span>` : ""}
- ${r.meta.score !== undefined ? `<span>↑ ${fmt(r.meta.score)}</span>` : ""}
- ${r.meta.comments !== undefined ? `<span>◨ ${fmt(r.meta.comments)}</span>` : ""}
- ${r.meta.words !== undefined ? `<span>◎ ${fmt(r.meta.words)}W</span>` : ""}
- ${r.meta.lang ? `<span>${r.meta.lang.toUpperCase()}</span>` : ""}
- ${r.meta.version ? `<span>V${r.meta.version}</span>` : ""}
- </div>`
- : ""
- }
+ /**
+ * 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) => `
+ <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>
- `;
- resultsEl.appendChild(el);
- });
-}
+ `,
+ )
+ .join("");
+ }
+ }
-function showLoading() {
- resultsEl.innerHTML = Array(4)
- .fill(
- `<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></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();
+ };
+ }
-function showCalcResult(expr) {
- try {
- const result = Function(`"use strict";return (${expr.replace(/\^/g, "**")})`)();
- resultsEl.innerHTML = `<div class="instant-box"><div class="instant-label">CALCULATOR</div><div class="instant-title" style="font-variant-numeric:tabular-nums">${esc(expr)} = ${result}</div></div>`;
- } catch {
- toast("INVALID EXPRESSION");
- }
-}
+ // Voice search
+ const voiceBtn = document.getElementById("voice-btn");
+ if (voiceBtn) {
+ voiceBtn.onclick = () => this.voiceSearch();
+ }
-// ═══════════════════════════════════════════════════════════
-// CALCULATOR
-// ═══════════════════════════════════════════════════════════
-function setupCalc() {
- document.querySelectorAll(".calc-btn").forEach((btn) => {
- btn.onclick = () => {
- const v = btn.dataset.v;
- if (v === "C") {
- state.calcExpr = "";
- state.calcResult = "0";
- } else if (v === "=") {
- try {
- state.calcResult = Function(
- `"use strict";return (${state.calcExpr.replace(/\^/g, "**")})`,
- )();
- } catch {
- state.calcResult = "ERR";
+ // 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();
}
- } else {
- state.calcExpr += v;
}
- $("calc-expr").textContent = state.calcExpr;
- $("calc-result").textContent = state.calcResult;
+ });
+ }
+
+ /**
+ * 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>
+ `,
+ )
+ .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);
};
- });
-}
-// ═══════════════════════════════════════════════════════════
-// TOOLS
-// ═══════════════════════════════════════════════════════════
-function toolUUID() {
- const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
- const r = (Math.random() * 16) | 0;
- return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
- });
- copy(uuid);
- toast(`UUID: ${uuid.substring(0, 13)}...`);
-}
+ recognition.onend = () => {
+ voiceBtn.style.background = "";
+ voiceBtn.style.color = "";
+ };
-let timerInterval,
- timerSec = 0;
-function toolTimer() {
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
- }
- 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="startTimer()">START</button><button class="tool-action" onclick="stopTimer()">STOP</button><button class="tool-action" onclick="resetTimer()">RESET</button></div></div>`;
- $("results-label").textContent = "TIMER";
-}
-window.startTimer = () => {
- if (timerInterval) return;
- timerInterval = setInterval(() => {
- timerSec++;
- const h = String(Math.floor(timerSec / 3600)).padStart(2, "0");
- const m = String(Math.floor((timerSec % 3600) / 60)).padStart(2, "0");
- const s = String(timerSec % 60).padStart(2, "0");
- $("timer-display").textContent = `${h}:${m}:${s}`;
- }, 1000);
-};
-window.stopTimer = () => {
- clearInterval(timerInterval);
- timerInterval = null;
-};
-window.resetTimer = () => {
- stopTimer();
- timerSec = 0;
- $("timer-display").textContent = "00:00:00";
-};
-
-function toolBase64() {
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
- }
- 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="$('b64-output').textContent=btoa($('b64-input').value)">ENCODE</button><button class="tool-action" onclick="try{$('b64-output').textContent=atob($('b64-input').value)}catch{$('b64-output').textContent='INVALID'}">DECODE</button></div><div class="tool-output" id="b64-output">OUTPUT</div></div>`;
- $("results-label").textContent = "BASE64";
-}
+ recognition.start();
+ }
-function toolJSON() {
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
+ /**
+ * Search for specific query
+ * @param {string} query - Query to search for
+ */
+ searchFor(query) {
+ this.searchInput.value = query;
+ this.search(query);
}
- 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="formatJSON()">FORMAT</button><button class="tool-action" onclick="minifyJSON()">MINIFY</button></div><pre class="tool-output" id="json-output" style="white-space:pre-wrap;max-height:300px;overflow:auto">OUTPUT</pre></div>`;
- $("results-label").textContent = "JSON";
-}
-window.formatJSON = () => {
- try {
- $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value), null, 2);
- } catch (e) {
- $("json-output").textContent = `INVALID: ${e.message}`;
- }
-};
-window.minifyJSON = () => {
- try {
- $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value));
- } catch (e) {
- $("json-output").textContent = `INVALID: ${e.message}`;
- }
-};
-
-function toolHash() {
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
- }
- 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="genHash()">GENERATE</button></div><div class="tool-output" id="hash-output">OUTPUT</div></div>`;
- $("results-label").textContent = "HASH";
-}
-window.genHash = async () => {
- const data = new TextEncoder().encode($("hash-input").value);
- const hash = await crypto.subtle.digest("SHA-256", data);
- $("hash-output").textContent = Array.from(new Uint8Array(hash))
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("");
-};
-
-function toolColor() {
- if (!state.hasSearched) {
- state.hasSearched = true;
- heroEl.classList.add("compact");
- mainEl.style.display = "grid";
- statsBar.classList.add("visible");
- }
- 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 updateColor = () => {
- const hex = $("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);
- $("color-output").innerHTML =
- `HEX: ${hex}<br>RGB: ${r}, ${g}, ${b}<br>HSL: ${rgbToHsl(r, g, b)}`;
- };
- $("color-input").oninput = updateColor;
- updateColor();
- $("results-label").textContent = "COLOR";
-}
-function 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;
+
+ /**
+ * 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");
}
}
- return `${Math.round(h * 360)}°, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%`;
-}
-function toolPassword() {
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
- let pass = "";
- for (let i = 0; i < 20; i++) pass += chars[Math.floor(Math.random() * chars.length)];
- copy(pass);
- toast(`PASSWORD: ${pass.substring(0, 10)}...`);
-}
+ // Tool methods
+ toolUUID() {
+ const uuid = Utils.generateUUID();
+ Utils.copy(uuid);
+ Toast.show(`UUID: ${uuid.substring(0, 13)}...`);
+ }
-function 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.";
- copy(lorem);
- toast("LOREM IPSUM COPIED");
-}
+ 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";
+ }
-// ═══════════════════════════════════════════════════════════
-// HISTORY
-// ═══════════════════════════════════════════════════════════
-function addHistory(q) {
- state.history = state.history.filter((h) => h.q !== q);
- state.history.unshift({ q, t: Date.now() });
- state.history = state.history.slice(0, 20);
- localStorage.setItem("sh", JSON.stringify(state.history));
- renderHistory();
-}
+ 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);
+ }
-function renderHistory() {
- const list = $("history-list");
- if (!state.history.length) {
- list.innerHTML = '<div style="color:var(--gray);font-size:11px;">NO HISTORY</div>';
- return;
- }
- list.innerHTML = state.history
- .slice(0, 6)
- .map(
- (h) =>
- `<div class="history-item" onclick="searchFor('${esc(h.q)}')"><span>${esc(h.q)}</span><span class="history-time">${timeAgo(h.t)}</span></div>`,
- )
- .join("");
-}
+ stopTimer() {
+ clearInterval(this.timerInterval);
+ this.timerInterval = null;
+ }
-// ═══════════════════════════════════════════════════════════
-// CLOCKS
-// ═══════════════════════════════════════════════════════════
-function updateClocks() {
- const now = new Date();
- $("clock").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" },
- ];
- $("world-clocks").innerHTML = zones
- .map(
- (z) =>
- `<div class="clock"><div class="clock-time">${new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: z.tz })}</div><div class="clock-label">${z.label}</div></div>`,
- )
- .join("");
-}
+ resetTimer() {
+ this.stopTimer();
+ this.timerSec = 0;
+ const display = document.getElementById("timer-display");
+ if (display) display.textContent = "00:00:00";
+ }
-// ═══════════════════════════════════════════════════════════
-// LISTENERS
-// ═══════════════════════════════════════════════════════════
-function setupListeners() {
- let debounce;
- searchInput.addEventListener("input", (e) => {
- clearTimeout(debounce);
- debounce = setTimeout(() => search(e.target.value), 300);
- });
-
- searchInput.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- clearTimeout(debounce);
- search(searchInput.value);
+ toolBase64() {
+ if (!this.state.hasSearched) {
+ this.state.hasSearched = true;
+ this.heroEl.classList.add("compact");
+ this.mainEl.style.display = "grid";
+ this.statsBar.classList.add("visible");
}
- if (e.key === "Escape") goHome();
- });
-
- document.addEventListener("keydown", (e) => {
- if (
- e.key === "/" &&
- document.activeElement !== searchInput &&
- document.activeElement !== $("watchlist-input")
- ) {
- e.preventDefault();
- searchInput.focus();
+ 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");
}
- if (e.key === "Escape") goHome();
- });
+ 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";
+ }
- $("clear-btn").onclick = goHome;
- $("voice-btn").onclick = voiceSearch;
+ 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}`;
+ }
+ }
- // Also update URL when clearing with clear button
- $("clear-btn").onclick = () => {
- goHome();
- searchInput.focus();
- };
-}
+ 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}`;
+ }
+ }
-function voiceSearch() {
- if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) {
- toast("VOICE NOT SUPPORTED");
- return;
- }
- const Sr = window.SpeechRecognition || window.webkitSpeechRecognition;
- const r = new Sr();
- r.lang = "en-US";
- $("voice-btn").style.background = "#fff";
- $("voice-btn").style.color = "#000";
- r.onresult = (e) => {
- searchInput.value = e.results[0][0].transcript;
- search(searchInput.value);
- };
- r.onend = () => {
- $("voice-btn").style.background = "";
- $("voice-btn").style.color = "";
- };
- r.start();
-}
+ 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";
+ }
-// ═══════════════════════════════════════════════════════════
-// UTILITIES
-// ═══════════════════════════════════════════════════════════
-function esc(s) {
- if (!s) return "";
- return s.replace(
- /[&<>"']/g,
- (m) => ({ "'": "&#39;", '"': "&quot;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })[m],
- );
-}
-function decodeHTML(s) {
- const t = document.createElement("textarea");
- t.innerHTML = s;
- return t.value;
-}
-function highlight(text, query) {
- if (!query) return text;
- const words = query.split(/\s+/).filter((w) => w.length > 2);
- let result = text;
- words.forEach((w) => {
- result = result.replace(new RegExp(`(${w})`, "gi"), "<mark>$1</mark>");
- });
- return result;
-}
-function fmt(n) {
- if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
- if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
- return n?.toString() || "0";
-}
-function 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`;
-}
-function copy(text) {
- navigator.clipboard.writeText(text);
- toast("COPIED");
-}
-function toast(msg) {
- const t = document.createElement("div");
- t.className = "toast";
- t.textContent = msg;
- $("toast-container").appendChild(t);
- setTimeout(() => t.remove(), 3000);
-}
-function searchFor(q) {
- searchInput.value = q;
- search(q);
-}
-function insertBang(bang) {
- searchInput.value = `${bang} `;
- searchInput.focus();
-}
+ 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;
+ }
-window.addEventListener("popstate", (event) => {
- const query = getQueryFromUrl();
- if (query !== state.query) {
- if (query.trim()) {
- searchInput.value = query;
- search(query);
- } else {
- goHome();
+ 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";
}
-});
-// Also update the URL when using shortcuts
-window.insertBang = (bang) => {
- searchInput.value = `${bang} `;
- searchInput.focus();
- updateUrlWithQuery(`${bang} `);
-};
+ 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
+// ═══════════════════════════════════════════════════════════
-init();
+// 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);