aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-12-14 14:50:33 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-12-14 14:50:33 +0200
commit693a048d4aaff9f6cfcdf5093638fbe80f28f91b (patch)
tree71322108cefc109d12fe0b2890eba2df531b1d01 /src
parentbf67302ce6757ef77d84221ead640f1e84df6265 (diff)
downloadpage-693a048d4aaff9f6cfcdf5093638fbe80f28f91b.tar.zst
Structure better
Diffstat (limited to 'src')
-rw-r--r--src/main.js544
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();