import { apiHttp } from "../services/http"; import Alpine from "alpinejs"; import clone from "just-clone"; const UPLOAD_DELAY = 1500; const START_SLIDE = "#quick-picks-slide"; function initInteractiveElements() { let configurationToggle = document.getElementById("configuration-toggle"); let configurationElem = document.getElementById("configuration"); configurationElem.addEventListener("show.bs.collapse", function () { configurationToggle.classList.add("active"); }); configurationElem.addEventListener("hidden.bs.collapse", function () { configurationToggle.classList.remove("active"); }); } function initConfigData() { document.addEventListener("alpine:init", () => { Alpine.data("search", () => ({ loggedIn: false, query: "", searchOutline: { ready: false, filters: { "currency": 0, "minRating": 80, "keepUnrated": true, "enableUpperPrice": false, "upperPrice": 0, "lowerPrice": 0, "minPurchases": 0, "keepUnknownPurchaseCount": true, "minReviews": 0, "keepUnknownReviewCount": true, "enableMaxShipping": false, "maxShippingFee": 0, "keepUnknownShipping": true }, shopToggles: {}, }, deletingSearchOutline: false, creatingSearchOutline: false, updatingLastUsed: false, changingName: false, updatingFilters: false, updatingDisabledShops: false, searchOutlines: [], selectedSearchOutline: 0, serverSearchOutlineName: null, resultsQuery: null, results: { bestPrice: null, }, searchOutlineChangeTimeout: null, timeoutInProgress: false, hasResults() { return this.resultsQuery !== null; }, submitSearch() { // TODO: implement search Web API call. this.resultsQuery = this.query; console.log("Search requested."); }, SearchOutlineNameChange() { if (this.validateSearchOutlineName()) { this.changeSearchOutlineName(this.serverSearchOutlineName, this.searchOutlines[this.selectedSearchOutline]); } }, validateAllNumericalInputs() { if (!this.searchOutline.filters.lowerPrice) this.searchOutline.filters.lowerPrice = 0; if (!this.searchOutline.filters.upperPrice) this.searchOutline.filters.upperPrice = 0; if (!this.searchOutline.filters.maxShippingFee) this.searchOutline.filters.maxShippingFee = 0; if (!this.searchOutline.filters.minPurchases) this.searchOutline.filters.minPurchases = 0; if (!this.searchOutline.filters.minRating) this.searchOutline.filters.minRating = 0; if (!this.searchOutline.filters.minReviews) this.searchOutline.filters.minReviews = 0; }, validateSearchOutlineName() { let clonedSearchOutlines = this.searchOutlines.slice(); clonedSearchOutlines.splice(this.selectedSearchOutline, 1); if (this.searchOutlines[this.selectedSearchOutline].length < 1 || clonedSearchOutlines.includes(this.searchOutlines[this.selectedSearchOutline])) { this.searchOutlines[this.selectedSearchOutline] = this.serverSearchOutlineName; return false; } return true; }, searchOutlineChanged() { if (!this.loggedIn) return; if (this.searchOutlineChangeTimeout != null) { clearTimeout(this.searchOutlineChangeTimeout); } this.timeoutInProgress = true; this.searchOutlineChangeTimeout = setTimeout(() => { this.uploadAll(); this.timeoutInProgress = false; }, UPLOAD_DELAY); }, uploadAll() { let name = this.searchOutlines[this.selectedSearchOutline]; this.uploadFilters(name, clone(this.searchOutline.filters)); this.uploadDisabledShops(name, clone(this.searchOutline.shopToggles)); }, async uploadFilters(name, filters) { if (!this.loggedIn) return; this.validateAllNumericalInputs(); this.updatingFilters = true; let uploadFilterResponse = await apiHttp.put(`SearchOutline/${name}/Filters`, filters); this.updatingFilters = false; if (uploadFilterResponse.status != 204) { throw `Error while attempting to upload filters. Response code ${uploadFilterResponse.status} (expected 204).`; } }, async uploadDisabledShops(name, disabledShops) { if (!this.loggedIn) return; this.updatingDisabledShops = true; let disabledShopSet = []; Object.keys(disabledShops).forEach(key => { if (!this.searchOutline.shopToggles[key]) { disabledShopSet.push(key); } }); let uploadDisabledShopsResponse = await apiHttp.put(`SearchOutline/${name}/DisabledShops`, disabledShopSet); this.updatingDisabledShops = false; if (uploadDisabledShopsResponse.status != 204) { throw `Error while attempting to upload disabled shops. Response code ${uploadDisabledShopsResponse.status} (expected 204).`; } }, async createSearchOutline(name) { if (!this.loggedIn) return; this.creatingSearchOutline = true; let createRequest = await apiHttp.post("SearchOutline/" + name); this.creatingSearchOutline = false; if (createRequest.status != 204) { throw `Could not create profile. Response code ${createRequest.status} (expected 204).`; } this.searchOutlines.push(name); }, async updateLastUsed(name) { if (!this.loggedIn) return; this.updatingLastUsed = true; let lastUsedRequest = await apiHttp.put("SearchOutline/" + name + "/LastUsed"); this.updatingLastUsed = false; if (lastUsedRequest.status != 204) { throw `Could not update last used search outline. Received status code ${lastUsedRequest.status} (expected 204).`; } }, async changeSearchOutlineName(old, current) { if (!this.loggedIn) return; this.changingName = true; let nameChangeRequest = await apiHttp.put(`SearchOutline/${old}/Name/${current}`); this.changingName = false; if (nameChangeRequest.status != 204) { throw `Could not update name on server side. Received ${nameChangeRequest.status} (expected 204).`; } this.serverSearchOutlineName = current; }, async loadSearchOutline(name) { this.searchOutline.ready = false; if (!this.loggedIn) { let defaultNameRequest = await apiHttp.get("SearchOutline/DefaultName"); if (defaultNameRequest.status != 200) { console.error(`Could not load default search outline name. Got response code ${defaultNameRequest.status} (Expected 200).`); return; } this.searchOutlines.push(defaultNameRequest.data); let disabledShopsResponse = await apiHttp.get("SearchOutline/DefaultDisabledShops"); let availableShops = (await apiHttp.get("Search/AvailableShops")).data; if (disabledShopsResponse.status == 200) { availableShops.forEach(shopName => { this.searchOutline.shopToggles[shopName] = !disabledShopsResponse.data.includes(shopName); }); } else { console.error(`Could not fetch default disabled shops for "${name}". Status code: ${disabledShopsResponse.status} (Expected 200)`); return; } } else { if (this.searchOutlineChangeTimeout != null) { clearTimeout(this.searchOutlineChangeTimeout); this.uploadAll(); } let filterResponse = await apiHttp.get("SearchOutline/" + name + "/Filters"); if (filterResponse.status == 200) { this.searchOutline.filters = filterResponse.data; } else { console.error(`Could not fetch filter for "${name}". Status code: ${filterResponse.status} (Expected 200)`); return; } let disabledShopsResponse = await apiHttp.get("SearchOutline/" + name + "/DisabledShops"); let availableShops = (await apiHttp.get("Search/AvailableShops")).data; if (disabledShopsResponse.status == 200) { availableShops.forEach(shopName => { this.searchOutline.shopToggles[shopName] = !disabledShopsResponse.data.includes(shopName); }); } else { console.error(`Could not fetch disabled shops for "${name}". Status code: ${disabledShopsResponse.status} (Expected 200)`); return; } await this.updateLastUsed(name); } this.serverSearchOutlineName = name; this.searchOutline.ready = true; }, createSearchOutlineWithGeneratedName() { apiHttp.get("SearchOutline/DefaultName/").then((response) => { if (response.status != 200) { throw `Could not get a default name. Response code ${response.status} (expected 200).`; } this.createSearchOutline(response.data); }); }, deleteSearchOutline(name) { this.deletingSearchOutline = true; let beforeDelete = this.searchOutlines[this.selectedSearchOutline]; if (this.selectedSearchOutline == this.searchOutlines.length - 1 && this.searchOutlines.indexOf(name) <= this.selectedSearchOutline) { this.selectedSearchOutline -= 1; } apiHttp.delete(`SearchOutline/${name}`).then((results) => { this.deletingSearchOutline = false; if (results.status != 204) { throw `Unable to delete ${name}. Received status ${results.status} (expected 204)`; } this.searchOutlines.splice(this.searchOutlines.indexOf(name), 1); if (beforeDelete !== this.searchOutlines[this.selectedSearchOutline]) { this.loadSearchOutline(this.searchOutlines[this.selectedSearchOutline]).then(() => { this.deletingSearchOutline = false; }); } else { this.deletingSearchOutline = false; } }); }, async init() { // TODO: Test logged in outline sequence and logged out outline sequence. this.loggedIn = (await apiHttp.get("User/LoggedIn")).data; if (this.loggedIn) { this.searchOutlines = (await apiHttp.get("SearchOutline/Names")).data; if (this.searchOutlines.length == 0) { let name = (await apiHttp.get("SearchOutline/DefaultName")).data; try { await this.createSearchOutline(name); await this.updateLastUsed(name); } catch (error) { console.error(error); return; } } else { let lastUsedRequest = await apiHttp.get("SearchOutline/LastUsed"); if (lastUsedRequest.status == 200) { this.selectedSearchOutline = this.searchOutlines.indexOf(lastUsedRequest.data); } else { console.warn(`Could not load name of last used search outline. Got response code ${lastUsedRequest.status} (Expected 200). Using "${this.searchOutlines[0]}".`); let putlastUsedRequest = await apiHttp.put("SearchOutline/" + this.searchOutlines[this.selectedSearchOutline] + "/LastUsed"); if (putlastUsedRequest.status != 204) { console.error(`Could not update last used search outline. Received status code ${putlastUsedRequest.status} (Expected 204).`); return; } } } } this.loadSearchOutline(this.searchOutlines[this.selectedSearchOutline]); } })); }); Alpine.start(); } function initSlides() { document.querySelectorAll("#content-pages > .selectors > .nav-item > button").forEach(tabElem => { tabElem.addEventListener("click", () => { const destUrl = new URL(tabElem.getAttribute("data-bs-target"), window.location.href); if (location.href === destUrl.href) return; history.pushState({}, document.title, destUrl); }); }); const goTo = () => { const match = location.href.match("(#[\\w-]+)"); const idAnchor = match && match[1] ? match[1] : START_SLIDE; document.querySelector("#content-pages > .selectors > .nav-item > .active")?.classList.remove("active"); document.querySelector("#content-pages > .multipage-slides > .active.show")?.classList.remove("active", "show"); document.querySelector(`#content-pages > .selectors > .nav-item > [data-bs-target="${idAnchor}"]`).classList.add("active"); document.querySelector(`#content-pages > .multipage-slides > ${idAnchor}`).classList.add("active", "show"); }; window.addEventListener("popstate", goTo); goTo(); require("bootstrap/js/dist/tab.js"); document.querySelector("#content-pages").classList.remove("invisible"); } async function main() { initInteractiveElements(); initSlides(); initConfigData(); } main();