aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.js')
-rw-r--r--src/main.js972
1 files changed, 972 insertions, 0 deletions
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..e9bc9f7
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,972 @@
+// ═══════════════════════════════════════════════════════════
+// SOURCES
+// ═══════════════════════════════════════════════════════════
+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");
+
+// ═══════════════════════════════════════════════════════════
+// INIT
+// ═══════════════════════════════════════════════════════════
+function init() {
+ renderSources();
+ renderHistory();
+ renderWatchlist();
+ initTheme();
+ updateClocks();
+ setInterval(updateClocks, 1000);
+ setupCalc();
+ setupListeners();
+ setupWatchlistListeners();
+}
+
+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("");
+}
+
+function toggleSource(k) {
+ SOURCES[k].enabled = !SOURCES[k].enabled;
+ renderSources();
+ if (state.query) search(state.query);
+}
+
+function toggleWidget(id) {
+ $(id).classList.toggle("open");
+}
+
+// ═══════════════════════════════════════════════════════════
+// THEME
+// ═══════════════════════════════════════════════════════════
+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");
+}
+
+// ═══════════════════════════════════════════════════════════
+// WATCHLIST
+// ═══════════════════════════════════════════════════════════
+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 = "";
+ }
+}
+
+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();
+ });
+}
+
+// ═══════════════════════════════════════════════════════════
+// SEARCH
+// ═══════════════════════════════════════════════════════════
+async function search(query) {
+ if (!query.trim()) return goHome();
+
+ // 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;
+ }
+ }
+
+ // Calc
+ if (query.startsWith("=")) {
+ showCalcResult(query.slice(1));
+ return;
+ }
+
+ state.query = query;
+ showLoading();
+ addHistory(query);
+
+ const start = performance.now();
+ const enabled = Object.entries(SOURCES).filter(([_, s]) => s.enabled);
+
+ fetchInstant(query);
+
+ 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 all = await Promise.all(promises);
+ state.results = all.flat();
+ const elapsed = Math.round(performance.now() - start);
+
+ $("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`;
+
+ renderResults();
+}
+
+function goHome() {
+ state.hasSearched = false;
+ heroEl.classList.remove("compact");
+ mainEl.style.display = "none";
+ statsBar.classList.remove("visible");
+ searchInput.value = "";
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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>
+ </div>
+ </div>
+ <div class="tradingview-widget-container" id="${widgetId}"></div>
+ </div>
+ `;
+
+ $("results-label").textContent = `STOCK: ${ticker}`;
+ $("results-info").textContent = symbol;
+
+ loadTradingView(widgetId, symbol, isLight);
+}
+
+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);
+ }
+}
+
+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%",
+ });
+}
+
+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;
+ }
+ }
+ } catch {}
+ }
+ if (SOURCES.wikipedia.enabled) {
+ const w = await SOURCES.wikipedia.instant?.(query);
+ if (w) showInstant(w);
+ }
+}
+
+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>` : ""}
+ </div>
+ `;
+ resultsEl.innerHTML = "";
+ resultsEl.appendChild(el);
+}
+
+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>`
+ : ""
+ }
+ </div>
+ `;
+ resultsEl.appendChild(el);
+ });
+}
+
+function showLoading() {
+ resultsEl.innerHTML = Array(4)
+ .fill(
+ `<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></div>`,
+ )
+ .join("");
+}
+
+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");
+ }
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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";
+ }
+ } else {
+ state.calcExpr += v;
+ }
+ $("calc-expr").textContent = state.calcExpr;
+ $("calc-result").textContent = state.calcResult;
+ };
+ });
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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)}...`);
+}
+
+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";
+}
+
+function toolJSON() {
+ 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">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;
+ }
+ }
+ 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)}...`);
+}
+
+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");
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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();
+}
+
+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("");
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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("");
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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);
+ }
+ if (e.key === "Escape") goHome();
+ });
+ document.addEventListener("keydown", (e) => {
+ if (
+ e.key === "/" &&
+ document.activeElement !== searchInput &&
+ document.activeElement !== $("watchlist-input")
+ ) {
+ e.preventDefault();
+ searchInput.focus();
+ }
+ if (e.key === "Escape") goHome();
+ });
+ $("clear-btn").onclick = goHome;
+ $("voice-btn").onclick = voiceSearch;
+}
+
+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();
+}
+
+// ═══════════════════════════════════════════════════════════
+// 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();
+}
+
+init();