From 17d72b30766197ba0d537a9bb62cbebce43b907b Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Mon, 2 May 2022 19:24:57 +0700 Subject: [PATCH 1/8] feat(index): add search function --- index.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index d3e17774..178abe44 100644 --- a/index.js +++ b/index.js @@ -150,6 +150,7 @@ class Grid extends react.Component { super(props); Object.assign(this, props); this.state = { + searchValue: "", cards: [], tabs: CONFIG.tabs, rest: true, @@ -537,14 +538,15 @@ class Grid extends react.Component { }, SETTINGS_ICON), // End of marketplace-header__right ), - // TODO: Add search bar and sort functionality - // react.createElement("div", { - // className: "searchbar--bar__wrapper", - // }, react.createElement("input", { - // className: "searchbar-bar", - // type: "text", - // placeholder: "Search for Extensions?", - // })), + react.createElement("div", { + className: "searchbar--bar__wrapper", + }, react.createElement("input", { + className: "searchbar-bar", + type: "text", + placeholder: "Search", + value: this.state.searchValue, + onChange: (event) => this.setState({ searchValue: event.target.value }), + })), ), [ // Add a header and grid for each card type if it has any cards { handle: "extension", name: "Extensions" }, @@ -552,6 +554,18 @@ class Grid extends react.Component { { handle: "snippet", name: "Snippets" }, ].map((cardType) => { const cardsOfType = cardList.filter((card) => card.props.type === cardType.handle) + .filter((card) => { // Search filter + const { searchValue } = this.state; + const { title, user } = card.props; + + if (searchValue.trim() === "") + return card; + else if ( + title.toLowerCase().includes(searchValue.trim().toLowerCase()) || + user.toLowerCase().includes(searchValue.trim().toLowerCase()) + ) + return card; + }) .map((card) => { // Clone the cards and update the prop to trigger re-render // TODO: is it possible to only re-render the theme cards whose status have changed? @@ -826,7 +840,7 @@ async function getThemeRepos(page = 1) { return filteredResults; } function addToSessionStorage(items, key) { - if (!items || items == null) return; + if (!items) return; items.forEach(item => { if (!key) key = `${items.user}-${items.repo}`; // If the key already exists, it will append to it instead of overwriting it From a410bbd74956270b253c5a27652da044180a9ebd Mon Sep 17 00:00:00 2001 From: Nam Anh <77577746+kyrie25@users.noreply.github.com> Date: Mon, 2 May 2022 20:48:57 +0700 Subject: [PATCH 2/8] refactor: apply suggestion Co-authored-by: Isaac --- index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 178abe44..7fc89ad4 100644 --- a/index.js +++ b/index.js @@ -558,13 +558,11 @@ class Grid extends react.Component { const { searchValue } = this.state; const { title, user } = card.props; - if (searchValue.trim() === "") - return card; - else if ( + if ( + searchValue.trim() === "" || title.toLowerCase().includes(searchValue.trim().toLowerCase()) || user.toLowerCase().includes(searchValue.trim().toLowerCase()) - ) - return card; + ) return card; }) .map((card) => { // Clone the cards and update the prop to trigger re-render From 0c734ecf1cbadd34d4bf81bdac02e9cbe0634644 Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Tue, 3 May 2022 17:21:12 +0700 Subject: [PATCH 3/8] feat: use GitHub API results --- index.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 7fc89ad4..532952f7 100644 --- a/index.js +++ b/index.js @@ -141,6 +141,7 @@ let requestPage = null; const ITEMS_PER_REQUEST = 100; let BLACKLIST = []; +let searchQuery = ""; // eslint-disable-next-line no-redeclare, no-unused-vars let gridUpdateTabs, gridUpdatePostsVisual; @@ -249,7 +250,8 @@ class Grid extends react.Component { // Returns the next page number to fetch, or null if at end // TODO: maybe we should rename `loadPage()`, since it's slightly confusing when we have github pages as well async loadPage(queue) { - if (CONFIG.activeTab === "Extensions") { + switch (CONFIG.activeTab) { + case "Extensions": { let pageOfRepos = await getExtensionRepos(requestPage); for (const repo of pageOfRepos.items) { let extensions = await fetchExtensionManifest(repo.contents_url, repo.default_branch, repo.stargazers_count); @@ -276,7 +278,8 @@ class Grid extends react.Component { console.log(`Parsed ${soFarResults}/${pageOfRepos.total_count} extensions`); if (remainingResults > 0) return currentPage + 1; else console.log("No more extension results"); - } else if (CONFIG.activeTab === "Installed") { + break; + } case "Installed": { const installedStuff = { theme: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedThemes, []), extension: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []), @@ -299,10 +302,10 @@ class Grid extends react.Component { }); } } - + break; // Don't need to return a page number because // installed extension do them all in one go, since it's local - } else if (CONFIG.activeTab == "Themes") { + } case "Themes": { let pageOfRepos = await getThemeRepos(requestPage); for (const repo of pageOfRepos.items) { @@ -327,7 +330,8 @@ class Grid extends react.Component { console.log(`Parsed ${soFarResults}/${pageOfRepos.total_count} themes`); if (remainingResults > 0) return currentPage + 1; else console.log("No more theme results"); - } else if (CONFIG.activeTab == "Snippets") { + break; + } case "Snippets": { let snippets = await fetchCssSnippets(); if (requestQueue.length > 1 && queue !== requestQueue[0]) { @@ -337,8 +341,8 @@ class Grid extends react.Component { if (snippets && snippets.length) { snippets.forEach((snippet) => this.appendCard(snippet, "snippet")); } - - } + break; + }} this.setState({ rest: true, endOfList: true }); endOfList = true; @@ -545,7 +549,13 @@ class Grid extends react.Component { type: "text", placeholder: "Search", value: this.state.searchValue, - onChange: (event) => this.setState({ searchValue: event.target.value }), + onChange: (event) => { + this.setState({ searchValue: event.target.value }); + searchQuery = event.target.value; + }, + onKeyPress: (event) => { + if (event.key === "Enter") this.newRequest(ITEMS_PER_REQUEST); + }, })), ), [ // Add a header and grid for each card type if it has any cards @@ -622,7 +632,10 @@ class Grid extends react.Component { */ async function getExtensionRepos(page = 1) { // www is needed or it will block with "cross-origin" error. - let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-extensions")}&per_page=${ITEMS_PER_REQUEST}`; + let url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`topic:spicetify-extensions`)}&per_page=${ITEMS_PER_REQUEST}`; + if (searchQuery?.trim() !== "") { + url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery} topic:spicetify-extensions`)}&per_page=${ITEMS_PER_REQUEST}`; + } // We can test multiple pages with this URL (58 results), as well as broken iamges etc. // let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify")}`; @@ -816,7 +829,9 @@ async function fetchThemeManifest(contents_url, branch, stars) { */ async function getThemeRepos(page = 1) { let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-themes")}&per_page=${ITEMS_PER_REQUEST}`; - + if (searchQuery?.trim() !== "") { + url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery} topic:spicetify-themes`)}&per_page=${ITEMS_PER_REQUEST}`; + } // We can test multiple pages with this URL (58 results), as well as broken iamges etc. // let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify")}`; if (page) url += `&page=${page}`; From 0856bd4c8817160b3e31dd9406624460308c4ee8 Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Tue, 3 May 2022 18:37:20 +0700 Subject: [PATCH 4/8] refactor: safety checks --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index 532952f7..a8c7dbee 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ /// /// /// +/// /// /// /// @@ -646,6 +647,7 @@ async function getExtensionRepos(page = 1) { const allRepos = await fetch(url).then(res => res.json()).catch(() => []); if (!allRepos.items) { Spicetify.showNotification("Too Many Requests, Cool Down."); + return null; } const filteredResults = { ...allRepos, @@ -841,6 +843,7 @@ async function getThemeRepos(page = 1) { const allThemes = await fetch(url).then(res => res.json()).catch(() => []); if (!allThemes.items) { Spicetify.showNotification("Too Many Requests, Cool Down."); + return null; } const filteredResults = { ...allThemes, From b24a380acd1784caa245d64445c80e27d60ec01c Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Tue, 3 May 2022 21:21:09 +0700 Subject: [PATCH 5/8] feat: refresh results when all queries are deleted --- index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index a8c7dbee..c7c0278b 100644 --- a/index.js +++ b/index.js @@ -142,7 +142,10 @@ let requestPage = null; const ITEMS_PER_REQUEST = 100; let BLACKLIST = []; + +// Search variables let searchQuery = ""; +let requested = false; // eslint-disable-next-line no-redeclare, no-unused-vars let gridUpdateTabs, gridUpdatePostsVisual; @@ -554,8 +557,18 @@ class Grid extends react.Component { this.setState({ searchValue: event.target.value }); searchQuery = event.target.value; }, - onKeyPress: (event) => { - if (event.key === "Enter") this.newRequest(ITEMS_PER_REQUEST); + onKeyDown: (event) => { + if (event.key === "Enter") { + this.newRequest(ITEMS_PER_REQUEST); + requested = true; + } else if ( // Refreshes result when user deletes all queries + ((event.key === "Backspace") || (event.key === "Delete")) && + requested && + this.state.searchValue.trim() === "" + ) { + this.newRequest(ITEMS_PER_REQUEST); + requested = false; + } }, })), ), From 96610fe4dd4af24499565f36c6168406751d894c Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Thu, 5 May 2022 12:26:42 +0700 Subject: [PATCH 6/8] feat: use loading icon when fetching live results --- index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.js b/index.js index c7c0278b..9958214b 100644 --- a/index.js +++ b/index.js @@ -559,6 +559,8 @@ class Grid extends react.Component { }, onKeyDown: (event) => { if (event.key === "Enter") { + this.setState({ endOfList: false }); + endOfList = false; this.newRequest(ITEMS_PER_REQUEST); requested = true; } else if ( // Refreshes result when user deletes all queries @@ -566,6 +568,8 @@ class Grid extends react.Component { requested && this.state.searchValue.trim() === "" ) { + this.setState({ endOfList: false }); + endOfList = false; this.newRequest(ITEMS_PER_REQUEST); requested = false; } From c5b6a95d61fde0330486369dd6c6885c07aa1252 Mon Sep 17 00:00:00 2001 From: Nam Anh <77577746+kyrie25@users.noreply.github.com> Date: Fri, 6 May 2022 06:10:53 +0700 Subject: [PATCH 7/8] refactor: apply suggestions Co-authored-by: Isaac --- index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 9958214b..16feec71 100644 --- a/index.js +++ b/index.js @@ -650,9 +650,10 @@ class Grid extends react.Component { */ async function getExtensionRepos(page = 1) { // www is needed or it will block with "cross-origin" error. - let url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`topic:spicetify-extensions`)}&per_page=${ITEMS_PER_REQUEST}`; + let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-extensions")}&per_page=${ITEMS_PER_REQUEST}`; if (searchQuery?.trim() !== "") { - url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery} topic:spicetify-extensions`)}&per_page=${ITEMS_PER_REQUEST}`; + url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery}+topic:spicetify-extensions`)}&per_page=${ITEMS_PER_REQUEST}`; + } // We can test multiple pages with this URL (58 results), as well as broken iamges etc. @@ -849,7 +850,7 @@ async function fetchThemeManifest(contents_url, branch, stars) { async function getThemeRepos(page = 1) { let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-themes")}&per_page=${ITEMS_PER_REQUEST}`; if (searchQuery?.trim() !== "") { - url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery} topic:spicetify-themes`)}&per_page=${ITEMS_PER_REQUEST}`; + url = `https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchQuery}+topic:spicetify-themes`)}&per_page=${ITEMS_PER_REQUEST}`; } // We can test multiple pages with this URL (58 results), as well as broken iamges etc. // let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify")}`; From 75ed9b20c6b9a206c1a3cebdd9d7f04b2b1b0320 Mon Sep 17 00:00:00 2001 From: Nam Anh Date: Fri, 6 May 2022 10:31:53 +0700 Subject: [PATCH 8/8] refactor: split search function --- index.js | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 16feec71..0e93de14 100644 --- a/index.js +++ b/index.js @@ -164,6 +164,8 @@ class Grid extends react.Component { activeScheme: CONFIG.theme.activeScheme, activeThemeKey: CONFIG.theme.activeThemeKey, }; + + this.handleSearch = this.handleSearch.bind(this); } // TODO: should I put this in Grid state? @@ -518,6 +520,25 @@ class Grid extends react.Component { return this.state.activeScheme; } + /** + * @param {import("react").KeyboardEvent} event + */ + handleSearch (event) { + if (event.key === "Enter") { + this.setState({ endOfList: false }); + this.newRequest(ITEMS_PER_REQUEST); + requested = true; + } else if ( // Refreshes result when user deletes all queries + ((event.key === "Backspace") || (event.key === "Delete")) && + requested && + this.state.searchValue.trim() === "" + ) { + this.setState({ endOfList: false }); + this.newRequest(ITEMS_PER_REQUEST); + requested = false; + } + } + render() { return react.createElement("section", { className: "contentSpacing", @@ -557,23 +578,7 @@ class Grid extends react.Component { this.setState({ searchValue: event.target.value }); searchQuery = event.target.value; }, - onKeyDown: (event) => { - if (event.key === "Enter") { - this.setState({ endOfList: false }); - endOfList = false; - this.newRequest(ITEMS_PER_REQUEST); - requested = true; - } else if ( // Refreshes result when user deletes all queries - ((event.key === "Backspace") || (event.key === "Delete")) && - requested && - this.state.searchValue.trim() === "" - ) { - this.setState({ endOfList: false }); - endOfList = false; - this.newRequest(ITEMS_PER_REQUEST); - requested = false; - } - }, + onKeyDown: (event) => this.handleSearch(event), })), ), [ // Add a header and grid for each card type if it has any cards