aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/index.html98
-rw-r--r--src/main.js1892
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);