Basic search outline config UI implemented.
This commit is contained in:
@@ -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();
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user