Basic search outline config UI implemented.

This commit is contained in:
2021-08-17 02:59:01 -05:00
parent 8a1e5aca15
commit c6b8ca523b
25 changed files with 1047 additions and 520 deletions

View File

@@ -1,3 +1,8 @@
import { apiHttp } from "../services/http";
import Alpine from "alpinejs";
import clone from "just-clone";
const uploadDelay = 500;
const startingSlide = "#quick-picks-slide";
function initInteractiveElements() {
@@ -11,14 +16,251 @@ function initInteractiveElements() {
});
}
function initConfigVisuals() {
const minRatingDisplay = document.querySelector("#configuration #min-rating-display");
const minRatingSlider = document.querySelector("#configuration #min-rating");
const updateDisplay = function () {
minRatingDisplay.innerHTML = `Minimum rating: ${minRatingSlider.value}%`;
};
minRatingSlider.addEventListener("input", updateDisplay);
updateDisplay();
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,
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]);
}
},
validateNumericalInputs() {
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.searchOutlineChangeTimeout = setTimeout(() => {
this.uploadAll();
}, uploadDelay);
},
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.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() {
@@ -46,8 +288,8 @@ function initSlides() {
async function main() {
initInteractiveElements();
initConfigVisuals();
initSlides();
initConfigData();
}
main();

View File

@@ -37,17 +37,6 @@ main {
display: flex;
flex-direction: column;
.tear {
border-top: 1px;
border-top-style: solid;
border-bottom: 1px;
border-bottom-style: solid;
@include themer.themed {
$tear: themer.color-of("background");
border-color: adjust-color($color: $tear, $lightness: -12%, $alpha: 1.0);
background-color: adjust-color($color: $tear, $lightness: -5%, $alpha: 1.0);
}
}
}
footer {
@@ -82,16 +71,24 @@ footer {
}
}
.center-overlay {
position: absolute;
left: 50%;
right: 50%;
bottom: 50%;
top: 50%;
}
.concise {
@extend .container;
max-width: 630px;
width: inherit;
width: 100%;
}
.less-concise {
@extend .container;
max-width: 720px;
width: inherit;
width: 100%;
}
hr {
@@ -111,6 +108,115 @@ hr {
}
}
.tear {
@include themer.themed {
$tear: themer.color-of("background");
background-color: adjust-color($color: $tear, $lightness: -8%, $alpha: 1.0);
box-shadow: 0px 0px 5px 0px adjust-color($color: $tear, $lightness: -25%, $alpha: 1.0) inset;
}
}
input[type="text"].title-input {
@include themer.themed {
color: themer.color-of("text");
}
font-size: 2.5em;
margin-bottom: 1.5rem;
border-style: none;
border-bottom: 1px solid transparent;
background-color: transparent;
text-align: center;
transition: border-bottom-color 0.5s;
&:hover:not(:disabled) {
@include themer.themed {
border-bottom-color: adjust-color($color: themer.color-of("text"), $lightness: 50%);
}
}
&:focus:not(:disabled) {
outline: none;
@include themer.themed {
border-bottom-color: adjust-color($color: themer.color-of("text"));
}
}
&:disabled {
border-bottom-color: transparent;
@include themer.themed {
color: themer.color-of("text");
}
}
}
.clean-radio {
input[type="radio"] {
display: none;
& + label {
transition: border-color 0.5s;
padding: 0.5rem;
border-left: 3px solid;
@include themer.themed {
border-color: themer.color-of("text");
}
cursor: pointer;
}
&:hover + label {
@include themer.themed {
border-color: adjust-color($color: themer.color-of("special"), $lightness: 30%);
}
}
&:checked + label {
@include themer.themed {
border-color: adjust-color($color: themer.color-of("special"), $lightness: 0%);
}
}
}
button.btn {
transition: color 0.5s;
color: transparent;
}
&:hover {
button.btn {
@include themer.themed {
color: themer.color-of("text");
}
}
}
}
.border-right-themed {
border-right: 1px solid;
@include themer.themed {
border-right-color: themer.color-of("text");
}
}
.border-left-themed {
border-left: 1px solid;
@include themer.themed {
border-left-color: themer.color-of("text");
}
}
.border-top-themed {
border-top: 1px solid;
@include themer.themed {
border-top-color: themer.color-of("text");
}
}
.border-bottom-themed {
border-bottom: 1px solid;
@include themer.themed {
border-bottom-color: themer.color-of("text");
}
}
html {
min-height: 100%;
display: flex;