diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.js | 544 |
1 files changed, 272 insertions, 272 deletions
diff --git a/src/main.js b/src/main.js index 4db4edb..aaa9142 100644 --- a/src/main.js +++ b/src/main.js @@ -115,7 +115,7 @@ class Utils { try { // Use Function constructor in a safer way with restricted scope const result = new Function(`return ${safeExpr}`)(); - return typeof result === "number" && !isNaN(result) && isFinite(result) ? result : "ERR"; + return typeof result === "number" && !Number.isNaN(result) && Number.isFinite(result) ? result : "ERR"; } catch { return "ERR"; } @@ -331,6 +331,272 @@ class TradingViewWidget { } } +SOURCES = { + dictionary: { + bang: "!d", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("dictionary", async (q, signal) => { + const response = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`, + { signal }, + ); + if (!response.ok) return []; + const data = await response.json(); + return data.slice(0, 3).flatMap((entry) => + entry.meanings.slice(0, 2).map((meaning) => ({ + meta: { phonetic: entry.phonetic }, + snippet: meaning.definitions[0]?.definition || "", + source: "dictionary", + title: `${entry.word} [${meaning.partOfSpeech}]`, + url: entry.sourceUrls?.[0] || "#", + })), + ); + }), + name: "DICTIONARY", + }, + github: { + bang: "!g", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("github", async (q, signal) => { + const response = await fetch( + `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`, + { signal }, + ); + const data = await response.json(); + return (data.items || []).map((item) => ({ + meta: { forks: item.forks_count, lang: item.language, stars: item.stargazers_count }, + snippet: item.description || "NO DESCRIPTION", + source: "github", + title: item.full_name, + url: item.html_url, + })); + }), + name: "GITHUB", + }, + hackernews: { + bang: "!hn", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("hackernews", async (q, signal) => { + const response = await fetch( + `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`, + { signal }, + ); + const data = await response.json(); + return (data.hits || []) + .filter((hit) => hit.title) + .map((hit) => ({ + meta: { comments: hit.num_comments, points: hit.points }, + snippet: `${hit.points || 0} POINTS // ${hit.author}`, + source: "hackernews", + title: hit.title, + url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, + })); + }), + name: "HACKERNEWS", + }, + news: { + bang: "!n", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("news", async (q, signal) => { + const [hn, rd] = await Promise.all([ + fetch( + `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`, + { signal }, + ) + .then((r) => r.json()) + .catch(() => ({ hits: [] })), + fetch( + `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`, + { signal }, + ) + .then((r) => r.json()) + .catch(() => ({ data: { children: [] } })), + ]); + + const hnResults = (hn.hits || []).map((hit) => ({ + meta: { date: hit.created_at_i, lang: "HN" }, + snippet: `HACKERNEWS // ${hit.points || 0} PTS // ${Utils.timeAgo(hit.created_at_i * 1000)}`, + source: "news", + title: hit.title, + url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, + })); + + const redditResults = (rd.data?.children || []).map((child) => ({ + meta: { date: child.data.created_utc, lang: "RD" }, + snippet: `REDDIT r/${child.data.subreddit.toUpperCase()} // ${child.data.score} UPVOTES`, + source: "news", + title: child.data.title, + url: `https://reddit.com${child.data.permalink}`, + })); + + return [...hnResults, ...redditResults].sort((a, b) => b.meta.date - a.meta.date); + }), + name: "AGGREGATE NEWS", + }, + npm: { + bang: "!npm", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("npm", async (q, signal) => { + const response = await fetch( + `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`, + { signal }, + ); + const data = await response.json(); + return (data.objects || []).map((obj) => ({ + meta: { version: obj.package.version }, + snippet: obj.package.description || "NO DESCRIPTION", + source: "npm", + title: obj.package.name, + url: `https://npmjs.com/package/${obj.package.name}`, + })); + }), + name: "NPM", + }, + openlibrary: { + bang: "!b", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("openlibrary", async (q, signal) => { + const response = await fetch( + `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`, + { signal }, + ); + const data = await response.json(); + return (data.docs || []).map((book) => ({ + meta: { year: book.first_publish_year }, + snippet: `BY ${(book.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, + source: "openlibrary", + title: book.title, + url: `https://openlibrary.org${book.key}`, + })); + }), + name: "OPENLIBRARY", + }, + reddit: { + bang: "!r", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("reddit", async (q, signal) => { + const response = await fetch( + `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`, + { signal }, + ); + const data = await response.json(); + return (data.data?.children || []).map((child) => ({ + meta: { comments: child.data.num_comments, score: child.data.score }, + snippet: child.data.selftext?.substring(0, 200) || `r/${child.data.subreddit}`, + source: "reddit", + title: child.data.title, + url: `https://reddit.com${child.data.permalink}`, + })); + }), + name: "REDDIT", + }, + stackoverflow: { + bang: "!so", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("stackoverflow", async (q, signal) => { + const response = await fetch( + `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`, + { signal }, + ); + const data = await response.json(); + return (data.items || []).map((item) => ({ + meta: { answers: item.answer_count, score: item.score }, + snippet: `${item.answer_count} ANSWERS // ${item.view_count} VIEWS`, + source: "stackoverflow", + title: Utils.decodeHTML(item.title), + url: item.link, + })); + }), + name: "STACKOVERFLOW", + }, + wikipedia: { + bang: "!w", + count: 0, + enabled: true, + fetch: this.createSourceFetchHandler("wikipedia", async (q, signal) => { + const response = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`, + { signal }, + ); + const data = await response.json(); + return (data.query?.search || []).map((item) => ({ + meta: { words: item.wordcount }, + snippet: item.snippet.replace(/<[^>]*>/g, ""), + source: "wikipedia", + title: item.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(item.title)}`, + })); + }), + instant: async (q, signal) => { + const searchResponse = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`, + { signal }, + ); + const searchData = await searchResponse.json(); + if (!searchData.query?.search?.length) return null; + + const title = searchData.query.search[0].title; + const pageResponse = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`, + { signal }, + ); + const pageData = await pageResponse.json(); + const page = Object.values(pageData.query.pages)[0]; + + return { + content: page.extract, + source: "WIKIPEDIA", + title: page.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, + }; + }, + name: "WIKIPEDIA", + }, +}; + +// Stock exchanges mapping +STOCK_EXHANGES = { + 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", +}; + // ═══════════════════════════════════════════════════════════ // SEARCH CLASS // ═══════════════════════════════════════════════════════════ @@ -339,7 +605,9 @@ class Search { /** * Create a new Search instance */ - constructor() { + constructor(sources, stockExchanges) { + this.sources = sources; + this.stockExchanges = stockExchanges; // State this.state = { calcExpr: "", @@ -366,274 +634,6 @@ class Search { this.mainEl = document.getElementById("main-content"); this.statsBar = document.getElementById("stats-bar"); - // Stock exchanges mapping - this.stockExchanges = { - AAPL: "NASDAQ", - ADA: "CRYPTO", - AMD: "NASDAQ", - AMZN: "NASDAQ", - BAC: "NYSE", - BTC: "CRYPTO", - DIS: "NYSE", - DOGE: "CRYPTO", - ETH: "CRYPTO", - EURUSD: "FX", - GBPUSD: "FX", - GOOG: "NASDAQ", - GOOGL: "NASDAQ", - INTC: "NASDAQ", - JNJ: "NYSE", - JPM: "NYSE", - KO: "NYSE", - META: "NASDAQ", - MSFT: "NASDAQ", - NFLX: "NASDAQ", - NVDA: "NASDAQ", - PYPL: "NASDAQ", - QQQ: "NASDAQ", - SOL: "CRYPTO", - SPY: "AMEX", - TSLA: "NASDAQ", - USDJPY: "FX", - V: "NYSE", - WMT: "NYSE", - XOM: "NYSE", - XRP: "CRYPTO", - }; - - // Sources configuration - this.sources = { - dictionary: { - bang: "!d", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("dictionary", async (q, signal) => { - const response = await fetch( - `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`, - { signal }, - ); - if (!response.ok) return []; - const data = await response.json(); - return data.slice(0, 3).flatMap((entry) => - entry.meanings.slice(0, 2).map((meaning) => ({ - meta: { phonetic: entry.phonetic }, - snippet: meaning.definitions[0]?.definition || "", - source: "dictionary", - title: `${entry.word} [${meaning.partOfSpeech}]`, - url: entry.sourceUrls?.[0] || "#", - })), - ); - }), - name: "DICTIONARY", - }, - github: { - bang: "!g", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("github", async (q, signal) => { - const response = await fetch( - `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`, - { signal }, - ); - const data = await response.json(); - return (data.items || []).map((item) => ({ - meta: { forks: item.forks_count, lang: item.language, stars: item.stargazers_count }, - snippet: item.description || "NO DESCRIPTION", - source: "github", - title: item.full_name, - url: item.html_url, - })); - }), - name: "GITHUB", - }, - hackernews: { - bang: "!hn", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("hackernews", async (q, signal) => { - const response = await fetch( - `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`, - { signal }, - ); - const data = await response.json(); - return (data.hits || []) - .filter((hit) => hit.title) - .map((hit) => ({ - meta: { comments: hit.num_comments, points: hit.points }, - snippet: `${hit.points || 0} POINTS // ${hit.author}`, - source: "hackernews", - title: hit.title, - url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, - })); - }), - name: "HACKERNEWS", - }, - news: { - bang: "!n", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("news", async (q, signal) => { - const [hn, rd] = await Promise.all([ - fetch( - `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`, - { signal }, - ) - .then((r) => r.json()) - .catch(() => ({ hits: [] })), - fetch( - `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`, - { signal }, - ) - .then((r) => r.json()) - .catch(() => ({ data: { children: [] } })), - ]); - - const hnResults = (hn.hits || []).map((hit) => ({ - meta: { date: hit.created_at_i, lang: "HN" }, - snippet: `HACKERNEWS // ${hit.points || 0} PTS // ${Utils.timeAgo(hit.created_at_i * 1000)}`, - source: "news", - title: hit.title, - url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`, - })); - - const redditResults = (rd.data?.children || []).map((child) => ({ - meta: { date: child.data.created_utc, lang: "RD" }, - snippet: `REDDIT r/${child.data.subreddit.toUpperCase()} // ${child.data.score} UPVOTES`, - source: "news", - title: child.data.title, - url: `https://reddit.com${child.data.permalink}`, - })); - - return [...hnResults, ...redditResults].sort((a, b) => b.meta.date - a.meta.date); - }), - name: "AGGREGATE NEWS", - }, - npm: { - bang: "!npm", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("npm", async (q, signal) => { - const response = await fetch( - `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`, - { signal }, - ); - const data = await response.json(); - return (data.objects || []).map((obj) => ({ - meta: { version: obj.package.version }, - snippet: obj.package.description || "NO DESCRIPTION", - source: "npm", - title: obj.package.name, - url: `https://npmjs.com/package/${obj.package.name}`, - })); - }), - name: "NPM", - }, - openlibrary: { - bang: "!b", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("openlibrary", async (q, signal) => { - const response = await fetch( - `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`, - { signal }, - ); - const data = await response.json(); - return (data.docs || []).map((book) => ({ - meta: { year: book.first_publish_year }, - snippet: `BY ${(book.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, - source: "openlibrary", - title: book.title, - url: `https://openlibrary.org${book.key}`, - })); - }), - name: "OPENLIBRARY", - }, - reddit: { - bang: "!r", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("reddit", async (q, signal) => { - const response = await fetch( - `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`, - { signal }, - ); - const data = await response.json(); - return (data.data?.children || []).map((child) => ({ - meta: { comments: child.data.num_comments, score: child.data.score }, - snippet: child.data.selftext?.substring(0, 200) || `r/${child.data.subreddit}`, - source: "reddit", - title: child.data.title, - url: `https://reddit.com${child.data.permalink}`, - })); - }), - name: "REDDIT", - }, - stackoverflow: { - bang: "!so", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("stackoverflow", async (q, signal) => { - const response = await fetch( - `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`, - { signal }, - ); - const data = await response.json(); - return (data.items || []).map((item) => ({ - meta: { answers: item.answer_count, score: item.score }, - snippet: `${item.answer_count} ANSWERS // ${item.view_count} VIEWS`, - source: "stackoverflow", - title: Utils.decodeHTML(item.title), - url: item.link, - })); - }), - name: "STACKOVERFLOW", - }, - wikipedia: { - bang: "!w", - count: 0, - enabled: true, - fetch: this.createSourceFetchHandler("wikipedia", async (q, signal) => { - const response = await fetch( - `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`, - { signal }, - ); - const data = await response.json(); - return (data.query?.search || []).map((item) => ({ - meta: { words: item.wordcount }, - snippet: item.snippet.replace(/<[^>]*>/g, ""), - source: "wikipedia", - title: item.title, - url: `https://en.wikipedia.org/wiki/${encodeURIComponent(item.title)}`, - })); - }), - instant: async (q, signal) => { - const searchResponse = await fetch( - `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`, - { signal }, - ); - const searchData = await searchResponse.json(); - if (!searchData.query?.search?.length) return null; - - const title = searchData.query.search[0].title; - const pageResponse = await fetch( - `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`, - { signal }, - ); - const pageData = await pageResponse.json(); - const page = Object.values(pageData.query.pages)[0]; - - return { - content: page.extract, - source: "WIKIPEDIA", - title: page.title, - url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, - }; - }, - name: "WIKIPEDIA", - }, - }; - - // Initialize this.init(); } @@ -820,7 +820,7 @@ class Search { */ updateUrlWithQuery(query) { const url = new URL(window.location); - if (query && query.trim()) { + if (query?.trim()) { url.searchParams.set("q", query); } else { url.searchParams.delete("q"); @@ -1739,7 +1739,7 @@ class Search { // ═══════════════════════════════════════════════════════════ // Create global instance -const searchEngine = new Search(); +const searchEngine = new Search(SOURCES, STOCK_EXHANGES); // Expose methods to global scope for onclick handlers window.toggleTheme = () => searchEngine.toggleTheme(); |
